mirror of
https://github.com/layue13/MoonBridge.git
synced 2026-10-26 19:03:49 +00:00
Split LocalMessaging operations and failure helpers; deduplicate outbound dispatch
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
765dcffeaa
commit
fc951bfb7b
4 files changed
+266
-219
No files matched your search
+55
-219
@@ -12,6 +12,8 @@ import dev.moonbridge.messaging.SendReceipt;
|
|||||||
import dev.moonbridge.messaging.SendResult;
|
import dev.moonbridge.messaging.SendResult;
|
||||||
import dev.moonbridge.messaging.Subscription;
|
import dev.moonbridge.messaging.Subscription;
|
||||||
|
|
||||||
|
import static dev.moonbridge.messaging.internal.MessagingFailures.*;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -22,18 +24,16 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionStage;
|
import java.util.concurrent.CompletionStage;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ForkJoinPool;
|
import java.util.concurrent.ForkJoinPool;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Host-side scope registry and bounded local message dispatcher. The host owns the supplied scheduler and
|
* Host-side scope registry and bounded local message dispatcher. The host owns the supplied scheduler and
|
||||||
@@ -56,6 +56,13 @@ public final class LocalMessaging implements AutoCloseable {
|
|||||||
private final Map<String, Scope> scopes = new HashMap<String, Scope>();
|
private final Map<String, Scope> scopes = new HashMap<String, Scope>();
|
||||||
private final Map<String, ChannelEntry> channels = new HashMap<String, ChannelEntry>();
|
private final Map<String, ChannelEntry> channels = new HashMap<String, ChannelEntry>();
|
||||||
private final Set<Operation<?>> operations = new HashSet<Operation<?>>();
|
private final Set<Operation<?>> operations = new HashSet<Operation<?>>();
|
||||||
|
private final OperationHost operationHost = new OperationHost() {
|
||||||
|
@Override public ScheduledExecutorService scheduler() { return scheduler; }
|
||||||
|
@Override public void complete(Operation<?> operation, Runnable completion) {
|
||||||
|
completeOperation(operation, completion);
|
||||||
|
}
|
||||||
|
@Override public void finished(Operation<?> operation) { operationFinished(operation); }
|
||||||
|
};
|
||||||
private boolean closed;
|
private boolean closed;
|
||||||
private int outboundInFlight;
|
private int outboundInFlight;
|
||||||
private int inboundInFlight;
|
private int inboundInFlight;
|
||||||
@@ -182,7 +189,7 @@ public final class LocalMessaging implements AutoCloseable {
|
|||||||
if (inboundInFlight >= MAX_IN_FLIGHT) return failed(new MessagingException(
|
if (inboundInFlight >= MAX_IN_FLIGHT) return failed(new MessagingException(
|
||||||
MessagingException.Code.BACKPRESSURED, "inbound request limit reached"));
|
MessagingException.Code.BACKPRESSURED, "inbound request limit reached"));
|
||||||
inboundInFlight++;
|
inboundInFlight++;
|
||||||
operation = new Operation<Message>(handler.scope, new Runnable() {
|
operation = new Operation<Message>(operationHost, handler.scope.operations, new Runnable() {
|
||||||
@Override public void run() { inboundInFlight--; }
|
@Override public void run() { inboundInFlight--; }
|
||||||
});
|
});
|
||||||
operations.add(operation);
|
operations.add(operation);
|
||||||
@@ -366,7 +373,7 @@ public final class LocalMessaging implements AutoCloseable {
|
|||||||
private void operationFinished(Operation<?> operation) {
|
private void operationFinished(Operation<?> operation) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
operations.remove(operation);
|
operations.remove(operation);
|
||||||
operation.scope.operations.remove(operation);
|
operation.scopeOperations.remove(operation);
|
||||||
operation.release.run();
|
operation.release.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,57 +399,6 @@ public final class LocalMessaging implements AutoCloseable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long timeoutMillis(Duration timeout) {
|
|
||||||
Objects.requireNonNull(timeout, "timeout");
|
|
||||||
final long millis;
|
|
||||||
try {
|
|
||||||
millis = timeout.toMillis();
|
|
||||||
} catch (ArithmeticException overflow) {
|
|
||||||
throw new IllegalArgumentException("timeout is too large", overflow);
|
|
||||||
}
|
|
||||||
if (millis <= 0 || millis > MAX_REQUEST_TIMEOUT.toMillis()) {
|
|
||||||
throw new IllegalArgumentException("timeout must be positive and at most 60 seconds");
|
|
||||||
}
|
|
||||||
return millis;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MessagingException handlerFailure(Throwable failure) {
|
|
||||||
return new MessagingException(MessagingException.Code.HANDLER_FAILED,
|
|
||||||
"message handler failed", failure);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SendReceipt sendReceiptForFailure(UUID messageId, Throwable failure) {
|
|
||||||
Throwable cause = unwrap(failure);
|
|
||||||
if (cause instanceof MessagingException) {
|
|
||||||
MessagingException messagingFailure = (MessagingException) cause;
|
|
||||||
switch (messagingFailure.code()) {
|
|
||||||
case TIMED_OUT: return new SendReceipt(messageId, SendResult.TIMED_OUT);
|
|
||||||
case BACKPRESSURED: return new SendReceipt(messageId, SendResult.BACKPRESSURED);
|
|
||||||
case NOT_CONNECTED: return new SendReceipt(messageId, SendResult.NOT_CONNECTED);
|
|
||||||
case NO_HANDLER: return new SendReceipt(messageId, SendResult.NO_SUBSCRIBER);
|
|
||||||
case REJECTED: return new SendReceipt(messageId, SendResult.REJECTED);
|
|
||||||
case HANDLER_FAILED:
|
|
||||||
case PROTOCOL_ERROR: return new SendReceipt(messageId, SendResult.FAILED);
|
|
||||||
case CLOSED: throw messagingFailure;
|
|
||||||
default: return new SendReceipt(messageId, SendResult.FAILED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new SendReceipt(messageId, SendResult.FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Throwable unwrap(Throwable failure) {
|
|
||||||
if (failure instanceof java.util.concurrent.CompletionException && failure.getCause() != null) {
|
|
||||||
return failure.getCause();
|
|
||||||
}
|
|
||||||
return failure;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T> CompletionStage<T> failed(Throwable failure) {
|
|
||||||
CompletableFuture<T> future = new CompletableFuture<T>();
|
|
||||||
future.completeExceptionally(failure);
|
|
||||||
return future;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureHostOpen() {
|
private void ensureHostOpen() {
|
||||||
if (closed) throw new MessagingException(MessagingException.Code.CLOSED, "messaging host is closed");
|
if (closed) throw new MessagingException(MessagingException.Code.CLOSED, "messaging host is closed");
|
||||||
}
|
}
|
||||||
@@ -504,52 +460,26 @@ public final class LocalMessaging implements AutoCloseable {
|
|||||||
@Override public CompletionStage<SendReceipt> send(Endpoint target, byte[] payload) {
|
@Override public CompletionStage<SendReceipt> send(Endpoint target, byte[] payload) {
|
||||||
Objects.requireNonNull(target, "target");
|
Objects.requireNonNull(target, "target");
|
||||||
final Message message = Message.event(name, self, target, payload);
|
final Message message = Message.event(name, self, target, payload);
|
||||||
final Operation<SendReceipt> operation = new Operation<SendReceipt>(scope,
|
Operation<SendReceipt> operation = newOutboundOperation(scope,
|
||||||
new Runnable() { @Override public void run() { outboundInFlight--; } },
|
|
||||||
failure -> sendReceiptForFailure(message.id(), failure));
|
failure -> sendReceiptForFailure(message.id(), failure));
|
||||||
if (!reserveOutbound(scope, operation)) return operation.future;
|
return dispatchOutbound(scope, operation, DEFAULT_REQUEST_TIMEOUT.toMillis(),
|
||||||
if (!operation.armTimeout(DEFAULT_REQUEST_TIMEOUT.toMillis())) return operation.future;
|
() -> outbound.send(message), "outbound send stage", result -> {
|
||||||
if (operation.isDone()) return operation.future;
|
if (result == null) throw new MessagingException(
|
||||||
CompletionStage<SendResult> stage;
|
MessagingException.Code.PROTOCOL_ERROR, "outbound returned null send result");
|
||||||
try {
|
return new SendReceipt(message.id(), result);
|
||||||
stage = Objects.requireNonNull(outbound.send(message), "outbound send stage");
|
|
||||||
} catch (Throwable failure) {
|
|
||||||
operation.fail(failure);
|
|
||||||
return operation.future;
|
|
||||||
}
|
|
||||||
operation.setCancellation(stage);
|
|
||||||
stage.whenComplete((result, failure) -> {
|
|
||||||
if (failure != null) operation.fail(unwrap(failure));
|
|
||||||
else if (result == null) operation.fail(new MessagingException(
|
|
||||||
MessagingException.Code.PROTOCOL_ERROR, "outbound returned null send result"));
|
|
||||||
else operation.succeed(new SendReceipt(message.id(), result));
|
|
||||||
});
|
});
|
||||||
return operation.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public CompletionStage<PublishResult> publish(byte[] payload) {
|
@Override public CompletionStage<PublishResult> publish(byte[] payload) {
|
||||||
final Message event = Message.event(name, self, null, payload);
|
final Message event = Message.event(name, self, null, payload);
|
||||||
final Operation<PublishResult> operation = new Operation<PublishResult>(scope,
|
return dispatchOutbound(scope, newOutboundOperation(scope, null), DEFAULT_REQUEST_TIMEOUT.toMillis(),
|
||||||
new Runnable() { @Override public void run() { outboundInFlight--; } });
|
() -> outbound.publish(event), "outbound publish stage", result -> {
|
||||||
if (!reserveOutbound(scope, operation)) return operation.future;
|
if (result == null || !event.id().equals(result.messageId())) {
|
||||||
if (!operation.armTimeout(DEFAULT_REQUEST_TIMEOUT.toMillis())) return operation.future;
|
throw new MessagingException(MessagingException.Code.PROTOCOL_ERROR,
|
||||||
if (operation.isDone()) return operation.future;
|
"outbound returned invalid publish result");
|
||||||
CompletionStage<PublishResult> stage;
|
|
||||||
try {
|
|
||||||
stage = Objects.requireNonNull(outbound.publish(event), "outbound publish stage");
|
|
||||||
} catch (Throwable failure) {
|
|
||||||
operation.fail(failure);
|
|
||||||
return operation.future;
|
|
||||||
}
|
}
|
||||||
operation.setCancellation(stage);
|
return result;
|
||||||
stage.whenComplete((result, failure) -> {
|
|
||||||
if (failure != null) operation.fail(unwrap(failure));
|
|
||||||
else if (result == null || !event.id().equals(result.messageId())) operation.fail(
|
|
||||||
new MessagingException(MessagingException.Code.PROTOCOL_ERROR,
|
|
||||||
"outbound returned invalid publish result"));
|
|
||||||
else operation.succeed(result);
|
|
||||||
});
|
});
|
||||||
return operation.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public CompletionStage<Message> request(Endpoint target, byte[] payload) {
|
@Override public CompletionStage<Message> request(Endpoint target, byte[] payload) {
|
||||||
@@ -560,33 +490,52 @@ public final class LocalMessaging implements AutoCloseable {
|
|||||||
Objects.requireNonNull(target, "target");
|
Objects.requireNonNull(target, "target");
|
||||||
final long timeoutMillis = timeoutMillis(timeout);
|
final long timeoutMillis = timeoutMillis(timeout);
|
||||||
final Message request = Message.request(name, self, target, payload);
|
final Message request = Message.request(name, self, target, payload);
|
||||||
final Operation<Message> operation = new Operation<Message>(scope,
|
return dispatchOutbound(scope, newOutboundOperation(scope, null), timeoutMillis,
|
||||||
new Runnable() { @Override public void run() { outboundInFlight--; } });
|
() -> outbound.request(request, Duration.ofMillis(timeoutMillis)),
|
||||||
|
"outbound request stage", reply -> {
|
||||||
|
if (!isValidReply(request, reply)) throw new MessagingException(
|
||||||
|
MessagingException.Code.PROTOCOL_ERROR, "outbound returned an uncorrelated reply");
|
||||||
|
return reply;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> Operation<T> newOutboundOperation(Scope scope, Function<Throwable, T> failureMapper) {
|
||||||
|
return new Operation<T>(operationHost, scope.operations,
|
||||||
|
new Runnable() { @Override public void run() { outboundInFlight--; } }, failureMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Admits {@code operation}, arms its timeout, calls the transport and resolves with {@code accept}'s result. */
|
||||||
|
private <R, T> CompletionStage<T> dispatchOutbound(Scope scope, Operation<T> operation, long timeoutMillis,
|
||||||
|
Supplier<CompletionStage<R>> call, String stageName,
|
||||||
|
Function<R, T> accept) {
|
||||||
if (!reserveOutbound(scope, operation)) return operation.future;
|
if (!reserveOutbound(scope, operation)) return operation.future;
|
||||||
if (!operation.armTimeout(timeoutMillis)) return operation.future;
|
if (!operation.armTimeout(timeoutMillis)) return operation.future;
|
||||||
if (operation.isDone()) return operation.future;
|
if (operation.isDone()) return operation.future;
|
||||||
CompletionStage<Message> stage;
|
CompletionStage<R> stage;
|
||||||
try {
|
try {
|
||||||
stage = Objects.requireNonNull(outbound.request(request, Duration.ofMillis(timeoutMillis)),
|
stage = Objects.requireNonNull(call.get(), stageName);
|
||||||
"outbound request stage");
|
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
operation.fail(failure);
|
operation.fail(failure);
|
||||||
return operation.future;
|
return operation.future;
|
||||||
}
|
}
|
||||||
operation.setCancellation(stage);
|
operation.setCancellation(stage);
|
||||||
stage.whenComplete((reply, failure) -> {
|
stage.whenComplete((result, failure) -> {
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
operation.fail(unwrap(failure));
|
operation.fail(unwrap(failure));
|
||||||
} else if (!isValidReply(request, reply)) {
|
return;
|
||||||
operation.fail(new MessagingException(MessagingException.Code.PROTOCOL_ERROR,
|
|
||||||
"outbound returned an uncorrelated reply"));
|
|
||||||
} else {
|
|
||||||
operation.succeed(reply);
|
|
||||||
}
|
}
|
||||||
|
T value;
|
||||||
|
try {
|
||||||
|
value = accept.apply(result);
|
||||||
|
} catch (MessagingException invalid) {
|
||||||
|
operation.fail(invalid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
operation.succeed(value);
|
||||||
});
|
});
|
||||||
return operation.future;
|
return operation.future;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isValidReply(Message request, Message reply) {
|
private boolean isValidReply(Message request, Message reply) {
|
||||||
return reply != null && reply.kind() == MessageKind.REPLY
|
return reply != null && reply.kind() == MessageKind.REPLY
|
||||||
@@ -618,108 +567,6 @@ public final class LocalMessaging implements AutoCloseable {
|
|||||||
private Registration requestHandler;
|
private Registration requestHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class Operation<T> {
|
|
||||||
private final Scope scope;
|
|
||||||
private final Runnable release;
|
|
||||||
private final Function<Throwable, T> failureMapper;
|
|
||||||
private final OperationFuture<T> future = new OperationFuture<T>(this);
|
|
||||||
private final AtomicBoolean done = new AtomicBoolean();
|
|
||||||
private volatile ScheduledFuture<?> timer;
|
|
||||||
private volatile CompletionStage<?> underlying;
|
|
||||||
|
|
||||||
private Operation(Scope scope, Runnable release) {
|
|
||||||
this(scope, release, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Operation(Scope scope, Runnable release, Function<Throwable, T> failureMapper) {
|
|
||||||
this.scope = scope;
|
|
||||||
this.release = release;
|
|
||||||
this.failureMapper = failureMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDone() { return done.get(); }
|
|
||||||
|
|
||||||
private boolean armTimeout(long millis) {
|
|
||||||
if (done.get()) return false;
|
|
||||||
try {
|
|
||||||
ScheduledFuture<?> scheduled = scheduler.schedule(new Runnable() {
|
|
||||||
@Override public void run() {
|
|
||||||
fail(new MessagingException(MessagingException.Code.TIMED_OUT,
|
|
||||||
"message request timed out"));
|
|
||||||
}
|
|
||||||
}, millis, TimeUnit.MILLISECONDS);
|
|
||||||
timer = scheduled;
|
|
||||||
if (done.get()) {
|
|
||||||
scheduled.cancel(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (RejectedExecutionException rejected) {
|
|
||||||
fail(new MessagingException(MessagingException.Code.CLOSED,
|
|
||||||
"request timer is unavailable", rejected));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void succeed(T value) { finish(value, null); }
|
|
||||||
|
|
||||||
private void fail(Throwable failure) { finish(null, failure); }
|
|
||||||
|
|
||||||
private void failLocked(Throwable failure) {
|
|
||||||
if (!done.compareAndSet(false, true)) return;
|
|
||||||
// This operation was rejected before admission and its future has not escaped the API call.
|
|
||||||
completeFailureNow(failure);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCancellation(CompletionStage<?> stage) {
|
|
||||||
underlying = stage;
|
|
||||||
if (done.get()) cancelUnderlying();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cancel() {
|
|
||||||
if (!done.compareAndSet(false, true)) return;
|
|
||||||
ScheduledFuture<?> scheduled = timer;
|
|
||||||
if (scheduled != null) scheduled.cancel(false);
|
|
||||||
cancelUnderlying();
|
|
||||||
operationFinished(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void finish(T value, Throwable failure) {
|
|
||||||
if (!done.compareAndSet(false, true)) return;
|
|
||||||
ScheduledFuture<?> scheduled = timer;
|
|
||||||
if (scheduled != null) scheduled.cancel(false);
|
|
||||||
if (failure instanceof MessagingException) {
|
|
||||||
MessagingException.Code code = ((MessagingException) failure).code();
|
|
||||||
if (code == MessagingException.Code.TIMED_OUT || code == MessagingException.Code.CLOSED) {
|
|
||||||
cancelUnderlying();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (failure == null) {
|
|
||||||
completeOperation(this, new Runnable() { @Override public void run() { future.complete(value); } });
|
|
||||||
} else {
|
|
||||||
completeOperation(this, new Runnable() {
|
|
||||||
@Override public void run() { completeFailureNow(failure); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void completeFailureNow(Throwable failure) {
|
|
||||||
if (failureMapper == null) {
|
|
||||||
future.completeExceptionally(failure);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try { future.complete(failureMapper.apply(failure)); }
|
|
||||||
catch (Throwable mappingFailure) { future.completeExceptionally(mappingFailure); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cancelUnderlying() {
|
|
||||||
CompletionStage<?> stage = underlying;
|
|
||||||
if (stage != null) {
|
|
||||||
try { stage.toCompletableFuture().cancel(true); } catch (Throwable ignored) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class EventDelivery {
|
private final class EventDelivery {
|
||||||
private final Scope scope;
|
private final Scope scope;
|
||||||
private final AtomicBoolean done = new AtomicBoolean();
|
private final AtomicBoolean done = new AtomicBoolean();
|
||||||
@@ -736,15 +583,4 @@ public final class LocalMessaging implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class OperationFuture<T> extends CompletableFuture<T> {
|
|
||||||
private final Operation<T> operation;
|
|
||||||
|
|
||||||
private OperationFuture(Operation<T> operation) { this.operation = operation; }
|
|
||||||
|
|
||||||
@Override public boolean cancel(boolean mayInterruptIfRunning) {
|
|
||||||
boolean cancelled = super.cancel(mayInterruptIfRunning);
|
|
||||||
if (cancelled) operation.cancel();
|
|
||||||
return cancelled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package dev.moonbridge.messaging.internal;
|
||||||
|
|
||||||
|
import dev.moonbridge.messaging.MessagingException;
|
||||||
|
import dev.moonbridge.messaging.SendReceipt;
|
||||||
|
import dev.moonbridge.messaging.SendResult;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
|
|
||||||
|
/** Failure translation shared by the local messaging classes. */
|
||||||
|
final class MessagingFailures {
|
||||||
|
private MessagingFailures() { }
|
||||||
|
|
||||||
|
static long timeoutMillis(Duration timeout) {
|
||||||
|
Objects.requireNonNull(timeout, "timeout");
|
||||||
|
final long millis;
|
||||||
|
try {
|
||||||
|
millis = timeout.toMillis();
|
||||||
|
} catch (ArithmeticException overflow) {
|
||||||
|
throw new IllegalArgumentException("timeout is too large", overflow);
|
||||||
|
}
|
||||||
|
if (millis <= 0 || millis > LocalMessaging.MAX_REQUEST_TIMEOUT.toMillis()) {
|
||||||
|
throw new IllegalArgumentException("timeout must be positive and at most 60 seconds");
|
||||||
|
}
|
||||||
|
return millis;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MessagingException handlerFailure(Throwable failure) {
|
||||||
|
return new MessagingException(MessagingException.Code.HANDLER_FAILED,
|
||||||
|
"message handler failed", failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SendReceipt sendReceiptForFailure(UUID messageId, Throwable failure) {
|
||||||
|
Throwable cause = unwrap(failure);
|
||||||
|
if (cause instanceof MessagingException) {
|
||||||
|
MessagingException messagingFailure = (MessagingException) cause;
|
||||||
|
switch (messagingFailure.code()) {
|
||||||
|
case TIMED_OUT: return new SendReceipt(messageId, SendResult.TIMED_OUT);
|
||||||
|
case BACKPRESSURED: return new SendReceipt(messageId, SendResult.BACKPRESSURED);
|
||||||
|
case NOT_CONNECTED: return new SendReceipt(messageId, SendResult.NOT_CONNECTED);
|
||||||
|
case NO_HANDLER: return new SendReceipt(messageId, SendResult.NO_SUBSCRIBER);
|
||||||
|
case REJECTED: return new SendReceipt(messageId, SendResult.REJECTED);
|
||||||
|
case HANDLER_FAILED:
|
||||||
|
case PROTOCOL_ERROR: return new SendReceipt(messageId, SendResult.FAILED);
|
||||||
|
case CLOSED: throw messagingFailure;
|
||||||
|
default: return new SendReceipt(messageId, SendResult.FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new SendReceipt(messageId, SendResult.FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Throwable unwrap(Throwable failure) {
|
||||||
|
if (failure instanceof java.util.concurrent.CompletionException && failure.getCause() != null) {
|
||||||
|
return failure.getCause();
|
||||||
|
}
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
static <T> CompletionStage<T> failed(Throwable failure) {
|
||||||
|
CompletableFuture<T> future = new CompletableFuture<T>();
|
||||||
|
future.completeExceptionally(failure);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package dev.moonbridge.messaging.internal;
|
||||||
|
|
||||||
|
import dev.moonbridge.messaging.MessagingException;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/** One bounded messaging operation: a timeout, an optional upstream stage to cancel, and a single completion. */
|
||||||
|
final class Operation<T> {
|
||||||
|
private final OperationHost host;
|
||||||
|
final Set<Operation<?>> scopeOperations;
|
||||||
|
final Runnable release;
|
||||||
|
private final Function<Throwable, T> failureMapper;
|
||||||
|
final OperationFuture<T> future = new OperationFuture<T>(this);
|
||||||
|
private final AtomicBoolean done = new AtomicBoolean();
|
||||||
|
private volatile ScheduledFuture<?> timer;
|
||||||
|
private volatile CompletionStage<?> underlying;
|
||||||
|
|
||||||
|
Operation(OperationHost host, Set<Operation<?>> scopeOperations, Runnable release) {
|
||||||
|
this(host, scopeOperations, release, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Operation(OperationHost host, Set<Operation<?>> scopeOperations, Runnable release,
|
||||||
|
Function<Throwable, T> failureMapper) {
|
||||||
|
this.host = host;
|
||||||
|
this.scopeOperations = scopeOperations;
|
||||||
|
this.release = release;
|
||||||
|
this.failureMapper = failureMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isDone() { return done.get(); }
|
||||||
|
|
||||||
|
boolean armTimeout(long millis) {
|
||||||
|
if (done.get()) return false;
|
||||||
|
try {
|
||||||
|
ScheduledFuture<?> scheduled = host.scheduler().schedule(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
fail(new MessagingException(MessagingException.Code.TIMED_OUT,
|
||||||
|
"message request timed out"));
|
||||||
|
}
|
||||||
|
}, millis, TimeUnit.MILLISECONDS);
|
||||||
|
timer = scheduled;
|
||||||
|
if (done.get()) {
|
||||||
|
scheduled.cancel(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (RejectedExecutionException rejected) {
|
||||||
|
fail(new MessagingException(MessagingException.Code.CLOSED,
|
||||||
|
"request timer is unavailable", rejected));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void succeed(T value) { finish(value, null); }
|
||||||
|
|
||||||
|
void fail(Throwable failure) { finish(null, failure); }
|
||||||
|
|
||||||
|
void failLocked(Throwable failure) {
|
||||||
|
if (!done.compareAndSet(false, true)) return;
|
||||||
|
// This operation was rejected before admission and its future has not escaped the API call.
|
||||||
|
completeFailureNow(failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCancellation(CompletionStage<?> stage) {
|
||||||
|
underlying = stage;
|
||||||
|
if (done.get()) cancelUnderlying();
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
if (!done.compareAndSet(false, true)) return;
|
||||||
|
ScheduledFuture<?> scheduled = timer;
|
||||||
|
if (scheduled != null) scheduled.cancel(false);
|
||||||
|
cancelUnderlying();
|
||||||
|
host.finished(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finish(T value, Throwable failure) {
|
||||||
|
if (!done.compareAndSet(false, true)) return;
|
||||||
|
ScheduledFuture<?> scheduled = timer;
|
||||||
|
if (scheduled != null) scheduled.cancel(false);
|
||||||
|
if (failure instanceof MessagingException) {
|
||||||
|
MessagingException.Code code = ((MessagingException) failure).code();
|
||||||
|
if (code == MessagingException.Code.TIMED_OUT || code == MessagingException.Code.CLOSED) {
|
||||||
|
cancelUnderlying();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failure == null) {
|
||||||
|
host.complete(this, new Runnable() { @Override public void run() { future.complete(value); } });
|
||||||
|
} else {
|
||||||
|
host.complete(this, new Runnable() {
|
||||||
|
@Override public void run() { completeFailureNow(failure); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeFailureNow(Throwable failure) {
|
||||||
|
if (failureMapper == null) {
|
||||||
|
future.completeExceptionally(failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try { future.complete(failureMapper.apply(failure)); }
|
||||||
|
catch (Throwable mappingFailure) { future.completeExceptionally(mappingFailure); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelUnderlying() {
|
||||||
|
CompletionStage<?> stage = underlying;
|
||||||
|
if (stage != null) {
|
||||||
|
try { stage.toCompletableFuture().cancel(true); } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class OperationFuture<T> extends CompletableFuture<T> {
|
||||||
|
private final Operation<T> operation;
|
||||||
|
|
||||||
|
OperationFuture(Operation<T> operation) { this.operation = operation; }
|
||||||
|
|
||||||
|
@Override public boolean cancel(boolean mayInterruptIfRunning) {
|
||||||
|
boolean cancelled = super.cancel(mayInterruptIfRunning);
|
||||||
|
if (cancelled) operation.cancel();
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.moonbridge.messaging.internal;
|
||||||
|
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
|
||||||
|
/** What an {@link Operation} needs from the messaging host that admitted it. */
|
||||||
|
interface OperationHost {
|
||||||
|
ScheduledExecutorService scheduler();
|
||||||
|
|
||||||
|
/** Runs {@code completion} on the completion executor, releasing the admission slot first. */
|
||||||
|
void complete(Operation<?> operation, Runnable completion);
|
||||||
|
|
||||||
|
/** Releases the admission slot of an operation that was cancelled before completing. */
|
||||||
|
void finished(Operation<?> operation);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user