Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.hugegraph.computer.core.sender;

import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;

import org.apache.hugegraph.computer.core.network.message.MessageType;

Expand All @@ -26,11 +27,18 @@ public class QueuedMessage {
private final int partitionId;
private final MessageType type;
private final ByteBuffer buffer;
private final CompletableFuture<Void> controlFuture;

public QueuedMessage(int partitionId, MessageType type, ByteBuffer buffer) {
this(partitionId, type, buffer, null);
}

public QueuedMessage(int partitionId, MessageType type, ByteBuffer buffer,
CompletableFuture<Void> controlFuture) {
this.partitionId = partitionId;
this.type = type;
this.buffer = buffer;
this.controlFuture = controlFuture;
}

public int partitionId() {
Expand All @@ -44,4 +52,8 @@ public MessageType type() {
public ByteBuffer buffer() {
return this.buffer;
}

public CompletableFuture<Void> controlFuture() {
return this.controlFuture;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,20 @@ public void addWorkerClient(int workerId, TransportClient client) {
public CompletableFuture<Void> send(int workerId, MessageType type)
throws InterruptedException {
WorkerChannel channel = this.channels[channelId(workerId)];
CompletableFuture<Void> future = channel.newFuture();
future.whenComplete((r, e) -> {
channel.resetFuture(future);
});
CompletableFuture<Void> future = new CompletableFuture<>();
if (!channel.setControlFuture(future)) {
return future;
}
/*
* Control message just need message type is enough,
* partitionId = -1 and buffer = null represents a meaningless value
*/
channel.queue.put(new QueuedMessage(-1, type, null));
try {
channel.queue.put(new QueuedMessage(-1, type, null, future));
} catch (InterruptedException e) {
channel.completeControlFuture(future, e);
throw e;
}
return future;
}

Expand All @@ -108,7 +113,7 @@ public void send(int workerId, QueuedMessage message)
public void transportExceptionCaught(TransportException cause, ConnectionId connectionId) {
for (WorkerChannel channel : this.channels) {
if (channel.client.connectionId().equals(connectionId)) {
channel.futureRef.get().completeExceptionally(cause);
channel.transportExceptionCaught(cause);
}
}
}
Expand Down Expand Up @@ -137,11 +142,19 @@ public void run() {
++emptyQueueCount;
continue;
}
if (channel.doSend(message)) {
// Only consume the message after it is sent
try {
if (channel.doSend(message)) {
// Only consume the message after it is sent
channel.queue.take();
} else {
++busyClientCount;
}
} catch (TransportException | RuntimeException e) {
channel.failDataSend(e);
// Discard the failed data message to keep sending
channel.queue.take();
} else {
++busyClientCount;
LOG.warn("Failed to send {} message to {}, " +
"discard it", message.type(), channel, e);
}
}
int channelCount = channels.length;
Expand Down Expand Up @@ -172,9 +185,6 @@ public void run() {
"Interrupted when waiting for message " +
"queue not empty");
}
} catch (TransportException e) {
// TODO: should handle this in main workflow thread
throw new ComputerException("Failed to send message", e);
}
}
LOG.info("The send-executor is terminated");
Expand Down Expand Up @@ -227,75 +237,127 @@ 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;
private final AtomicReference<Throwable> dataFailureRef;

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<>();
this.dataFailureRef = 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;

if (e != null) {
LOG.info("Failed to start session connected to {}", this);
future.completeExceptionally(e);
} else {
LOG.info("Start session connected to {}", this);
future.complete(null);
}
});
public void sendStartMessage(CompletableFuture<Void> future) {
if (!this.controlFutureInFlight(future)) {
return;
}
try {
this.client.startSessionAsync().whenComplete((r, e) -> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

‼️ 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.

if (e != null) {
LOG.info("Failed to start session connected to {}", this);
} else {
LOG.info("Start session connected to {}", this);
}
this.completeControlFuture(future, e);
});
} catch (TransportException e) {
this.completeControlFuture(future, e);
} catch (RuntimeException e) {
this.completeControlFuture(future, e);
}
}

public void sendFinishMessage(CompletableFuture<Void> future) {
if (!this.controlFutureInFlight(future)) {
return;
}
try {
this.client.finishSessionAsync().whenComplete((r, e) -> {
if (e != null) {
LOG.info("Failed to finish session connected to {}", this);
} else {
LOG.info("Finish session connected to {}", this);
}
this.completeControlFuture(future, e);
});
} catch (TransportException e) {
this.completeControlFuture(future, e);
} catch (RuntimeException e) {
this.completeControlFuture(future, e);
}
}

public void transportExceptionCaught(TransportException cause) {
CompletableFuture<Void> future = this.controlFutureRef.get();
if (future == null) {
this.failDataSend(cause);
} else {
this.completeControlFuture(future, cause);
}
}

public void failControlFuture(Throwable cause) {
CompletableFuture<Void> future = this.controlFutureRef.getAndSet(null);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

‼️ send() now only places the control future in the queue (lines 88-93), while controlFutureRef stays null until the sender thread dispatches that entry at lines 254/279. A connection callback in that window therefore makes this getAndSet(null) discard the original failure; FINISH may later succeed, or the caller may wait until timeout without the real cause, even though preceding data was lost. Please reserve the future before enqueue while retaining clear-before-complete ordering, or explicitly fail queued as well as in-flight controls, and add a regression that injects the exception after send() returns but before the session method is called.

if (future != null) {
future.completeExceptionally(cause);
}
}

public void failDataSend(Throwable cause) {
this.dataFailureRef.compareAndSet(null, cause);
this.failControlFuture(this.dataFailureRef.get());
}

public void sendFinishMessage() throws TransportException {
this.client.finishSessionAsync().whenComplete((r, e) -> {
CompletableFuture<Void> future = this.futureRef.get();
assert future != null;

if (e != null) {
LOG.info("Failed to finish session connected to {}", this);
future.completeExceptionally(e);
} else {
LOG.info("Finish session connected to {}", this);
future.complete(null);
private boolean setControlFuture(CompletableFuture<Void> future) {
Throwable failure = this.dataFailureRef.get();
if (failure != null) {
future.completeExceptionally(failure);
return false;
}
if (this.controlFutureRef.compareAndSet(null, future)) {
failure = this.dataFailureRef.get();
if (failure == null) {
return true;
}
});
this.completeControlFuture(future, failure);
return false;
}
ComputerException e = new ComputerException(
"The origin future must be null");
future.completeExceptionally(e);
return false;
}

private boolean controlFutureInFlight(CompletableFuture<Void> future) {
return this.controlFutureRef.get() == future;
}

private void completeControlFuture(CompletableFuture<Void> future,
Throwable cause) {
if (!this.controlFutureRef.compareAndSet(future, null)) {
return;
}
if (cause == null) {
future.complete(null);
} else {
future.completeExceptionally(cause);
}
}

public boolean sendDataMessage(QueuedMessage message)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

‼️ A synchronous failure on this data path still terminates the sole send executor without resolving control work. TransportClient.send() declares TransportException and the underlying session/send function can also throw unchecked state failures; either escapes the loop, while a FINISH future already reserved behind that data message remains queued and incomplete. Please transition the sender to an explicit failed state that exceptionally completes every queued/in-flight control future (or otherwise keep the executor alive while propagating the failure), and add regressions for both synchronous TransportException and RuntimeException with a queued FINISH.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ public void testSend() throws IOException {

@Test
public void testDataUniformity() throws IOException {
NettyTransportClient client = (NettyTransportClient) this.oneClient();
byte[] sourceBytes1 = StringEncodeUtil.encode("test data message");
byte[] sourceBytes2 = StringEncodeUtil.encode("test data edge");
byte[] sourceBytes3 = StringEncodeUtil.encode("test data vertex");
Expand Down Expand Up @@ -165,6 +164,7 @@ public void testDataUniformity() throws IOException {
return null;
}).when(serverHandler).handle(Mockito.any(), Mockito.eq(1), Mockito.any());

NettyTransportClient client = (NettyTransportClient) this.oneClient();
client.startSession();
client.send(MessageType.MSG, 1, ByteBuffer.wrap(sourceBytes1));
client.send(MessageType.EDGE, 1, ByteBuffer.wrap(sourceBytes2));
Expand Down Expand Up @@ -274,12 +274,12 @@ public void testFlowControl() throws IOException {

@Test
public void testHandlerException() throws IOException {
NettyTransportClient client = (NettyTransportClient) this.oneClient();
client.startSession();

Mockito.doThrow(new RuntimeException("test exception")).when(serverHandler)
.handle(Mockito.any(), Mockito.anyInt(), Mockito.any());

NettyTransportClient client = (NettyTransportClient) this.oneClient();
client.startSession();

ByteBuffer buffer = ByteBuffer.wrap(StringEncodeUtil.encode("test data"));
boolean send = client.send(MessageType.MSG, 1, buffer);
Assert.assertTrue(send);
Expand Down
Loading
Loading