This is an automated email from the ASF dual-hosted git repository.

ahmedabu98 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new a412e69657e enable canceling SQL streaming pipeline (#40666)
a412e69657e is described below

commit a412e69657e6528ef893003345dfba37909535fd
Author: Ahmed Abualsaud <[email protected]>
AuthorDate: Tue Oct 20 10:59:59 2026 -0700

    enable canceling SQL streaming pipeline (#40666)
---
 .../sql/impl/rel/BeamEnumerableConverter.java      | 123 +++++++++++++++++----
 .../sdk/extensions/sql/impl/JdbcDriverTest.java    |  51 +++++++++
 .../sql/impl/rel/BeamEnumerableConverterTest.java  |  13 ++-
 3 files changed, 159 insertions(+), 28 deletions(-)

diff --git 
a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamEnumerableConverter.java
 
b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamEnumerableConverter.java
index 69eb0a0a8d5..12a93975460 100644
--- 
a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamEnumerableConverter.java
+++ 
b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamEnumerableConverter.java
@@ -28,11 +28,14 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 import org.apache.beam.runners.direct.DirectOptions;
+import org.apache.beam.runners.direct.DirectRunner;
 import org.apache.beam.sdk.Pipeline;
 import org.apache.beam.sdk.Pipeline.PipelineVisitor;
 import org.apache.beam.sdk.PipelineResult;
@@ -58,6 +61,7 @@ import org.apache.beam.sdk.values.PCollection;
 import org.apache.beam.sdk.values.PCollection.IsBounded;
 import org.apache.beam.sdk.values.PValue;
 import org.apache.beam.sdk.values.Row;
+import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.DataContext;
 import 
org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.adapter.enumerable.EnumerableRel;
 import 
org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.adapter.enumerable.EnumerableRelImplementor;
 import 
org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.adapter.enumerable.PhysType;
@@ -111,16 +115,18 @@ public class BeamEnumerableConverter extends 
ConverterImpl implements Enumerable
     final PhysType physType =
         PhysTypeImpl.of(implementor.getTypeFactory(), rowType, 
prefer.preferArray());
     final Expression node = implementor.stash((BeamRelNode) getInput(), 
BeamRelNode.class);
-    list.add(Expressions.call(BeamEnumerableConverter.class, "toEnumerable", 
node));
+    list.add(
+        Expressions.call(
+            BeamEnumerableConverter.class, "toEnumerable", node, 
implementor.getRootExpression()));
     return implementor.result(physType, list.toBlock());
   }
 
-  public static Enumerable<Object> toEnumerable(BeamRelNode node) {
+  public static Enumerable<Object> toEnumerable(BeamRelNode node, DataContext 
root) {
     final ClassLoader originalClassLoader = 
Thread.currentThread().getContextClassLoader();
     try {
       
Thread.currentThread().setContextClassLoader(BeamEnumerableConverter.class.getClassLoader());
       final PipelineOptions options = 
createPipelineOptions(node.getPipelineOptions());
-      return toEnumerable(options, node);
+      return toEnumerable(options, node, 
DataContext.Variable.CANCEL_FLAG.get(root));
     } finally {
       Thread.currentThread().setContextClassLoader(originalClassLoader);
     }
@@ -161,16 +167,21 @@ public class BeamEnumerableConverter extends 
ConverterImpl implements Enumerable
     } else if (isLimitQuery(node)) {
       throw new UnsupportedOperationException("Does not support queries with 
LIMIT in toRowList.");
     }
-    return collectRows(options, node).stream().collect(Collectors.toList());
+    return collectRows(options, node, new 
AtomicBoolean()).stream().collect(Collectors.toList());
   }
 
-  static Enumerable<Object> toEnumerable(PipelineOptions options, BeamRelNode 
node) {
+  /**
+   * Runs the pipeline for {@code node}. Calcite sets {@code cancelFlag} when 
the JDBC statement is
+   * cancelled (e.g. with Ctrl+C in the SQL shell), which cancels the pipeline.
+   */
+  static Enumerable<Object> toEnumerable(
+      PipelineOptions options, BeamRelNode node, AtomicBoolean cancelFlag) {
     if (node instanceof BeamIOSinkRel) {
-      return count(options, node);
+      return count(options, node, cancelFlag);
     } else if (isLimitQuery(node)) {
-      return limitCollect(options, node);
+      return limitCollect(options, node, cancelFlag);
     }
-    return Linq4j.asEnumerable(rowToAvaticaAndUnboxValues(collectRows(options, 
node)));
+    return Linq4j.asEnumerable(rowToAvaticaAndUnboxValues(collectRows(options, 
node, cancelFlag)));
   }
 
   private static PipelineResult limitRun(
@@ -178,7 +189,8 @@ public class BeamEnumerableConverter extends ConverterImpl 
implements Enumerable
       BeamRelNode node,
       DoFn<Row, Void> doFn,
       Queue<Row> values,
-      int limitCount) {
+      int limitCount,
+      AtomicBoolean cancelFlag) {
     options.as(DirectOptions.class).setBlockOnRun(false);
     Pipeline pipeline = Pipeline.create(options);
     PCollection<Row> resultCollection = 
BeamSqlRelUtils.toPCollection(pipeline, node);
@@ -198,7 +210,7 @@ public class BeamEnumerableConverter extends ConverterImpl 
implements Enumerable
       }
 
       try {
-        if (values.size() >= limitCount) {
+        if (values.size() >= limitCount || cancelFlag.get()) {
           result.cancel();
           break;
         }
@@ -207,21 +219,78 @@ public class BeamEnumerableConverter extends 
ConverterImpl implements Enumerable
         break;
       }
     }
+    checkNotCancelled(cancelFlag);
 
     return result;
   }
 
-  private static void runCollector(PipelineOptions options, BeamRelNode node) {
+  private static void runCollector(
+      PipelineOptions options, BeamRelNode node, AtomicBoolean cancelFlag) {
     Pipeline pipeline = Pipeline.create(options);
     PCollection<Row> resultCollection = 
BeamSqlRelUtils.toPCollection(pipeline, node);
     resultCollection.apply(ParDo.of(new Collector()));
-    PipelineResult result = pipeline.run();
-    if (PipelineResult.State.FAILED.equals(result.waitUntilFinish())) {
+    PipelineResult result = run(pipeline, cancelFlag);
+    if (PipelineResult.State.FAILED.equals(waitUntilFinish(result, 
cancelFlag))) {
       throw new RuntimeException("Pipeline failed for unknown reason");
     }
   }
 
-  private static Queue<Row> collectRows(PipelineOptions options, BeamRelNode 
node) {
+  /**
+   * Runs the pipeline. If the DirectRunner would block in {@code run()} until 
the pipeline
+   * finishes, waits here instead, where the wait can be cancelled.
+   */
+  private static PipelineResult run(Pipeline pipeline, AtomicBoolean 
cancelFlag) {
+    DirectOptions options = pipeline.getOptions().as(DirectOptions.class);
+    boolean blockOnRun =
+        options.isBlockOnRun()
+            && 
options.getRunner().getCanonicalName().equals(DirectRunner.class.getName());
+    options.setBlockOnRun(false);
+    PipelineResult result = pipeline.run();
+    if (blockOnRun) {
+      waitUntilFinish(result, cancelFlag);
+    }
+    return result;
+  }
+
+  /**
+   * Waits for the pipeline to finish. If {@code cancelFlag} is set while 
waiting, cancels the
+   * pipeline so that the wait returns, and throws.
+   */
+  private static State waitUntilFinish(PipelineResult result, AtomicBoolean 
cancelFlag) {
+    Thread canceller =
+        new Thread(
+            () -> {
+              try {
+                while (!cancelFlag.get()) {
+                  Thread.sleep(100);
+                }
+                result.cancel();
+              } catch (InterruptedException e) {
+                // The pipeline finished.
+              } catch (IOException | RuntimeException e) {
+                LOG.warn("Failed to cancel the pipeline", e);
+              }
+            },
+            "BeamSqlPipelineCanceller");
+    canceller.setDaemon(true);
+    canceller.start();
+    try {
+      State state = result.waitUntilFinish();
+      checkNotCancelled(cancelFlag);
+      return state;
+    } finally {
+      canceller.interrupt();
+    }
+  }
+
+  private static void checkNotCancelled(AtomicBoolean cancelFlag) {
+    if (cancelFlag.get()) {
+      throw new CancellationException("Statement canceled");
+    }
+  }
+
+  private static Queue<Row> collectRows(
+      PipelineOptions options, BeamRelNode node, AtomicBoolean cancelFlag) {
     long id = options.getOptionsId();
     Queue<Row> values = new ConcurrentLinkedQueue<>();
 
@@ -233,14 +302,16 @@ public class BeamEnumerableConverter extends 
ConverterImpl implements Enumerable
         "collectRowList is only available in direct runner.");
 
     Collector.globalValues.put(id, values);
-
-    runCollector(options, node);
-
-    Collector.globalValues.remove(id);
+    try {
+      runCollector(options, node, cancelFlag);
+    } finally {
+      Collector.globalValues.remove(id);
+    }
     return values;
   }
 
-  private static Enumerable<Object> limitCollect(PipelineOptions options, 
BeamRelNode node) {
+  private static Enumerable<Object> limitCollect(
+      PipelineOptions options, BeamRelNode node, AtomicBoolean cancelFlag) {
     long id = options.getOptionsId();
     ConcurrentLinkedQueue<Row> values = new ConcurrentLinkedQueue<>();
 
@@ -254,8 +325,11 @@ public class BeamEnumerableConverter extends ConverterImpl 
implements Enumerable
     int limitCount = getLimitCount(node);
 
     Collector.globalValues.put(id, values);
-    limitRun(options, node, new Collector(), values, limitCount);
-    Collector.globalValues.remove(id);
+    try {
+      limitRun(options, node, new Collector(), values, limitCount, cancelFlag);
+    } finally {
+      Collector.globalValues.remove(id);
+    }
 
     // remove extra retrieved values
     while (values.size() > limitCount) {
@@ -378,14 +452,15 @@ public class BeamEnumerableConverter extends 
ConverterImpl implements Enumerable
     }
   }
 
-  private static Enumerable<Object> count(PipelineOptions options, BeamRelNode 
node) {
+  private static Enumerable<Object> count(
+      PipelineOptions options, BeamRelNode node, AtomicBoolean cancelFlag) {
     Pipeline pipeline = Pipeline.create(options);
     BeamSqlRelUtils.toPCollection(pipeline, node).apply(ParDo.of(new 
RowCounter()));
-    PipelineResult result = pipeline.run();
+    PipelineResult result = run(pipeline, cancelFlag);
 
     long count = 0;
     if (!containsUnboundedPCollection(pipeline)) {
-      if (PipelineResult.State.FAILED.equals(result.waitUntilFinish())) {
+      if (PipelineResult.State.FAILED.equals(waitUntilFinish(result, 
cancelFlag))) {
         throw new RuntimeException("Pipeline failed for unknown reason");
       }
       MetricQueryResults metrics =
diff --git 
a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/JdbcDriverTest.java
 
b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/JdbcDriverTest.java
index 4133fb8b070..b022d94777b 100644
--- 
a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/JdbcDriverTest.java
+++ 
b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/JdbcDriverTest.java
@@ -22,9 +22,11 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.nullValue;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import java.sql.Connection;
@@ -36,13 +38,21 @@ import java.sql.SQLException;
 import java.sql.Statement;
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.TimeZone;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+import org.apache.beam.sdk.extensions.sql.meta.catalog.InMemoryCatalogManager;
 import org.apache.beam.sdk.extensions.sql.meta.provider.ReadOnlyTableProvider;
+import 
org.apache.beam.sdk.extensions.sql.meta.provider.seqgen.GenerateSequenceTableProvider;
 import org.apache.beam.sdk.extensions.sql.meta.provider.test.TestBoundedTable;
 import org.apache.beam.sdk.extensions.sql.meta.provider.test.TestTableProvider;
 import 
org.apache.beam.sdk.extensions.sql.meta.provider.test.TestUnboundedTable;
@@ -449,6 +459,47 @@ public class JdbcDriverTest {
     assertFalse(resultSet2.next());
   }
 
+  /** Statement.cancel() (e.g. Ctrl+C in the SQL shell) must stop a 
never-ending pipeline. */
+  @Test
+  public void testCancelUnboundedQuery() throws Exception {
+    TestTableProvider testTables = new TestTableProvider();
+    InMemoryCatalogManager catalogManager = new InMemoryCatalogManager();
+    catalogManager.registerTableProvider(testTables);
+    catalogManager.registerTableProvider(new GenerateSequenceTableProvider());
+    Connection connection = JdbcDriver.connect(catalogManager, 
PipelineOptionsFactory.create());
+    Statement ddl = connection.createStatement();
+    ddl.execute(
+        "CREATE EXTERNAL TABLE seq (sequence BIGINT, event_time TIMESTAMP) 
TYPE 'sequence'");
+    ddl.execute("CREATE EXTERNAL TABLE sink (sequence BIGINT) TYPE 'test'");
+
+    ExecutorService executor = Executors.newCachedThreadPool();
+    try {
+      Statement insert = connection.createStatement();
+      Future<?> insertRun =
+          executor.submit(() -> insert.execute("INSERT INTO sink SELECT 
sequence FROM seq"));
+      while (testTables.tableRows("sink").isEmpty() && !insertRun.isDone()) {
+        Thread.sleep(100);
+      }
+      assertCancels(insert, insertRun);
+
+      for (String select :
+          Arrays.asList("SELECT sequence FROM seq", "SELECT sequence FROM seq 
LIMIT 1000000")) {
+        Statement statement = connection.createStatement();
+        assertCancels(statement, executor.submit(() -> 
statement.executeQuery(select)));
+      }
+    } finally {
+      executor.shutdownNow();
+    }
+  }
+
+  private static void assertCancels(Statement statement, Future<?> run) throws 
Exception {
+    statement.cancel();
+    ExecutionException e =
+        assertThrows(ExecutionException.class, () -> run.get(30, 
TimeUnit.SECONDS));
+    assertThat(e.getCause(), instanceOf(SQLException.class));
+    assertThat(e.getCause().getMessage(), containsString("Statement 
canceled"));
+  }
+
   private List<List<Object>> readResultSet(ResultSet result) throws Exception {
     List<List<Object>> results = new ArrayList<>();
 
diff --git 
a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamEnumerableConverterTest.java
 
b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamEnumerableConverterTest.java
index 697643d4715..42d138e2f38 100644
--- 
a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamEnumerableConverterTest.java
+++ 
b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamEnumerableConverterTest.java
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertTrue;
 
 import java.math.BigDecimal;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.beam.sdk.extensions.sql.impl.utils.CalciteUtils;
 import org.apache.beam.sdk.extensions.sql.meta.SchemaBaseBeamTable;
 import org.apache.beam.sdk.options.PipelineOptions;
@@ -77,7 +78,8 @@ public class BeamEnumerableConverterTest {
           
ImmutableList.of(ImmutableList.of(rexBuilder.makeBigintLiteral(BigDecimal.ZERO)));
       BeamRelNode node = new BeamValuesRel(cluster, type, tuples, 
RelTraitSet.createEmpty());
 
-      Enumerable<Object> enumerable = 
BeamEnumerableConverter.toEnumerable(options, node);
+      Enumerable<Object> enumerable =
+          BeamEnumerableConverter.toEnumerable(options, node, new 
AtomicBoolean());
       Enumerator<Object> enumerator = enumerable.enumerator();
 
       assertTrue(enumerator.moveNext());
@@ -97,7 +99,8 @@ public class BeamEnumerableConverterTest {
                   rexBuilder.makeBigintLiteral(BigDecimal.ONE)));
       BeamRelNode node = new BeamValuesRel(cluster, type, tuples, 
RelTraitSet.createEmpty());
 
-      Enumerable<Object> enumerable = 
BeamEnumerableConverter.toEnumerable(options, node);
+      Enumerable<Object> enumerable =
+          BeamEnumerableConverter.toEnumerable(options, node, new 
AtomicBoolean());
       Enumerator<Object> enumerator = enumerable.enumerator();
 
       assertTrue(enumerator.moveNext());
@@ -173,7 +176,8 @@ public class BeamEnumerableConverterTest {
               new FakeTable(),
               null);
 
-      Enumerable<Object> enumerable = 
BeamEnumerableConverter.toEnumerable(options, node);
+      Enumerable<Object> enumerable =
+          BeamEnumerableConverter.toEnumerable(options, node, new 
AtomicBoolean());
       Enumerator<Object> enumerator = enumerable.enumerator();
 
       assertTrue(enumerator.moveNext());
@@ -225,7 +229,8 @@ public class BeamEnumerableConverterTest {
                   
rexBuilder.makeNullLiteral(CalciteUtils.toRelDataType(TYPE_FACTORY, 
fieldType))));
       BeamRelNode node = new BeamValuesRel(cluster, type, tuples, 
RelTraitSet.createEmpty());
 
-      Enumerable<Object> enumerable = 
BeamEnumerableConverter.toEnumerable(options, node);
+      Enumerable<Object> enumerable =
+          BeamEnumerableConverter.toEnumerable(options, node, new 
AtomicBoolean());
       Enumerator<Object> enumerator = enumerable.enumerator();
 
       assertTrue(enumerator.moveNext());

Reply via email to