imbajin commented on code in PR #357:
URL: 
https://github.com/apache/hugegraph-computer/pull/357#discussion_r3652466487


##########
computer/computer-test/src/main/java/org/apache/hugegraph/computer/suite/integrate/SenderIntegrateTest.java:
##########
@@ -331,6 +442,103 @@ private WorkerService initWorker(String[] args) {
         return service;
     }
 
+    private static void waitForServices(List<CompletableFuture<Void>> futures) 
{
+        CompletableFuture<Void> result = new CompletableFuture<>();
+        for (CompletableFuture<Void> future : futures) {
+            future.whenComplete((r, e) -> {
+                if (e != null) {
+                    result.completeExceptionally(e);
+                }
+            });
+        }
+        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
+                         .whenComplete((r, e) -> {
+            if (e == null) {
+                result.complete(null);
+            } else {
+                result.completeExceptionally(e);
+            }
+        });
+        try {
+            result.get(SERVICE_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
+        } catch (TimeoutException e) {
+            throw new ComputerException("Timed out to wait for master and " +
+                                        "worker services", e);
+        } catch (ExecutionException e) {
+            throw new ComputerException("Failed to wait for master and " +
+                                        "worker services", e.getCause());
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new ComputerException("Interrupted when waiting for master " 
+
+                                        "and worker services", e);
+        }
+    }
+
+    private static void closeServicesAndJoin(ServiceLifecycle lifecycle,
+                                             List<Thread> threads) {
+        lifecycle.closeAll();

Review Comment:
   ⚠️ `closeAll()` runs every closer sequentially before any service thread is 
interrupted, but registration order is concurrent. If the master closer runs 
before a still-active worker closer, `MasterService.close()` waits for 
`workerCloseDone` that the queued worker closer has not had a chance to send, 
so this fail-fast path can block for the full five-minute BSP timeout; one 
thrown closer also skips the remaining cleanup. Please encode 
worker-before-master shutdown (or close services concurrently), ensure one 
close failure cannot prevent the rest, and cover a master-first registration 
with one failed and one active worker.



##########
computer/computer-core/src/main/java/org/apache/hugegraph/computer/core/sender/QueuedMessageSender.java:
##########
@@ -227,75 +224,102 @@ private static class WorkerChannel {
         private final MessageQueue queue;
         // Each target worker has a TransportClient
         private final TransportClient client;
-        private final AtomicReference<CompletableFuture<Void>> futureRef;
+        private final AtomicReference<CompletableFuture<Void>> 
controlFutureRef;
 
         public WorkerChannel(int workerId, MessageQueue queue,
                              TransportClient client) {
             this.workerId = workerId;
             this.queue = queue;
             this.client = client;
-            this.futureRef = new AtomicReference<>();
-        }
-
-        public CompletableFuture<Void> newFuture() {
-            CompletableFuture<Void> future = new CompletableFuture<>();
-            if (!this.futureRef.compareAndSet(null, future)) {
-                throw new ComputerException("The origin future must be null");
-            }
-            return future;
-        }
-
-        public void resetFuture(CompletableFuture<Void> future) {
-            if (!this.futureRef.compareAndSet(future, null)) {
-                throw new ComputerException("Failed to reset futureRef, " +
-                                            "expect future object is %s, " +
-                                            "but some thread modified it",
-                                            future);
-            }
+            this.controlFutureRef = new AtomicReference<>();
         }
 
         public boolean doSend(QueuedMessage message)
                               throws TransportException, InterruptedException {
             switch (message.type()) {
                 case START:
-                    this.sendStartMessage();
+                    this.sendStartMessage(message.controlFuture());
                     return true;
                 case FINISH:
-                    this.sendFinishMessage();
+                    this.sendFinishMessage(message.controlFuture());
                     return true;
                 default:
                     return this.sendDataMessage(message);
             }
         }
 
-        public void sendStartMessage() throws TransportException {
-            this.client.startSessionAsync().whenComplete((r, e) -> {
-                CompletableFuture<Void> future = this.futureRef.get();
-                assert future != null;
-
+        public void sendStartMessage(CompletableFuture<Void> future)
+                                     throws TransportException {
+            try {
+                this.setControlFuture(future);
+            } catch (ComputerException e) {
+                // The control future has been completed exceptionally
+                return;
+            }
+            try {
+                this.client.startSessionAsync().whenComplete((r, e) -> {

Review Comment:
   ‼️ A synchronous unchecked exception from this call leaves 
`controlFutureRef` pointing at `future`. `ClientSession.startAsync()` can throw 
`IllegalArgumentException` from its state checks, and its send function can 
propagate unchecked failures; neither reaches this `TransportException` catch, 
so the returned future stays incomplete and `Sender.run()` also exits. Please 
clear and complete the control future for synchronous runtime failures here and 
in `finishSessionAsync()`, then add a client-stub regression that throws 
synchronously and verifies both the returned future and the executor's intended 
state.



##########
computer/computer-test/src/main/java/org/apache/hugegraph/computer/core/sender/QueuedMessageSenderTest.java:
##########
@@ -64,4 +74,195 @@ public void testInitAndClose() {
         Assert.assertTrue(ImmutableSet.of(Thread.State.TERMINATED)
                                       .contains(sendExecutor.getState()));
     }
+
+    @Test
+    public void 
testControlFutureCanQueueNextControlBeforeCompletionDependentFinishes()
+            throws Exception {
+        QueuedMessageSender sender = new QueuedMessageSender(this.config);
+        ControlFutureClient client = new ControlFutureClient();
+        sender.addWorkerClient(1, client);
+        sender.addWorkerClient(2, new MockTransportClient());
+        sender.init();
+
+        CountDownLatch completionStarted = new CountDownLatch(1);
+        CountDownLatch allowCompletion = new CountDownLatch(1);
+        Thread completionThread = null;
+        try {
+            CompletableFuture<Void> startFuture = sender.send(1,
+                                                               
MessageType.START);
+            Assert.assertTrue(client.awaitStart());
+            startFuture.whenComplete((r, e) -> {
+                completionStarted.countDown();
+                try {
+                    allowCompletion.await();
+                } catch (InterruptedException exception) {
+                    Thread.currentThread().interrupt();
+                    throw new AssertionError(exception);
+                }
+            });
+
+            completionThread = new Thread(client::completeStart);
+            completionThread.start();
+            Assert.assertTrue(completionStarted.await(1, TimeUnit.SECONDS));
+
+            CompletableFuture<Void> finishFuture = sender.send(1,
+                                                                
MessageType.FINISH);
+            allowCompletion.countDown();

Review Comment:
   ⚠️ This releases the blocked completion dependent before confirming that 
FINISH was dispatched. An implementation that still clears the control 
reference only after completion dependents return can therefore process FINISH 
after this countdown and pass the test. Please await `client.awaitFinish()` 
before releasing `allowCompletion` (while keeping the `finally` release) so the 
test actually proves the next control message proceeds while the prior 
dependent is still blocked.



##########
computer/computer-test/src/main/java/org/apache/hugegraph/computer/core/sender/QueuedMessageSenderTest.java:
##########
@@ -64,4 +74,195 @@ public void testInitAndClose() {
         Assert.assertTrue(ImmutableSet.of(Thread.State.TERMINATED)
                                       .contains(sendExecutor.getState()));
     }
+
+    @Test
+    public void 
testControlFutureCanQueueNextControlBeforeCompletionDependentFinishes()
+            throws Exception {
+        QueuedMessageSender sender = new QueuedMessageSender(this.config);
+        ControlFutureClient client = new ControlFutureClient();
+        sender.addWorkerClient(1, client);
+        sender.addWorkerClient(2, new MockTransportClient());
+        sender.init();
+
+        CountDownLatch completionStarted = new CountDownLatch(1);
+        CountDownLatch allowCompletion = new CountDownLatch(1);
+        Thread completionThread = null;
+        try {
+            CompletableFuture<Void> startFuture = sender.send(1,
+                                                               
MessageType.START);
+            Assert.assertTrue(client.awaitStart());
+            startFuture.whenComplete((r, e) -> {
+                completionStarted.countDown();
+                try {
+                    allowCompletion.await();
+                } catch (InterruptedException exception) {
+                    Thread.currentThread().interrupt();
+                    throw new AssertionError(exception);
+                }
+            });
+
+            completionThread = new Thread(client::completeStart);
+            completionThread.start();
+            Assert.assertTrue(completionStarted.await(1, TimeUnit.SECONDS));
+
+            CompletableFuture<Void> finishFuture = sender.send(1,
+                                                                
MessageType.FINISH);
+            allowCompletion.countDown();
+            completionThread.join(TimeUnit.SECONDS.toMillis(1));
+            Assert.assertFalse(completionThread.isAlive());
+            Assert.assertTrue(client.awaitFinish());
+            client.completeFinish();
+            finishFuture.get(1, TimeUnit.SECONDS);
+        } finally {
+            allowCompletion.countDown();
+            if (completionThread != null) {
+                completionThread.join(TimeUnit.SECONDS.toMillis(1));
+            }
+            sender.close();
+        }
+    }
+
+    @Test
+    public void testTransportExceptionCompletesInFlightControlFuture()
+            throws Exception {
+        QueuedMessageSender sender = new QueuedMessageSender(this.config);
+        ControlFutureClient client = new ControlFutureClient();
+        sender.addWorkerClient(1, client);
+        sender.addWorkerClient(2, new MockTransportClient());
+        sender.init();
+
+        try {
+            CompletableFuture<Void> startFuture = sender.send(1,
+                                                               
MessageType.START);
+            Assert.assertTrue(client.awaitStart());
+
+            TransportException cause =
+                    new TransportException("connection failed");
+            sender.transportExceptionCaught(cause, client.connectionId());
+            assertFutureFailedWith(startFuture, cause);
+
+            client.completeStart();

Review Comment:
   ⚠️ This completes the stale START callback immediately after the transport 
exception, without first installing a next-generation FINISH future. It 
therefore does not protect the identity-sensitive CAS behavior in 
`completeControlFuture()`; an unconditional clear could still pass while later 
stranding FINISH. Please send and await FINISH first, then complete the stale 
START callback, and finally complete/assert FINISH so the test locks in 
cross-generation isolation.



-- 
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