hudi-agent commented on code in PR #19696:
URL: https://github.com/apache/hudi/pull/19696#discussion_r3825971709
##########
hudi-utilities/src/main/java/org/apache/hudi/utilities/streamer/HoodieMultiTableStreamer.java:
##########
@@ -479,11 +512,81 @@ public void sync() {
}
}
}
+ }
- log.info("Ingestion was successful for topics: {}", successTables);
- if (!failedTables.isEmpty()) {
- log.info("Ingestion failed for topics: {}", failedTables);
+ /**
+ * Syncs all tables concurrently, one thread per table. Used for continuous
mode where each table's sync blocks
+ * indefinitely.
+ *
+ * <p>When {@code --fail-fast-on-continuous} is enabled, the first table
failure fails the whole job: the sibling
+ * streamers are shut down and a {@link HoodieException} is thrown so the
caller can exit with a non-zero status.
+ * Otherwise every table is synced independently and a single failure does
not affect the others.
+ */
+ private void syncContinuously() {
+ // Streamer instances are registered from worker threads, so a thread-safe
list is required.
+ final List<HoodieStreamer> streamerInstances = new
CopyOnWriteArrayList<>();
+ final ExecutorService executor =
Executors.newFixedThreadPool(tableExecutionContexts.size(),
+ new CustomizedThreadFactory("multi-table-streamer", true));
+ try {
+ final CompletableFuture<?>[] tableFutures =
tableExecutionContexts.stream()
+ .map(context -> CompletableFuture.runAsync(() -> {
+ HoodieStreamer streamer = null;
+ try {
+ streamer = new HoodieStreamer(context.getConfig(), jssc,
Option.ofNullable(context.getProperties()));
+ streamerInstances.add(streamer);
+ streamer.sync();
+ successTables.add(Helpers.getTableWithDatabase(context));
+ } catch (Exception e) {
+ log.error("error while running MultiTableDeltaStreamer for
table: {}", context.getTableName(), e);
+ failedTables.add(Helpers.getTableWithDatabase(context));
+ if (failFastOnContinuousMode) {
+ throw new CompletionException(e);
+ }
+ } finally {
+ if (streamer != null) {
+ streamer.shutdownGracefully();
+ }
+ }
+ }, executor)).toArray(CompletableFuture[]::new);
+
+ // In fail-fast mode getConditionalFuture returns anyOf(...), which
completes exceptionally as soon as the first
+ // table fails; otherwise it returns allOf(...) and only completes once
every table sync has terminated.
+ Throwable failure = getConditionalFuture(tableFutures)
+ .handle((unused, throwable) -> throwable)
+ .join();
+ log.info("Successful tables: {}, Failed tables: {}", successTables,
failedTables);
+ if (failure != null && failFastOnContinuousMode) {
+ log.error("MultiTableDeltaStreamer failed with an exception, shutting
down remaining sources as fail fast is enabled!", failure);
+ shutdownStreamers(streamerInstances);
+ throw new HoodieException("Fail fast is enabled and a table sync
failed in continuous mode.", failure);
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ private void shutdownStreamers(List<HoodieStreamer> streamerInstances) {
+ for (HoodieStreamer streamer : streamerInstances) {
+ try {
+ if (!streamer.getIngestionService().isShutdown()) {
+ streamer.getIngestionService().shutdown(true);
+ }
+ } catch (Exception e) {
+ log.warn("Failed to shut down a streamer instance during fail-fast
handling.", e);
+ }
+ }
+ }
+
+ /**
+ * Returns a future that completes when the first table sync fails
(fail-fast) or when all table syncs
+ * complete (default).
+ */
+ private CompletableFuture<?> getConditionalFuture(final
CompletableFuture<?>[] tableFutures) {
+ if (failFastOnContinuousMode) {
+ log.info("Fail fast enabled in continuous mode. The whole job fails on
any single table failure.");
+ return CompletableFuture.anyOf(tableFutures);
Review Comment:
🤖 `CompletableFuture.anyOf` completes on the first future to *finish* —
success or failure — not the first failure. In continuous mode a table with a
`PostWriteTerminationStrategy` (like the `NoNewDataTerminationStrategy` your
new tests use) terminates normally, so if a healthy table finishes first,
`anyOf` completes normally, `failure` stays null, the fail-fast throw is
skipped, and the `finally`'s `executor.shutdownNow()` still interrupts the
other tables mid-sync. That also makes
`testFailFastOnContinuousThrowsWhenATableFails` depend on table 2 failing
before table 1 completes. Could you trigger fail-fast only on the first
*exceptional* completion instead — e.g. attach a `whenComplete`/`exceptionally`
that trips a signal on throwable rather than relying on `anyOf`?
<sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag
quality.</i></sub>
##########
hudi-utilities/src/main/java/org/apache/hudi/utilities/streamer/HoodieMultiTableStreamer.java:
##########
@@ -479,11 +512,81 @@ public void sync() {
}
}
}
+ }
- log.info("Ingestion was successful for topics: {}", successTables);
- if (!failedTables.isEmpty()) {
- log.info("Ingestion failed for topics: {}", failedTables);
+ /**
+ * Syncs all tables concurrently, one thread per table. Used for continuous
mode where each table's sync blocks
+ * indefinitely.
+ *
+ * <p>When {@code --fail-fast-on-continuous} is enabled, the first table
failure fails the whole job: the sibling
+ * streamers are shut down and a {@link HoodieException} is thrown so the
caller can exit with a non-zero status.
+ * Otherwise every table is synced independently and a single failure does
not affect the others.
+ */
+ private void syncContinuously() {
+ // Streamer instances are registered from worker threads, so a thread-safe
list is required.
+ final List<HoodieStreamer> streamerInstances = new
CopyOnWriteArrayList<>();
+ final ExecutorService executor =
Executors.newFixedThreadPool(tableExecutionContexts.size(),
+ new CustomizedThreadFactory("multi-table-streamer", true));
+ try {
+ final CompletableFuture<?>[] tableFutures =
tableExecutionContexts.stream()
+ .map(context -> CompletableFuture.runAsync(() -> {
+ HoodieStreamer streamer = null;
+ try {
+ streamer = new HoodieStreamer(context.getConfig(), jssc,
Option.ofNullable(context.getProperties()));
+ streamerInstances.add(streamer);
+ streamer.sync();
+ successTables.add(Helpers.getTableWithDatabase(context));
+ } catch (Exception e) {
+ log.error("error while running MultiTableDeltaStreamer for
table: {}", context.getTableName(), e);
+ failedTables.add(Helpers.getTableWithDatabase(context));
+ if (failFastOnContinuousMode) {
+ throw new CompletionException(e);
+ }
+ } finally {
+ if (streamer != null) {
+ streamer.shutdownGracefully();
+ }
+ }
+ }, executor)).toArray(CompletableFuture[]::new);
+
+ // In fail-fast mode getConditionalFuture returns anyOf(...), which
completes exceptionally as soon as the first
+ // table fails; otherwise it returns allOf(...) and only completes once
every table sync has terminated.
+ Throwable failure = getConditionalFuture(tableFutures)
+ .handle((unused, throwable) -> throwable)
+ .join();
+ log.info("Successful tables: {}, Failed tables: {}", successTables,
failedTables);
+ if (failure != null && failFastOnContinuousMode) {
+ log.error("MultiTableDeltaStreamer failed with an exception, shutting
down remaining sources as fail fast is enabled!", failure);
+ shutdownStreamers(streamerInstances);
Review Comment:
🤖 When fail-fast trips, `shutdownStreamers` only shuts down streamers
already added to `streamerInstances`. A sibling thread still inside `new
HoodieStreamer(...)` (or in the window before `streamerInstances.add(...)` at
the top of the task) won't be in the list, so its ingestion can start right
after this shutdown runs. And `executor.shutdownNow()` in the `finally` only
interrupts the worker thread — `StreamSyncService.startService()` uses its own
non-daemon single-thread executor for the ingest loop, which isn't stopped by
interrupting the worker. Could this leave a streamer running after fail-fast?
It might be safer to set a shutdown flag that every task re-checks after
registering, or to shut down by draining `streamerInstances` until empty.
<sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag
quality.</i></sub>
##########
hudi-utilities/src/test/java/org/apache/hudi/utilities/deltastreamer/TestHoodieMultiTableDeltaStreamer.java:
##########
@@ -243,6 +245,75 @@ public void testMultiTableExecutionWithParquetSource()
throws IOException {
}
}
+ @Test
+ public void testFailFastOnContinuousDefaultsToFalse() {
+ HoodieMultiTableDeltaStreamer.Config cfg = new
HoodieMultiTableDeltaStreamer.Config();
+ assertFalse(cfg.failFastOnContinuousMode);
+ }
+
+ @Test
+ public void testMultiTableContinuousModeSyncsAllTablesInParallel() throws
IOException {
+ // ingest test data to 2 parquet source paths
+ String parquetSourceRoot1 = basePath + "/parquetContSrc1/";
+ prepareParquetDFSFiles(10, parquetSourceRoot1);
+ String parquetSourceRoot2 = basePath + "/parquetContSrc2/";
+ prepareParquetDFSFiles(5, parquetSourceRoot2);
+
+ String parquetPropsFile = populateCommonPropsAndWriteToFile();
+
+ HoodieMultiTableDeltaStreamer.Config cfg =
TestHelpers.getConfig(parquetPropsFile, basePath + "/config",
ParquetDFSSource.class.getName(), false, false,
+ false, "multi_table_parquet_continuous", null);
+ // Continuous mode blocks per table, so tables must be synced concurrently.
+ cfg.continuousMode = true;
+
+ HoodieMultiTableDeltaStreamer streamer = new
HoodieMultiTableDeltaStreamer(cfg, jsc);
Review Comment:
🤖 nit: `Arrays.asList(new String[] {...})` — the intermediate `new String[]`
is redundant since `Arrays.asList` is already varargs. Could you simplify to
`Arrays.asList(parquetSourceRoot1, parquetSourceRoot2)`? Same applies on line
299.
<sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag
quality.</i></sub>
##########
hudi-utilities/src/main/java/org/apache/hudi/utilities/streamer/HoodieMultiTableStreamer.java:
##########
Review Comment:
🤖 **Line 613:** nit: `getConditionalFuture` is a bit opaque — "conditional"
doesn't say what the condition is or what kind of future is returned. Could you
rename it to something like `buildSyncCompletionFuture`? The Javadoc explains
the intent well, but a clearer name would make the call-site in
`syncContinuously` self-explanatory without jumping to the definition.
<sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag
quality.</i></sub>
--
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]