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

Abacn 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 2c11a5fbc04 Fail fast in artifact staging when storing an artifact 
fails (#39367)
2c11a5fbc04 is described below

commit 2c11a5fbc043a1b82dc4f236f4fef34c812d11ef
Author: Elia Liu <[email protected]>
AuthorDate: Tue Sep 1 00:20:46 2026 +1000

    Fail fast in artifact staging when storing an artifact fails (#39367)
    
    * Propagate artifact storage failures to the staging client instead of 
hanging
    
    A StoreArtifact task that failed with an unchecked exception, such as
    InvalidPathException on Windows, was never reported: the chunk producer
    blocked forever and the reverse-retrieval stream was never terminated,
    so clients hung in ArtifactStagingService.offer until their timeout.
    Report every failure to the pending-bytes semaphore, make the producer
    observe it promptly, and error the stream with the root cause.
---
 .../artifact/ArtifactStagingService.java           | 34 ++++++++---
 .../artifact/ArtifactStagingServiceTest.java       | 68 ++++++++++++++++++++++
 2 files changed, 95 insertions(+), 7 deletions(-)

diff --git 
a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/artifact/ArtifactStagingService.java
 
b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/artifact/ArtifactStagingService.java
index 8b403f2f25f..f6c4b4d4f34 100644
--- 
a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/artifact/ArtifactStagingService.java
+++ 
b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/artifact/ArtifactStagingService.java
@@ -223,15 +223,19 @@ public class ArtifactStagingService
     }
 
     synchronized void aquire(int permits) throws Exception {
-      while (usedPermits >= totalPermits) {
-        if (exception != null) {
-          throw exception;
-        }
+      while (exception == null && usedPermits >= totalPermits) {
         this.wait();
       }
+      checkException();
       usedPermits += permits;
     }
 
+    synchronized void checkException() throws Exception {
+      if (exception != null) {
+        throw exception;
+      }
+    }
+
     synchronized void release(int permits) {
       usedPermits -= permits;
       this.notifyAll();
@@ -282,13 +286,21 @@ public class ArtifactStagingService
             .setTypeUrn(dest.getTypeUrn())
             .setTypePayload(dest.getTypePayload())
             .build();
-      } catch (IOException | InterruptedException exn) {
+      } catch (Exception exn) {
         // As this thread will no longer be draining the queue, we don't want 
to get stuck writing
-        // to it.
+        // to it. This must happen for unchecked exceptions as well: 
getDestination can throw e.g.
+        // InvalidPathException, and leaving the error unset would block the 
producer forever.
         totalPendingBytes.setException(exn);
+        // Free a producer already blocked in put; its next aquire observes 
the exception.
+        bytesQueue.clear();
         LOG.error("Exception staging artifacts", exn);
+        if (exn instanceof InterruptedException) {
+          Thread.currentThread().interrupt();
+        }
         if (exn instanceof IOException) {
           throw (IOException) exn;
+        } else if (exn instanceof RuntimeException) {
+          throw (RuntimeException) exn;
         } else {
           throw new RuntimeException(exn);
         }
@@ -421,8 +433,16 @@ public class ArtifactStagingService
                 }
               }
             } catch (Exception exn) {
-              LOG.error("Error submitting.", exn);
+              if (exn instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+              }
               onError(exn);
+              // Terminate the stream towards the client, which would 
otherwise wait forever.
+              responseObserver.onError(
+                  Status.INTERNAL
+                      .withDescription("Error staging artifacts: " + exn)
+                      .withCause(exn)
+                      .asException());
             }
             break;
 
diff --git 
a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/artifact/ArtifactStagingServiceTest.java
 
b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/artifact/ArtifactStagingServiceTest.java
index 5610e4f5bb5..abdce59458d 100644
--- 
a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/artifact/ArtifactStagingServiceTest.java
+++ 
b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/artifact/ArtifactStagingServiceTest.java
@@ -18,7 +18,10 @@
 package org.apache.beam.runners.fnexecution.artifact;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
+import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.util.Iterator;
 import java.util.List;
@@ -135,6 +138,22 @@ public class ArtifactStagingServiceTest {
     }
   }
 
+  /** Streams each artifact one byte per chunk, to exercise the service's 
chunk buffering. */
+  private static class OneBytePerChunkArtifactRetrievalService
+      extends FakeArtifactRetrievalService {
+    @Override
+    public void getArtifact(
+        ArtifactApi.GetArtifactRequest request,
+        StreamObserver<ArtifactApi.GetArtifactResponse> responseObserver) {
+      ByteString data = request.getArtifact().getTypePayload();
+      for (int i = 0; i < data.size(); i++) {
+        responseObserver.onNext(
+            
ArtifactApi.GetArtifactResponse.newBuilder().setData(data.substring(i, i + 
1)).build());
+      }
+      responseObserver.onCompleted();
+    }
+  }
+
   private String getArtifact(RunnerApi.ArtifactInformation artifact) {
     ByteString all = ByteString.EMPTY;
     Iterator<ArtifactApi.GetArtifactResponse> response =
@@ -166,6 +185,55 @@ public class ArtifactStagingServiceTest {
     checkArtifacts(contentsList, staged.get("env2"));
   }
 
+  @SuppressWarnings("InlineMeInliner") // inline `Strings.repeat()` - Java 11+ 
API only
+  @Test(timeout = 60_000)
+  public void testDestinationFailureFailsOfferInsteadOfHanging() throws 
Exception {
+    // Resolving the destination of a staged artifact can throw an unchecked 
exception, e.g.
+    // InvalidPathException on Windows where the generated filename may 
contain characters that
+    // are illegal in paths (https://github.com/apache/beam/issues/39364). 
This must fail the
+    // offering client instead of stalling the transfer forever.
+    ArtifactStagingService failingStagingService =
+        new ArtifactStagingService(
+            new ArtifactStagingService.ArtifactDestinationProvider() {
+              @Override
+              public ArtifactStagingService.ArtifactDestination getDestination(
+                  String stagingToken, String name) {
+                throw new InvalidPathException(name, "Illegal char simulated");
+              }
+
+              @Override
+              public void removeStagedArtifacts(String stagingToken) {}
+            });
+    grpcCleanup.register(
+        InProcessServerBuilder.forName("failing-server")
+            .directExecutor()
+            .addService(failingStagingService)
+            .build()
+            .start());
+    ManagedChannel failingChannel =
+        
grpcCleanup.register(InProcessChannelBuilder.forName("failing-server").build());
+    ArtifactStagingServiceGrpc.ArtifactStagingServiceStub failingStub =
+        ArtifactStagingServiceGrpc.newStub(failingChannel);
+
+    // More chunks than the service buffers per artifact, so staging cannot 
run to completion
+    // before the destination failure is observed.
+    String contents = Strings.repeat("x", 300);
+    failingStagingService.registerJob(
+        "failingToken",
+        ImmutableMap.of(
+            "env1", 
ImmutableList.of(FakeArtifactRetrievalService.resolvedArtifact(contents))));
+
+    ExecutionException exn =
+        assertThrows(
+            ExecutionException.class,
+            () ->
+                ArtifactStagingService.offer(
+                    new OneBytePerChunkArtifactRetrievalService(), 
failingStub, "failingToken"));
+    assertTrue(
+        "Expected the destination failure, got: " + exn.getCause(),
+        exn.getCause().getMessage().contains("Illegal char simulated"));
+  }
+
   private void checkArtifacts(
       List<String> expectedContents, List<RunnerApi.ArtifactInformation> 
staged) {
     assertEquals(

Reply via email to