-
Notifications
You must be signed in to change notification settings - Fork 30
CASSANALYTICS-160: Determine whether mutation tracking is enabled for keyspace for bulk writes #214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mutation-tracking-support
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,9 @@ public final class CqlUtils | |
| "max_index_interval" | ||
| ); | ||
| private static final Pattern REPLICATION_FACTOR_PATTERN = Pattern.compile("WITH REPLICATION = (\\{[^\\}]*\\})"); | ||
| private static final String TRACKED_REPLICATION_TYPE = "tracked"; | ||
| private static final Pattern REPLICATION_TYPE_PATTERN = Pattern.compile("replication_type\\s*=\\s*'(\\w+)'", | ||
| Pattern.CASE_INSENSITIVE); | ||
| // Initialize a mapper allowing single quotes to process the RF string from the CREATE KEYSPACE statement | ||
| private static final ObjectMapper MAPPER = new ObjectMapper().configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); | ||
| private static final Pattern ESCAPED_WHITESPACE_PATTERN = Pattern.compile("(\\\\r|\\\\n|\\\\r\\n)+"); | ||
|
|
@@ -296,4 +299,33 @@ public static boolean isTimeRangeFilterSupported(String compactionStrategy) | |
| { | ||
| return compactionStrategy == null || compactionStrategy.endsWith("TimeWindowCompactionStrategy"); | ||
| } | ||
|
|
||
| /** | ||
| * Extracts replication type from create schema statement | ||
| * | ||
| * @param schemaStr full cluster schema string as returned by Sidecar | ||
| * @param keyspace name of the keyspace to check | ||
| * @return {@code true} if keyspace is tracked {@code false} otherwise | ||
| */ | ||
| public static String extractReplicationType(@NotNull String schemaStr, @NotNull String keyspace) | ||
| { | ||
| String createKeyspaceSchema = extractKeyspaceSchema(schemaStr, keyspace); | ||
| Matcher matcher = REPLICATION_TYPE_PATTERN.matcher(createKeyspaceSchema); | ||
| if (matcher.find()) | ||
| { | ||
| return matcher.group(1); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Returns {@code true} if {@code replication_type = 'tracked'} in create statement otherwise {@code false} | ||
| * | ||
| * @param replicationType replication type extracted from create statement | ||
| * @return {@code true} if replication type is tracked {@code false} otherwise | ||
| */ | ||
| public static boolean isTracked(String replicationType) | ||
| { | ||
| return TRACKED_REPLICATION_TYPE.equalsIgnoreCase(replicationType); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we are checking for replication_type value only. But the cluster need to have mutation_tracking.enabled to true for the feature to work at Cassandra side. Can you please explore what happens in Cassandra if a keyspace was created with replication_type = tracked, but then later admins changed mutation_tracking.enabled to false? Based on these findings we may need to check for mutation_tracking.enabled as well.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch Shailaja! thanks, looks like we need to check the cluster level flag as well
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @skoppu22 I looked into this more, in Cassandra side during imports through coordinated transfer Cassandra is only checking replication type so far, it is not checking the cluster level flag. If replication type is tracked, regardless of whether cluster level flag is enabled or disabled Cassandra takes the coordinated transfer route, we should be good to use the The scenario of what if the cluster level mutation tracking flag is turned off after creating a tracked keyspace, needs further discussion and we can leave that out of this PR
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -356,6 +356,17 @@ public ReplicationFactor replicationFactor() | |
| } | ||
| } | ||
|
|
||
| @Override | ||
| public String getReplicationType() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CassandraClusterInfoGroup not overriding this, i.e, doesn't detect tracked keyspaces
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
| { | ||
| String keyspaceSchema = getKeyspaceSchema(true); | ||
| if (keyspaceSchema == null) | ||
| { | ||
| throw new RuntimeException("Could not retrieve keyspace schema information for keyspace " + conf.keyspace); | ||
| } | ||
| return CqlUtils.extractReplicationType(keyspaceSchema, conf.keyspace); | ||
| } | ||
|
|
||
| @Override | ||
| public TokenRangeMapping<RingInstance> getTokenRangeMapping(boolean cached) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| package org.apache.cassandra.spark.bulkwriter; | ||
|
|
||
| import java.math.BigInteger; | ||
| import java.util.ArrayList; | ||
| import java.util.Collection; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import com.google.common.annotations.VisibleForTesting; | ||
| import com.google.common.collect.Range; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import org.apache.cassandra.spark.bulkwriter.token.ReplicaAwareFailureHandler; | ||
|
|
||
| /** | ||
| * Stream session for bulk writes to keyspaces with mutation tracking enabled. | ||
| * | ||
| * <p> | ||
| * Tracked stream session uploads and triggers import on the <em>coordinator node only</em>. Cassandra's coordinated | ||
| * transfer then propagates the data to all other replicas, avoiding the duplicate-row updates that would occur if each | ||
| * replica independently streamed the data to its peers. | ||
| * </p> | ||
| * | ||
| * <p> | ||
| * The coordinator selection policy is: | ||
| * <ol> | ||
| * <li>Prefer the replica designated as coordinator by Sidecar (via {@link RingInstance#isCoordinator()}).</li> | ||
| * <li>If no such replica is found (e.g. older Sidecar without the flag), fall back to the first non-failed replica | ||
| * in the token range.</li> | ||
| * </ol> | ||
| * Superseded coordinators are marked failed via the {@link ReplicaAwareFailureHandler} so that retries pick the | ||
| * next available candidate. | ||
| * </p> | ||
| */ | ||
| public class TrackedDirectStreamSession extends DirectStreamSession | ||
| { | ||
| private static final Logger LOGGER = LoggerFactory.getLogger(TrackedDirectStreamSession.class); | ||
|
|
||
| public TrackedDirectStreamSession(BulkWriterContext writerContext, | ||
| SortedSSTableWriter sstableWriter, | ||
| TransportContext.DirectDataBulkWriterContext transportContext, | ||
| String sessionID, | ||
| Range<BigInteger> tokenRange, | ||
| ReplicaAwareFailureHandler<RingInstance> failureHandler, | ||
| ExecutorService executorService) | ||
| { | ||
| super(writerContext, sstableWriter, transportContext, sessionID, tokenRange, failureHandler, executorService); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a single-element list containing the coordinator node for the token range. | ||
| * | ||
| * <p> | ||
| * The coordinator is the node Cassandra designates to receive the bulk import and coordinate its propagation | ||
| * to all replicas. Analytics only needs to upload to this one node; Cassandra handles the rest. | ||
| * </p> | ||
| * | ||
| * <p> | ||
| * Selection order: | ||
| * <ol> | ||
| * <li>The replica marked as coordinator by Sidecar ({@link RingInstance#isCoordinator()}).</li> | ||
| * <li>First non-failed replica (fallback when Sidecar does not yet surface the coordinator flag).</li> | ||
| * </ol> | ||
| * </p> | ||
| * | ||
| * @return list with exactly one coordinator instance, or an empty list when all replicas have failed | ||
| */ | ||
| @Override | ||
| @VisibleForTesting | ||
| List<RingInstance> getReplicas() | ||
| { | ||
| Set<RingInstance> failedInstances = failureHandler.getFailedInstances(); | ||
| List<RingInstance> candidates = tokenRangeMapping.getSubRanges(tokenRange) | ||
| .asMapOfRanges() | ||
| .values() | ||
| .stream() | ||
| .flatMap(Collection::stream) | ||
| .distinct() | ||
| .filter(instance -> !failedInstances.contains(instance)) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| LOGGER.debug("[{}]: Selecting coordinator from {} candidate(s) for range {}", | ||
| sessionID, candidates.size(), tokenRange); | ||
|
|
||
| // Prefer the Sidecar-designated coordinator; fall back to any available replica | ||
| return candidates.stream() | ||
| .filter(RingInstance::isCoordinator) | ||
| .findFirst() | ||
| .map(List::of) | ||
| .orElseGet(() -> candidates.isEmpty() ? List.of() | ||
| : List.of(candidates.get(0))); | ||
| } | ||
|
|
||
| /** | ||
| * Finalizes the stream by uploading any remaining SSTables to the coordinator node and then triggering | ||
| * a coordinated import on that node. | ||
| * | ||
| * <p> | ||
| * Unlike {@link DirectStreamSession#doFinalizeStream()}, this does not validate CL against individual replica | ||
| * counts. Instead it delegates validation to | ||
| * {@link BulkWriteValidator#validateTrackedKeyspaceCLOrFail}, which checks that the coordinator accepted | ||
| * the import. Cassandra's coordinated transfer guarantees that CL is satisfied internally. | ||
| * </p> | ||
| */ | ||
| @Override | ||
| protected StreamResult doFinalizeStream() | ||
| { | ||
| sendRemainingSSTables(); | ||
|
|
||
| List<RingInstance> coordinator = getReplicas(); | ||
| DirectStreamResult streamResult = new DirectStreamResult(sessionID, | ||
| tokenRange, | ||
| errors, | ||
| new ArrayList<>(coordinator), | ||
| sstableWriter.rowCount(), | ||
| sstableWriter.bytesWritten()); | ||
| List<CommitResult> commitResults; | ||
| try | ||
| { | ||
| commitResults = commit(streamResult); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| if (e instanceof InterruptedException) | ||
| { | ||
| Thread.currentThread().interrupt(); | ||
| } | ||
| throw new RuntimeException(e); | ||
| } | ||
| streamResult.setCommitResults(commitResults); | ||
| LOGGER.debug("[{}]: TrackedStreamResult: {}", sessionID, streamResult); | ||
|
|
||
| // For tracked keyspaces, the coordinator accepts the import and Cassandra distributes it | ||
| // internally; we only need to verify that the coordinator commit succeeded. | ||
| BulkWriteValidator.validateTrackedKeyspaceCLOrFail(commitResults, | ||
| failureHandler, | ||
| LOGGER, | ||
| WRITE_PHASE, | ||
| writerContext.job(), | ||
| tokenRangeMapping, | ||
| writerContext.cluster()); | ||
| return streamResult; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.