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

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 84dd8827a2 TIKA-4804: complete the call in 
fetchAndParseServerSideStreaming (#3008)
84dd8827a2 is described below

commit 84dd8827a234554cf9e503bdb583468c7379a6c9
Author: Davide Polato <[email protected]>
AuthorDate: Wed Aug 12 18:31:36 2026 +0200

    TIKA-4804: complete the call in fetchAndParseServerSideStreaming (#3008)
    
    The server-streaming handler delivered its reply and never called
    onCompleted(), so clients waited for a terminal signal that never came.
    One line, mirroring the unary handler, plus a regression test that
    counts the observer events.
    
    Signed-off-by: Davide Polato <[email protected]>
---
 CHANGES.txt                                        |  4 ++
 .../apache/tika/pipes/grpc/TikaGrpcServerImpl.java |  1 +
 .../apache/tika/pipes/grpc/TikaGrpcServerTest.java | 71 ++++++++++++++++++++++
 3 files changed, 76 insertions(+)

diff --git a/CHANGES.txt b/CHANGES.txt
index e46059d1a1..0fd4471d4c 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -11,6 +11,10 @@ Release 4.0.0 - ???
    * MagicDetector now compiles its regular expression once, in the
      constructor, instead of recompiling it on every match (TIKA-4796).
 
+   * tika-grpc's fetchAndParseServerSideStreaming now completes the call
+     after delivering its reply, instead of leaving the client waiting
+     for a terminal signal that never came (TIKA-4804).
+
 
 Release 4.0.0-beta-1 - 6/29/2026
 
diff --git 
a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java 
b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
index 7213217edb..8f63ee4767 100644
--- a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
+++ b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
@@ -245,6 +245,7 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
             return;
         }
         fetchAndParseImpl(request, responseObserver);
+        responseObserver.onCompleted();
     }
 
     @Override
diff --git 
a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java 
b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
index 2380735a65..2709f6f070 100644
--- a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
+++ b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTest.java
@@ -38,6 +38,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import com.asarkar.grpc.test.GrpcCleanupExtension;
 import com.asarkar.grpc.test.Resources;
@@ -377,6 +378,76 @@ public class TikaGrpcServerTest {
         return TikaGrpc.newBlockingStub(channel);
     }
 
+    /**
+     * TIKA-4804: the server-streaming variant must close the call. With the 
in-process
+     * transport and a direct executor the whole handler runs inside the stub 
call, so
+     * the observer counts are final when it returns and nothing here needs to 
wait.
+     */
+    @Test
+    public void testServerSideStreamingSendsTerminalSignal(Resources 
resources) throws Exception {
+        String serverName = InProcessServerBuilder.generateName();
+        Server server = InProcessServerBuilder
+                .forName(serverName)
+                .directExecutor()
+                .addService(new 
TikaGrpcServerImpl(tikaConfigUnlocked.toAbsolutePath().toString()))
+                .build()
+                .start();
+        resources.register(server, Duration.ofSeconds(10));
+
+        ManagedChannel channel = InProcessChannelBuilder
+                .forName(serverName)
+                .directExecutor()
+                .build();
+        resources.register(channel, Duration.ofSeconds(10));
+        TikaGrpc.TikaStub tikaStub = TikaGrpc.newStub(channel);
+
+        // The fetcher must come from the config file: one saved at runtime 
through
+        // saveFetcher is not visible to the forked worker, and the fetch 
would fail.
+        String fetcherId = createFetcherId(1);
+        String fetchKey = "tika4804-" + UUID.randomUUID() + ".html";
+        File testFile = new File("target", fetchKey);
+        FileUtils.writeStringToFile(testFile,
+                "<html><body>terminal signal</body></html>", 
StandardCharsets.UTF_8);
+
+        List<FetchAndParseReply> replies = Collections.synchronizedList(new 
ArrayList<>());
+        AtomicInteger errors = new AtomicInteger();
+        AtomicInteger completions = new AtomicInteger();
+        StreamObserver<FetchAndParseReply> observer = new StreamObserver<>() {
+            @Override
+            public void onNext(FetchAndParseReply reply) {
+                replies.add(reply);
+            }
+
+            @Override
+            public void onError(Throwable throwable) {
+                errors.incrementAndGet();
+            }
+
+            @Override
+            public void onCompleted() {
+                completions.incrementAndGet();
+            }
+        };
+
+        try {
+            tikaStub.fetchAndParseServerSideStreaming(FetchAndParseRequest
+                    .newBuilder()
+                    .setFetcherId(fetcherId)
+                    .setFetchKey(fetchKey)
+                    .build(), observer);
+
+            assertEquals(1, replies.size(), "one reply for one fetch key");
+            assertEquals(PipesResult.RESULT_STATUS.PARSE_SUCCESS.name(),
+                    replies.get(0).getStatus(),
+                    "the fixture must actually parse, or this test proves 
nothing");
+            assertEquals(0, errors.get(), "no error on the happy path");
+            assertEquals(1, completions.get(),
+                    "server streaming must send a terminal signal");
+        } finally {
+            FileUtils.deleteQuietly(testFile);
+        }
+    }
+
     @Test
     public void testBiStream(Resources resources) throws Exception {
         String serverName = InProcessServerBuilder.generateName();

Reply via email to