jmckenzie-dev commented on code in PR #224:
URL: 
https://github.com/apache/cassandra-analytics/pull/224#discussion_r3624413279


##########
cassandra-analytics-cdc/src/main/java/org/apache/cassandra/cdc/Cdc.java:
##########
@@ -415,22 +415,33 @@ protected void refreshSchema()
         try
         {
             schemaSupplier
-            .getCdcEnabledTables()
-            .handle((tables, throwable) -> {
+            .getTables()
+            .handle((allTables, throwable) -> {
                 if (throwable != null)
                 {
                     LOGGER.warn("Error refreshing schema", throwable);
                     return null;
                 }
-                this.cdcEnabledTables = tables;
-                if (tables == null || tables.isEmpty())
+                if (allTables == null || allTables.isEmpty())
                 {
-                    LOGGER.warn("No CQL enabled tables");
+                    LOGGER.warn("No tables returned from schema supplier");
                     return null;
                 }
 
-                // update Schema instance with latest schema
-                cdcBridge().updateCdcSchema(tables, cdcOptions.partitioner(), 
tableIdLookup);
+                // Filter CDC-enabled tables for publishing decisions
+                Set<CqlTable> cdcTables = allTables.stream()
+                                                   .filter(CqlTable::cdc)
+                                                   
.collect(Collectors.toSet());
+                this.cdcEnabledTables = cdcTables;
+                if (cdcTables.isEmpty())
+                {
+                    LOGGER.warn("No CDC-enabled tables found");

Review Comment:
   In this case, we're proceeding to update the schema we have cached to 
effectively wipe any prior state in the "no cdc enabled" case. What do you 
think about us adding this context to the log? Because my initial naive 
assumption was "if we hit this point why don't we fast-fail out?" and then 
realized "Ah yes. We need to have our schema reflect this".
   
   Concretely, something like:
   "No CDC-enabled tables found; no cdc updates will be processed until CDC is 
enabled on a table's schema"?



##########
cassandra-analytics-cdc-sidecar/src/main/java/org/apache/cassandra/cdc/sidecar/SidecarCdc.java:
##########
@@ -92,11 +92,12 @@ public static SidecarCdcBuilder builder(@NotNull String 
jobId,
 
     public void initSchema()
     {
-        Set<CqlTable> tables = 
FutureUtils.get(schemaSupplier.getCdcEnabledTables());
-        Optional<ReplicationFactor> rfOp = tables.stream()
-                                                 
.map(CqlTable::replicationFactor)
-                                                 .filter(rf -> 
rf.getOptions().containsKey(dc()))
-                                                 
.max(Comparator.comparingInt(rf -> rf.getOptions().get(dc())));
+        Set<CqlTable> allTables = FutureUtils.get(schemaSupplier.getTables());
+        Optional<ReplicationFactor> rfOp = allTables.stream()

Review Comment:
   Same thing here; the pattern of:
   1. `schemaSupplier.getTables()`
   2. `.filter(CqlTable::cdc)`
   



##########
cassandra-analytics-cdc-codec/src/main/java/org/apache/cassandra/cdc/schemastore/CachingSchemaStore.java:
##########
@@ -118,11 +122,16 @@ public void onSchemaChange()
                     }
                     return value;
                 });
+            }
+            // publishSchemas() re-fetches and republishes every CDC table's 
schema, so it only
+            // needs to run once after the cache has been updated for all 
changed tables above —
+            // calling it per-table would redundantly re-fetch the schema and 
republish every
+            // table N times over.
+            if (!refreshedCdcTables.isEmpty())

Review Comment:
   What happens in the case where we had CDC tables then the DDL was changed 
and they were disabled? Wouldn't we skip the publish here so we never get the 
memo that there's no more CDC enabled?



##########
cassandra-analytics-cdc/src/main/java/org/apache/cassandra/cdc/Cdc.java:
##########
@@ -415,22 +415,33 @@ protected void refreshSchema()
         try
         {
             schemaSupplier
-            .getCdcEnabledTables()
-            .handle((tables, throwable) -> {
+            .getTables()
+            .handle((allTables, throwable) -> {
                 if (throwable != null)
                 {
                     LOGGER.warn("Error refreshing schema", throwable);
                     return null;
                 }
-                this.cdcEnabledTables = tables;
-                if (tables == null || tables.isEmpty())
+                if (allTables == null || allTables.isEmpty())
                 {
-                    LOGGER.warn("No CQL enabled tables");
+                    LOGGER.warn("No tables returned from schema supplier");

Review Comment:
   Is this a warn condition or ERROR? Is there ever a non-error case where a 
schema supplier would return 0 tables at all for the "getAllTables" shaped case?



##########
cassandra-analytics-cdc/src/test/java/org/apache/cassandra/cdc/CdcTests.java:
##########
@@ -266,6 +278,171 @@ public List<CdcState> loadState(String jobId, int 
partitionId, @Nullable TokenRa
         }
     }
 
+    @ParameterizedTest
+    
@MethodSource("org.apache.cassandra.cdc.test.TestVersionSupplier#testVersions")
+    public void 
testRefreshSchemaUpdatesBridgeWithAllTablesButFiltersCdcEnabledTables(CassandraVersion
 version)
+    {
+        // Regression test for the CDC batch-write bug: refreshSchema() must 
register EVERY
+        // table returned by the schema supplier (CDC-enabled and 
CDC-disabled) with the
+        // bridge's Schema.instance — not just the CDC-enabled ones — so that 
a commit log
+        // Mutation spanning both a CDC-enabled and a CDC-disabled table in 
the same keyspace
+        // (e.g. a BEGIN BATCH statement) can still be deserialized. 
Separately, only the
+        // CDC-enabled tables should end up in cdcEnabledTables, since that 
set drives
+        // publishing/replica-check decisions, not schema completeness.
+        TestSchema cdcSchema = TestSchema.builder(bridge)
+                                         .withPartitionKey("pk", bridge.uuid())
+                                         .withColumn("val", bridge.text())
+                                         .withCdc(true)
+                                         .build();
+        CqlTable cdcTable = cdcSchema.buildTable();
+
+        TestSchema nonCdcSchema = TestSchema.builder(bridge)
+                                            .withKeyspace(cdcSchema.keyspace)
+                                            .withPartitionKey("pk", 
bridge.uuid())
+                                            .withColumn("val", bridge.text())
+                                            .withCdc(false)
+                                            .build();
+        CqlTable nonCdcTable = nonCdcSchema.buildTable();
+
+        AtomicReference<Set<CqlTable>> tablesPassedToBridge = new 
AtomicReference<>();
+        SchemaSupplier schemaSupplier = () -> 
CompletableFuture.completedFuture(ImmutableSet.of(cdcTable, nonCdcTable));
+
+        try (RecordingCdc cdc = new 
RecordingCdc(Cdc.builder("test-refresh-schema", 0, event -> { }, schemaSupplier)
+                                                    
.withExecutor(CdcTests.ASYNC_EXECUTOR)
+                                                    
.withTableIdLookup(cdcBridge.internalTableIdLookup())
+                                                    
.withCommitLogProvider(tokenRange -> Stream.empty())
+                                                    
.withCdcOptions(cdcOptions),
+                                                    tablesPassedToBridge))
+        {
+            // start() flips isRunning to true and calls refreshSchema() 
synchronously (the
+            // schemaSupplier future is already completed, so the 
.handle()/.whenComplete()
+            // chain runs inline). scheduleRun()/scheduleMonitorSchema() are 
overridden to
+            // no-ops in RecordingCdc, so this only exercises the schema 
refresh path.
+            cdc.start();
+
+            assertThat(tablesPassedToBridge.get())
+            .as("Schema.instance must be updated with ALL tables (CDC and 
non-CDC) so a "

Review Comment:
   Do we normally have . continuations indented on the same line as their root? 
I find that a little hard / surprising to read (i.e. it's not immediately clear 
these are coupled / dependent operations from code structure, have to infer 
from leading .).
   
   Vs for example:
   ```java
               assertThat(tablesPassedToBridge.get())
                   .as("Schema.instance must be updated with ALL tables (CDC 
and non-CDC) so a "...
                   .containsExactlyInAnyOrder(cdcTable, nonCdcTable);
   ```
   I won't push back if our project norm is . on the same line as the lines 
above, but I personally pretty strongly prefer the indentation.



##########
cassandra-four-zero-types/src/main/java/org/apache/cassandra/bridge/CassandraSchema.java:
##########
@@ -304,31 +305,27 @@ public static void updateCdcSchema(@NotNull Schema schema,
             UUID tableId = tableIdLookup.lookup(table.keyspace(), 
table.table());
             if (cdcEnabledTables.containsKey(table.keyspace()) && 
cdcEnabledTables.get(table.keyspace()).contains(table.table()))
             {
-                // table has cdc enabled already, update schema if it has 
changed
-                LOGGER.debug("CDC already enabled keyspace={} table={}", 
table.keyspace(), table.table());
+                // table already registered in Schema.instance — update if 
schema changed
+                LOGGER.debug("Table already in Schema.instance, updating if 
changed keyspace={} table={}", table.keyspace(), table.table());

Review Comment:
   Since this can noop presumably on a lot of cases, I think we should consider 
logging in the `maybeUpdateSchema` call in the case where we have an update. 
The prior logging here made sense as it was a dupe process; here it's just 
telling us that we already knew a table existed but not much else.



##########
cassandra-analytics-core/src/test/java/org/apache/cassandra/spark/KryoSerializationTests.java:
##########
@@ -269,6 +270,38 @@ public void testCqlTable(CassandraBridge bridge)
         assertThat(deserialized).isEqualTo(table);
     }
 
+    @ParameterizedTest
+    @MethodSource("org.apache.cassandra.bridge.VersionRunner#bridges")
+    public void testCqlTableCdcFlagSurvivesSerialization(CassandraBridge 
bridge)
+    {
+        // CqlTable.equals()/hashCode() deliberately exclude the cdc field 
(see CqlTable.java),

Review Comment:
   Why don't we have the `cdc` flag as part of our equality comparison and 
hashcode impl?



##########
cassandra-four-zero-types/src/main/java/org/apache/cassandra/bridge/CassandraSchema.java:
##########
@@ -304,31 +305,27 @@ public static void updateCdcSchema(@NotNull Schema schema,
             UUID tableId = tableIdLookup.lookup(table.keyspace(), 
table.table());
             if (cdcEnabledTables.containsKey(table.keyspace()) && 
cdcEnabledTables.get(table.keyspace()).contains(table.table()))
             {
-                // table has cdc enabled already, update schema if it has 
changed
-                LOGGER.debug("CDC already enabled keyspace={} table={}", 
table.keyspace(), table.table());
+                // table already registered in Schema.instance — update if 
schema changed
+                LOGGER.debug("Table already in Schema.instance, updating if 
changed keyspace={} table={}", table.keyspace(), table.table());
                 cdcEnabledTables.get(table.keyspace()).remove(table.table());
-                CassandraSchema.maybeUpdateSchema(schema, partitioner, table, 
tableId, true);
-                
Preconditions.checkArgument(CassandraSchema.isCdcEnabled(schema, table),
-                                            "CDC not enabled for table: " + 
table.keyspace() + "." + table.table());
+                CassandraSchema.maybeUpdateSchema(schema, partitioner, table, 
tableId, table.cdc());
                 continue;

Review Comment:
   Why the if / continue structure (which is arguably glorified modern-day 
`goto`) instead of an if / else if / else structure?



##########
cassandra-analytics-cdc-codec/src/main/java/org/apache/cassandra/cdc/schemastore/CachingSchemaStore.java:
##########
@@ -160,8 +169,11 @@ protected CqlToAvroSchemaConverter schemaConverter()
     private void publishSchemas()
     {
         schemaSupplier
-        .getCdcEnabledTables()
-        .thenAccept(refreshedCdcTables -> {
+        .getTables()
+        .thenAccept(allTables -> {
+            Set<CqlTable> refreshedCdcTables = allTables.stream()

Review Comment:
   We have this pattern of filtering down to just the CDC enabled tables in 
multiple places. What do you think about augmenting the `SchemaSupplier` to 
provide both:
   ```java
       CompletableFuture<Set<CqlTable>> getTables();
       CompletableFuture<Set<CqlTable>> getCDCEnabledTables();
   ```
   
   That'd tidy up a lot of this duplicated stream code and give us a single 
inflection point to make modifications if this pattern needs to change (i.e. 
`CqlTable::cdc` becomes something else, or needs more data to resolve, for 
instance)



##########
cassandra-four-zero-types/src/main/java/org/apache/cassandra/bridge/CassandraSchema.java:
##########


Review Comment:
   This confused me. The name `cdcEnabledTables` followed by:
   ```java
           cdcEnabledTables.forEach((ks, tables) -> tables.forEach(table -> {
               LOGGER.warn("Disabling CDC on table keyspace={} table={}", ks, 
table);
               CassandraSchema.disableCdc(schema, ks, table);
           }));
   ```
   Read that we were removing cdcEnabledTables from our schema.
   
   Renaming this variable to `remainingCdcEnabledTables` or 
`schemaCdcEnabledTables` plus a comment would help disambiguate this so it's 
clear we're not unregistering cdc enabled tables there at the end of the loop. 
The logic looks right but this naming threw me for a loop.



##########
cassandra-four-zero-types/src/main/java/org/apache/cassandra/bridge/CassandraSchema.java:
##########
@@ -337,13 +334,92 @@ public static void updateCdcSchema(@NotNull Schema schema,
                                             "CDC not enabled for table: " + 
table.keyspace() + "." + table.table());
             }
         }
-        // existing table no longer with cdc = true, so disable
+        // tables previously with cdc=true that are no longer present → 
disable CDC
         cdcEnabledTables.forEach((ks, tables) -> tables.forEach(table -> {
             LOGGER.warn("Disabling CDC on table keyspace={} table={}", ks, 
table);
             CassandraSchema.disableCdc(schema, ks, table);
         }));
     }
 
+    /**
+     * Removes tables from {@code Schema.instance} that were previously 
registered via
+     * {@link #updateCdcSchema} but are no longer needed — e.g. a non-CDC 
table that no longer
+     * shares partition-key structure with any CDC-enabled table in its 
keyspace after a schema
+     * change. See {@code CdcBridge#unregisterTables} for the full rationale.
+     *
+     * <p>Idempotent: a table not currently registered is silently skipped. 
Refuses (skips, with
+     * a warning) to unregister any table that is currently CDC-enabled — the 
caller is
+     * responsible for only requesting removal of tables it has determined are 
safe, but this is
+     * a last-line defense against silently dropping schema CDC still needs.
+     *
+     * @param tables the tables to unregister
+     */
+    public static void unregisterTables(@NotNull Set<TableIdentifier> tables)
+    {
+        unregisterTables(Schema.instance, tables);
+    }
+
+    public static void unregisterTables(@NotNull Schema schema, @NotNull 
Set<TableIdentifier> tables)
+    {
+        for (TableIdentifier id : tables)
+        {
+            String keyspace = id.keyspace();
+            String table = id.table();
+            try
+            {
+                unregisterTable(schema, keyspace, table);
+            }
+            catch (RuntimeException e)
+            {
+                // Never let one misbehaving table abort unregistration of the 
rest of the
+                // batch — the caller (CassandraClusterSchemaMonitor) treats 
this call as
+                // all-or-nothing for bookkeeping purposes, so a partial 
failure here must not
+                // prevent the other (unrelated) tables in the same batch from 
being cleaned up.
+                LOGGER.warn("Failed to unregister table keyspace={} table={}", 
keyspace, table, e);
+            }
+        }
+    }
+
+    private static void unregisterTable(@NotNull Schema schema, @NotNull 
String keyspace, @NotNull String table)

Review Comment:
   I think we should rename this. `CassandraSchema#unregisterTable` implies 
it's for just straight "unregistered your tables", vs. a name like 
`CassandraSchema#unregisterNonCdcTables`, which is where we see the "Refusing 
to unregistered CDC-enabled table" log message making a lot more sense.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to