Skip to content

Commit 0f412e7

Browse files
committed
Connector must invoke callback on registration error
When registering a command or event handler, the returned registration should invoke any registered acknowledgement callbacks, whether that registration was successful or not. An additional callback method is added to allow distinguishing between these situations. This issue is the root cause of AxonIQ/AxonFramework#3938
1 parent 19d544e commit 0f412e7

4 files changed

Lines changed: 88 additions & 7 deletions

File tree

src/main/java/io/axoniq/axonserver/connector/Registration.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,21 @@
1616

1717
package io.axoniq.axonserver.connector;
1818

19+
import java.util.Optional;
1920
import java.util.concurrent.CompletableFuture;
2021
import java.util.concurrent.TimeUnit;
2122
import java.util.concurrent.TimeoutException;
23+
import java.util.function.Consumer;
2224

2325
/**
24-
* Interface describing an instruction to perform a registration, which can be cancelled.
26+
* Interface describing an instruction to perform a registration, which can be canceled.
2527
*/
2628
@FunctionalInterface
2729
public interface Registration {
2830

2931
/**
3032
* Cancel the registration from which this instance was returned. Does nothing if the registration has already been
31-
* cancelled, or when the registration was undone by another mechanism (such as a new registration overriding this
33+
* canceled, or when the registration was undone by another mechanism (such as a new registration overriding this
3234
* one).
3335
*
3436
* @return a {@link CompletableFuture} of {@link Void} to react when {@code this} {@link Registration} has been
@@ -44,7 +46,7 @@ public interface Registration {
4446
* @param timeout the duration to wait until the operation has been acknowledged
4547
* @param unit the {@link TimeUnit} for the given {@code timeout} to wait until the operation has been
4648
* acknowledged
47-
* @return {@code this} {@link Registration} to support a fluent API
49+
* @return this Registration to support a fluent API
4850
* @throws TimeoutException is thrown when the given {@code timeout} and {@code unit} is surpassed
4951
* @throws InterruptedException is thrown when the thread waiting for the acknowledgement is interrupted
5052
*/
@@ -56,13 +58,31 @@ default Registration awaitAck(long timeout, TimeUnit unit) throws TimeoutExcepti
5658
* Registers the given {@code runnable} to {@code this} {@link Registration} to be executed when the acknowledgement
5759
* of {@code this} {@link Registration} is received. Allows for the addition of further logic to {@code this
5860
* Registration}, like invoking {@link #awaitAck(long, TimeUnit)} for example.
61+
* <p/>
62+
* The given {@code runnable} is invoked, regardless of successful or failed acknowledgement. Use
63+
* {@link #onAck(Consumer)} to register an action that needs to distinguish between successful and failed
64+
* registration.
5965
*
6066
* @param runnable the {@link Runnable} to execute when the acknowledgement of {@code this} {@link Registration} is
6167
* received
62-
* @return {@code this} {@link Registration} to support a fluent API
68+
* @return this Registration to support a fluent API
6369
*/
6470
default Registration onAck(Runnable runnable) {
6571
runnable.run();
6672
return this;
6773
}
74+
75+
/**
76+
* Registers the given {@code runnable} to {@code this} {@link Registration} to be executed when the acknowledgement
77+
* of {@code this} {@link Registration} is received. Allows for the addition of further logic to
78+
* {@code this Registration}, like invoking {@link #awaitAck(long, TimeUnit)} for example.
79+
*
80+
* @param action the action to execute when the acknowledgement of {@code this} {@link Registration} is received,
81+
* either normally or exceptionally.
82+
* @return this Registration to support a fluent API
83+
*/
84+
default Registration onAck(Consumer<Optional<Throwable>> action) {
85+
action.accept(Optional.empty());
86+
return this;
87+
}
6888
}

src/main/java/io/axoniq/axonserver/connector/impl/AsyncRegistration.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
import io.axoniq.axonserver.connector.ErrorCategory;
2121
import io.axoniq.axonserver.connector.Registration;
2222

23+
import java.util.Optional;
2324
import java.util.concurrent.CompletableFuture;
2425
import java.util.concurrent.ExecutionException;
2526
import java.util.concurrent.TimeUnit;
2627
import java.util.concurrent.TimeoutException;
28+
import java.util.function.Consumer;
2729
import java.util.function.Supplier;
2830

2931
/**
@@ -69,7 +71,15 @@ public Registration awaitAck(long timeout, TimeUnit unit) throws TimeoutExceptio
6971

7072
@Override
7173
public Registration onAck(Runnable runnable) {
72-
requestAck.thenRun(runnable);
74+
requestAck.exceptionally(e -> null).thenRun(runnable);
75+
return this;
76+
}
77+
78+
@Override
79+
public Registration onAck(Consumer<Optional<Throwable>> action) {
80+
requestAck.thenApply(ignored -> Optional.<Throwable>empty())
81+
.exceptionally(Optional::of)
82+
.thenAccept(action);
7383
return this;
7484
}
7585
}

src/test/java/io/axoniq/axonserver/connector/command/CommandChannelIntegrationTest.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package io.axoniq.axonserver.connector.command;
1818

19+
import eu.rekawek.toxiproxy.model.Toxic;
1920
import io.axoniq.axonserver.connector.AbstractAxonServerIntegrationTest;
2021
import io.axoniq.axonserver.connector.AxonServerConnection;
2122
import io.axoniq.axonserver.connector.AxonServerConnectionFactory;
@@ -33,17 +34,21 @@
3334
import java.io.IOException;
3435
import java.util.ArrayList;
3536
import java.util.List;
37+
import java.util.Optional;
3638
import java.util.Queue;
3739
import java.util.concurrent.CompletableFuture;
3840
import java.util.concurrent.ConcurrentLinkedQueue;
3941
import java.util.concurrent.ExecutionException;
4042
import java.util.concurrent.TimeUnit;
4143
import java.util.concurrent.TimeoutException;
44+
import java.util.concurrent.atomic.AtomicBoolean;
45+
import java.util.concurrent.atomic.AtomicReference;
4246

4347
import static io.axoniq.axonserver.connector.impl.ObjectUtils.doIfNotNull;
4448
import static io.axoniq.axonserver.connector.impl.ObjectUtils.silently;
4549
import static io.axoniq.axonserver.connector.testutils.AssertUtils.assertWithin;
4650
import static org.junit.jupiter.api.Assertions.*;
51+
import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
4752

4853
class CommandChannelIntegrationTest extends AbstractAxonServerIntegrationTest {
4954

@@ -59,13 +64,15 @@ void setUp() {
5964
"client1")
6065
.routingServers(axonServerAddress)
6166
.forceReconnectViaRoutingServers(false)
67+
.connectTimeout(1500, TimeUnit.MILLISECONDS)
6268
.reconnectInterval(500, TimeUnit.MILLISECONDS)
6369
.build();
6470
connection1 = connectionFactory1.connect("default");
6571

6672
connectionFactory2 = AxonServerConnectionFactory.forClient(getClass().getSimpleName(),
6773
"client2")
6874
.routingServers(axonServerAddress)
75+
.connectTimeout(1500, TimeUnit.MILLISECONDS)
6976
.reconnectInterval(500, TimeUnit.MILLISECONDS)
7077
.forceReconnectViaRoutingServers(false)
7178
.build();
@@ -74,9 +81,13 @@ void setUp() {
7481
}
7582

7683
@AfterEach
77-
void tearDown() {
84+
void tearDown() throws IOException {
7885
silently(connectionFactory1, AxonServerConnectionFactory::shutdown);
7986
silently(connectionFactory2, AxonServerConnectionFactory::shutdown);
87+
axonServerProxy.enable();
88+
for (Toxic toxic : axonServerProxy.toxics().getAll()) {
89+
toxic.remove();
90+
}
8091
}
8192

8293
@Test
@@ -293,6 +304,22 @@ void unsubscribingHandlerReturnsUnknownHandlerForCommand() throws TimeoutExcepti
293304

294305
}
295306

307+
@Test
308+
void subscribingHandlersWithAxonServerUnavailableAcknowledgesSubscription() throws IOException {
309+
axonServerProxy.disable();
310+
311+
AtomicBoolean acked = new AtomicBoolean(false);
312+
AtomicReference<Optional<Throwable>> ackError = new AtomicReference<>();
313+
Registration registration = connection1.commandChannel().registerCommandHandler(this::mockHandler,
314+
100,
315+
"testCommand");
316+
317+
registration.onAck(() -> acked.set(true))
318+
.onAck(ackError::set);
319+
await().until(ackError::get, v -> v != null && v.isPresent());
320+
await().untilTrue(acked);
321+
}
322+
296323
private CompletableFuture<CommandResponse> mockHandler(Command command) {
297324
return CompletableFuture.completedFuture(CommandResponse.getDefaultInstance());
298325
}

src/test/java/io/axoniq/axonserver/connector/query/QueryChannelIntegrationTest.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.axoniq.axonserver.connector.query;
1818

1919
import com.google.protobuf.ByteString;
20+
import eu.rekawek.toxiproxy.model.Toxic;
2021
import io.axoniq.axonserver.connector.AbstractAxonServerIntegrationTest;
2122
import io.axoniq.axonserver.connector.AxonServerConnection;
2223
import io.axoniq.axonserver.connector.AxonServerConnectionFactory;
@@ -39,19 +40,22 @@
3940
import java.io.IOException;
4041
import java.util.ArrayList;
4142
import java.util.List;
43+
import java.util.Optional;
4244
import java.util.Queue;
4345
import java.util.UUID;
4446
import java.util.concurrent.CompletableFuture;
4547
import java.util.concurrent.ConcurrentLinkedQueue;
4648
import java.util.concurrent.ExecutionException;
4749
import java.util.concurrent.TimeUnit;
4850
import java.util.concurrent.TimeoutException;
51+
import java.util.concurrent.atomic.AtomicBoolean;
4952
import java.util.concurrent.atomic.AtomicReference;
5053
import java.util.stream.Collectors;
5154

5255
import static io.axoniq.axonserver.connector.impl.ObjectUtils.doIfNotNull;
5356
import static io.axoniq.axonserver.connector.testutils.AssertUtils.*;
5457
import static org.junit.jupiter.api.Assertions.*;
58+
import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
5559

5660
class QueryChannelIntegrationTest extends AbstractAxonServerIntegrationTest {
5761

@@ -83,9 +87,13 @@ void setUp() {
8387
}
8488

8589
@AfterEach
86-
void tearDown() {
90+
void tearDown() throws Exception {
8791
connectionFactory1.shutdown();
8892
connectionFactory2.shutdown();
93+
axonServerProxy.enable();
94+
for (Toxic toxic : axonServerProxy.toxics().getAll()) {
95+
toxic.remove();
96+
}
8997
}
9098

9199
@Test
@@ -481,6 +489,22 @@ void testSubscriptionQueryReturnsNoUpdatesOnUnsupportedSubscription() throws Int
481489
assertTrue(result.updates().isClosed());
482490
}
483491

492+
@Test
493+
void subscribingHandlersWithAxonServerUnavailableAcknowledgesSubscription() throws IOException {
494+
axonServerProxy.disable();
495+
496+
AtomicBoolean acked = new AtomicBoolean(false);
497+
AtomicReference<Optional<Throwable>> ackError = new AtomicReference<>();
498+
Registration registration = connection1.queryChannel().registerQueryHandler(this::mockHandler,
499+
new QueryDefinition("testQuery",
500+
String.class));
501+
502+
registration.onAck(() -> acked.set(true))
503+
.onAck(ackError::set);
504+
await().until(ackError::get, v -> v != null && v.isPresent());
505+
await().untilTrue(acked);
506+
}
507+
484508
private void mockHandler(QueryRequest query, ReplyChannel<QueryResponse> responseHandler) {
485509
responseHandler.sendLast(QueryResponse.newBuilder().setRequestIdentifier(query.getMessageIdentifier()).setPayload(query.getPayload()).build());
486510
}

0 commit comments

Comments
 (0)