-
Notifications
You must be signed in to change notification settings - Fork 52
fix: prevent sender control future race #357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
abca23b
1f28ea1
6672de9
3d8441a
187a83f
ff76512
61bbf71
ca692c6
99e825d
e60d48d
f82527a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -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; | ||
|
|
@@ -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"); | ||
|
|
@@ -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) -> { | ||
| 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
controlFutureRefpointing atfuture.ClientSession.startAsync()can throwIllegalArgumentExceptionfrom its state checks, and its send function can propagate unchecked failures; neither reaches thisTransportExceptioncatch, so the returned future stays incomplete andSender.run()also exits. Please clear and complete the control future for synchronous runtime failures here and infinishSessionAsync(), then add a client-stub regression that throws synchronously and verifies both the returned future and the executor's intended state.