Skip to content

Commit 9bedf98

Browse files
minal-kyadakrummas
authored andcommitted
Send client warnings when writing to a large partition
Patch by Minal Kyada; reviewed by David Capwell and marcuse for CASSANDRA-17258
1 parent 87d79f3 commit 9bedf98

36 files changed

Lines changed: 2489 additions & 94 deletions

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
5.1
2+
* Send client warnings when writing to a large partition (CASSANDRA-17258)
23
* Harden the possible range of values for max dictionary size and max total sample size for dictionary training (CASSANDRA-21194)
34
* Implement a guardrail ensuring that minimum training frequency parameter is provided in ZstdDictionaryCompressor (CASSANDRA-21192)
45
* Replace manual referencing with ColumnFamilyStore.selectAndReference when training a dictionary (CASSANDRA-21188)

src/java/org/apache/cassandra/config/Config.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,11 @@ public static class SSTableConfig
579579
public volatile int tombstone_warn_threshold = 1000;
580580
public volatile int tombstone_failure_threshold = 100000;
581581

582+
public volatile boolean write_thresholds_enabled = false;
583+
public volatile DataStorageSpec.LongBytesBound write_size_warn_threshold = null;
584+
public volatile DurationSpec.LongMillisecondsBound coordinator_write_warn_interval = new DurationSpec.LongMillisecondsBound("1000ms");
585+
public volatile int write_tombstone_warn_threshold = -1;
586+
582587
public TombstonesMetricGranularity tombstone_read_purgeable_metric_granularity = TombstonesMetricGranularity.disabled;
583588

584589
public final ReplicaFilteringProtectionOptions replica_filtering_protection = new ReplicaFilteringProtectionOptions();

src/java/org/apache/cassandra/config/DatabaseDescriptor.java

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,7 @@ else if (conf.repair_session_space.toMebibytes() > (int) (Runtime.getRuntime().m
931931

932932
applyConcurrentValidations(conf);
933933
applyRepairCommandPoolSize(conf);
934-
applyReadThresholdsValidations(conf);
934+
applyThresholdsValidations(conf);
935935

936936
if (conf.concurrent_materialized_view_builders <= 0)
937937
throw new ConfigurationException("concurrent_materialized_view_builders should be strictly greater than 0, but was " + conf.concurrent_materialized_view_builders, false);
@@ -1311,11 +1311,19 @@ static void applyRepairCommandPoolSize(Config config)
13111311
}
13121312

13131313
@VisibleForTesting
1314-
static void applyReadThresholdsValidations(Config config)
1314+
static void applyThresholdsValidations(Config config)
13151315
{
1316+
// Validate read thresholds
13161317
validateReadThresholds("coordinator_read_size", config.coordinator_read_size_warn_threshold, config.coordinator_read_size_fail_threshold);
13171318
validateReadThresholds("local_read_size", config.local_read_size_warn_threshold, config.local_read_size_fail_threshold);
13181319
validateReadThresholds("row_index_read_size", config.row_index_read_size_warn_threshold, config.row_index_read_size_fail_threshold);
1320+
1321+
// Write threshold warning depends on top_partitions tracking
1322+
if (config.write_thresholds_enabled && !config.top_partitions_enabled)
1323+
logger.warn("Write thresholds require top partitions tracking to be enabled");
1324+
1325+
validateWriteSizeThreshold(config.write_size_warn_threshold, config.min_tracked_partition_size);
1326+
validateWriteTombstoneThresholdRange(config.write_tombstone_warn_threshold, config.min_tracked_partition_tombstone_count);
13191327
}
13201328

13211329
private static void validateReadThresholds(String name, DataStorageSpec.LongBytesBound warn, DataStorageSpec.LongBytesBound fail)
@@ -1326,6 +1334,25 @@ private static void validateReadThresholds(String name, DataStorageSpec.LongByte
13261334
name + "_warn_threshold", warn));
13271335
}
13281336

1337+
private static void validateWriteSizeThreshold(DataStorageSpec.LongBytesBound writeSizeWarn, DataStorageSpec.LongBytesBound minTrackedSize)
1338+
{
1339+
if (writeSizeWarn != null && minTrackedSize != null)
1340+
{
1341+
if (writeSizeWarn.toBytes() < minTrackedSize.toBytes())
1342+
throw new ConfigurationException(String.format("write_size_warn_threshold (%s) cannot be less than min_tracked_partition_size (%s)", writeSizeWarn, minTrackedSize));
1343+
}
1344+
}
1345+
1346+
private static void validateWriteTombstoneThresholdRange(int writeTombstoneWarn, long minTrackedTombstoneCount)
1347+
{
1348+
if (writeTombstoneWarn < -1)
1349+
throw new ConfigurationException(String.format("write_tombstone_warn_threshold (%d) must be -1 (disabled) or >= 0", writeTombstoneWarn));
1350+
1351+
if (writeTombstoneWarn != -1 && writeTombstoneWarn < minTrackedTombstoneCount)
1352+
throw new ConfigurationException(String.format("write_tombstone_warn_threshold (%d) cannot be less than min_tracked_partition_tombstone_count (%d)",
1353+
writeTombstoneWarn, minTrackedTombstoneCount));
1354+
}
1355+
13291356
public static GuardrailsOptions getGuardrailsConfig()
13301357
{
13311358
return guardrails;
@@ -5482,6 +5509,55 @@ public static void setRowIndexReadSizeFailThreshold(@Nullable DataStorageSpec.Lo
54825509
conf.row_index_read_size_fail_threshold = value;
54835510
}
54845511

5512+
public static boolean getWriteThresholdsEnabled()
5513+
{
5514+
return conf.write_thresholds_enabled;
5515+
}
5516+
5517+
public static void setWriteThresholdsEnabled(boolean enabled)
5518+
{
5519+
if (enabled && !conf.top_partitions_enabled)
5520+
logger.warn("Write thresholds require top partitions tracking to be enabled");
5521+
logger.info("updating write_thresholds_enabled to {}", enabled);
5522+
conf.write_thresholds_enabled = enabled;
5523+
}
5524+
5525+
@Nullable
5526+
public static DataStorageSpec.LongBytesBound getWriteSizeWarnThreshold()
5527+
{
5528+
return conf.write_size_warn_threshold;
5529+
}
5530+
5531+
public static void setWriteSizeWarnThreshold(@Nullable DataStorageSpec.LongBytesBound value)
5532+
{
5533+
validateWriteSizeThreshold(value, conf.min_tracked_partition_size);
5534+
logger.info("updating write_size_warn_threshold to {}", value);
5535+
conf.write_size_warn_threshold = value;
5536+
}
5537+
5538+
public static DurationSpec.LongMillisecondsBound getCoordinatorWriteWarnInterval()
5539+
{
5540+
return conf.coordinator_write_warn_interval;
5541+
}
5542+
5543+
public static void setCoordinatorWriteWarnInterval(DurationSpec.LongMillisecondsBound ms)
5544+
{
5545+
logger.info("updating coordinator_write_warn_interval to {}", ms);
5546+
conf.coordinator_write_warn_interval = ms;
5547+
}
5548+
5549+
public static int getWriteTombstoneWarnThreshold()
5550+
{
5551+
return conf.write_tombstone_warn_threshold;
5552+
}
5553+
5554+
public static void setWriteTombstoneWarnThreshold(int threshold)
5555+
{
5556+
validateWriteTombstoneThresholdRange(threshold, conf.min_tracked_partition_tombstone_count);
5557+
logger.info("updating write_tombstone_warn_threshold to {}", threshold);
5558+
conf.write_tombstone_warn_threshold = threshold;
5559+
}
5560+
54855561
public static int getDefaultKeyspaceRF()
54865562
{
54875563
return conf.default_keyspace_rf;

src/java/org/apache/cassandra/db/MessageParams.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package org.apache.cassandra.db;
2020

21+
import java.util.Collections;
2122
import java.util.EnumMap;
2223
import java.util.Map;
2324

@@ -66,6 +67,22 @@ public static void reset()
6667
get().clear();
6768
}
6869

70+
/**
71+
* Capture the current MessageParams for use across threads.
72+
* This returns a copy of the current ThreadLocal params, which can be safely
73+
* passed to async callbacks that may run on different threads.
74+
*
75+
* @return immutable copy of current params, or empty map if none
76+
*/
77+
public static Map<ParamType, Object> capture()
78+
{
79+
Map<ParamType, Object> current = local.get();
80+
if (current == null || current.isEmpty())
81+
return Collections.emptyMap();
82+
83+
return new EnumMap<>(current);
84+
}
85+
6986
public static <T> Message<T> addToMessage(Message<T> message)
7087
{
7188
return message.withParams(get());

src/java/org/apache/cassandra/db/MutationVerbHandler.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
*/
1818
package org.apache.cassandra.db;
1919

20+
import java.util.Map;
21+
2022
import org.apache.cassandra.exceptions.WriteTimeoutException;
2123
import org.apache.cassandra.locator.InetAddressAndPort;
2224
import org.apache.cassandra.net.ForwardingInfo;
@@ -33,10 +35,13 @@ public class MutationVerbHandler extends AbstractMutationVerbHandler<Mutation>
3335
{
3436
public static final MutationVerbHandler instance = new MutationVerbHandler();
3537

36-
private void respond(Message<?> respondTo, InetAddressAndPort respondToAddress)
38+
private void respond(Message<?> respondTo, InetAddressAndPort respondToAddress, Map<ParamType, Object> params)
3739
{
3840
Tracing.trace("Enqueuing response to {}", respondToAddress);
39-
MessagingService.instance().send(respondTo.emptyResponse(), respondToAddress);
41+
Message<?> response = respondTo.emptyResponse();
42+
if (!params.isEmpty())
43+
response = response.withParams(params);
44+
MessagingService.instance().send(response, respondToAddress);
4045
}
4146

4247
private void failed()
@@ -53,9 +58,10 @@ public void doVerb(Message<Mutation> message)
5358
return;
5459
}
5560

61+
MessageParams.reset();
5662
message.payload.validateSize(MessagingService.current_version, ENTRY_OVERHEAD_SIZE);
63+
WriteThresholds.checkWriteThresholds(message.payload);
5764

58-
// Check if there were any forwarding headers in this message
5965
ForwardingInfo forwardTo = message.forwardTo();
6066
if (forwardTo != null)
6167
forwardToLocalNodes(message, forwardTo);
@@ -73,7 +79,8 @@ public void doVerb(Message<Mutation> message)
7379

7480
protected void applyMutation(Message<Mutation> message, InetAddressAndPort respondToAddress)
7581
{
76-
message.payload.applyFuture().addCallback(o -> respond(message, respondToAddress), wto -> failed());
82+
Map<ParamType, Object> params = MessageParams.capture();
83+
message.payload.applyFuture().addCallback(o -> respond(message, respondToAddress, params), wto -> failed());
7784
}
7885

7986
private static void forwardToLocalNodes(Message<Mutation> originalMessage, ForwardingInfo forwardTo)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.cassandra.db;
20+
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.concurrent.TimeUnit;
24+
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import org.apache.cassandra.config.DataStorageSpec;
29+
import org.apache.cassandra.config.DatabaseDescriptor;
30+
import org.apache.cassandra.metrics.TopPartitionTracker;
31+
import org.apache.cassandra.net.ParamType;
32+
import org.apache.cassandra.schema.Schema;
33+
import org.apache.cassandra.schema.TableId;
34+
import org.apache.cassandra.schema.TableMetadata;
35+
import org.apache.cassandra.utils.NoSpamLogger;
36+
37+
/**
38+
* Utility class for checking write threshold warnings on replicas.
39+
* CASSANDRA-17258: paxos and accord do complex thread hand off and custom write logic which makes this patch complex, so was deferred
40+
*/
41+
public class WriteThresholds
42+
{
43+
private static final Logger logger = LoggerFactory.getLogger(WriteThresholds.class);
44+
private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES);
45+
46+
/**
47+
* Check write thresholds for a mutation by comparing the estimated partition size and tombstone count
48+
* from {@link TopPartitionTracker} against configured warn thresholds. If a threshold is breached,
49+
* a warning is logged and the corresponding {@link ParamType} is added to {@link MessageParams}
50+
* for propagation back to the coordinator.
51+
*
52+
* @param mutation the mutation containing one or more partition updates
53+
*/
54+
public static void checkWriteThresholds(Mutation mutation)
55+
{
56+
if (!DatabaseDescriptor.isDaemonInitialized() || !DatabaseDescriptor.getWriteThresholdsEnabled())
57+
return;
58+
59+
DataStorageSpec.LongBytesBound sizeWarnThreshold = DatabaseDescriptor.getWriteSizeWarnThreshold();
60+
int tombstoneWarnThreshold = DatabaseDescriptor.getWriteTombstoneWarnThreshold();
61+
62+
if (sizeWarnThreshold == null && tombstoneWarnThreshold == -1)
63+
return;
64+
65+
long sizeWarnBytes = sizeWarnThreshold != null ? sizeWarnThreshold.toBytes() : -1;
66+
DecoratedKey key = mutation.key();
67+
68+
Map<TableId, Long> sizeWarnings = null;
69+
Map<TableId, Long> tombstoneWarnings = null;
70+
71+
for (TableId tableId : mutation.getTableIds())
72+
{
73+
ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(tableId);
74+
if (cfs == null || cfs.topPartitions == null)
75+
continue;
76+
77+
TableMetadata metadata = cfs.metadata();
78+
if (sizeWarnBytes != -1)
79+
{
80+
long estimatedSize = cfs.topPartitions.topSizes().getEstimate(key);
81+
if (estimatedSize > sizeWarnBytes)
82+
{
83+
if (sizeWarnings == null)
84+
sizeWarnings = new HashMap<>();
85+
sizeWarnings.put(tableId, estimatedSize);
86+
noSpamLogger.warn("Write to {} partition {} triggered size warning; " +
87+
"estimated size is {} bytes, threshold is {} bytes (see write_size_warn_threshold)",
88+
metadata, metadata.partitionKeyType.toCQLString(key.getKey()), estimatedSize, sizeWarnBytes);
89+
}
90+
}
91+
92+
if (tombstoneWarnThreshold != -1)
93+
{
94+
long estimatedTombstones = cfs.topPartitions.topTombstones().getEstimate(key);
95+
if (estimatedTombstones > tombstoneWarnThreshold)
96+
{
97+
if (tombstoneWarnings == null)
98+
tombstoneWarnings = new HashMap<>();
99+
tombstoneWarnings.put(tableId, estimatedTombstones);
100+
noSpamLogger.warn("Write to {} partition {} triggered tombstone warning; " +
101+
"estimated tombstone count is {}, threshold is {} (see write_tombstone_warn_threshold)",
102+
metadata, metadata.partitionKeyType.toCQLString(key.getKey()), estimatedTombstones, tombstoneWarnThreshold);
103+
}
104+
}
105+
}
106+
107+
if (sizeWarnings != null)
108+
MessageParams.add(ParamType.WRITE_SIZE_WARN, sizeWarnings);
109+
if (tombstoneWarnings != null)
110+
MessageParams.add(ParamType.WRITE_TOMBSTONE_WARN, tombstoneWarnings);
111+
}
112+
}

src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ public class KeyspaceMetrics
191191

192192
public final Meter tooManySSTableIndexesReadWarnings;
193193
public final Meter tooManySSTableIndexesReadAborts;
194+
195+
public final Meter writeSizeWarnings;
196+
public final Meter writeTombstoneWarnings;
197+
194198
public final Meter bytesAnticompacted;
195199
public final Meter bytesMutatedAnticompaction;
196200
public final Meter bytesPreviewed;
@@ -309,6 +313,9 @@ public KeyspaceMetrics(final Keyspace ks)
309313
tooManySSTableIndexesReadWarnings = createKeyspaceMeter("TooManySSTableIndexesReadWarnings");
310314
tooManySSTableIndexesReadAborts = createKeyspaceMeter("TooManySSTableIndexesReadAborts");
311315

316+
writeSizeWarnings = createKeyspaceMeter("WriteSizeWarnings");
317+
writeTombstoneWarnings = createKeyspaceMeter("WriteTombstoneWarnings");
318+
312319
formatSpecificGauges = createFormatSpecificGauges(keyspace);
313320

314321
outOfRangeTokenReads = createKeyspaceCounter("ReadOutOfRangeToken");

src/java/org/apache/cassandra/metrics/TableMetrics.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@ public class TableMetrics
307307
public final TableMeter tooManySSTableIndexesReadWarnings;
308308
public final TableMeter tooManySSTableIndexesReadAborts;
309309

310+
public final TableMeter writeSizeWarnings;
311+
public final TableMeter writeTombstoneWarnings;
312+
310313
public final ImmutableMap<SSTableFormat<?, ?>, ImmutableMap<String, Gauge<? extends Number>>> formatSpecificGauges;
311314

312315
// Time spent building SSTableIntervalTree when constructing a new View under the Tracker lock
@@ -907,6 +910,9 @@ public Long getValue()
907910
tooManySSTableIndexesReadWarnings = createTableMeter("TooManySSTableIndexesReadWarnings", cfs.keyspace.metric.tooManySSTableIndexesReadWarnings);
908911
tooManySSTableIndexesReadAborts = createTableMeter("TooManySSTableIndexesReadAborts", cfs.keyspace.metric.tooManySSTableIndexesReadAborts);
909912

913+
writeSizeWarnings = createTableMeter("WriteSizeWarnings", cfs.keyspace.metric.writeSizeWarnings);
914+
writeTombstoneWarnings = createTableMeter("WriteTombstoneWarnings", cfs.keyspace.metric.writeTombstoneWarnings);
915+
910916
viewSSTableIntervalTree = createLatencyMetrics("ViewSSTableIntervalTree", cfs.keyspace.metric.viewSSTableIntervalTree);
911917

912918
formatSpecificGauges = createFormatSpecificGauges(cfs);

0 commit comments

Comments
 (0)