package org.infinispan.interceptors.impl;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.infinispan.test.Exceptions.expectExecutionException;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import org.infinispan.commands.VisitableCommand;
import org.infinispan.commands.control.LockControlCommand;
import org.infinispan.commands.read.GetKeyValueCommand;
import org.infinispan.context.InvocationContext;
import org.infinispan.context.SingleKeyNonTxInvocationContext;
import org.infinispan.factories.components.ComponentMetadataRepo;
import org.infinispan.interceptors.AsyncInterceptor;
import org.infinispan.interceptors.AsyncInterceptorChain;
import org.infinispan.interceptors.BaseAsyncInterceptor;
import org.infinispan.interceptors.InterceptorChainTest;
import org.infinispan.interceptors.InvocationSuccessFunction;
import org.infinispan.test.AbstractInfinispanTest;
import org.infinispan.test.TestException;
import org.infinispan.util.concurrent.CompletableFutures;
import org.testng.annotations.Test;
/**
* @author Dan Berindei
* @since 9.0
*/
@Test(groups = "unit", testName = "interceptors.AsyncInterceptorChainInvocationTest")
public class AsyncInterceptorChainInvocationTest extends AbstractInfinispanTest {
private VisitableCommand testCommand = new GetKeyValueCommand("k", 0);
private VisitableCommand testSubCommand = new LockControlCommand("k", null, 0, null);
private final AtomicReference<String> sideEffects = new AtomicReference<>("");
public void testCompletedStage() {
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return "v1";
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return "v2";
}
});
InvocationContext context = newInvocationContext();
Object returnValue = chain.invoke(context, testCommand);
assertEquals("v1", returnValue);
}
public void testAsyncStage() throws Exception {
CompletableFuture<Object> f = new CompletableFuture<>();
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return asyncValue(f);
}
});
InvocationContext context = newInvocationContext();
CompletableFuture<Object> invokeFuture = chain.invokeAsync(context, testCommand);
assertFalse(invokeFuture.isDone());
f.complete("v1");
assertEquals("v1", invokeFuture.get(10, SECONDS));
}
public void testComposeSync() {
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextAndHandle(ctx, command, (rCtx, rCommand, rv, t) -> "v1");
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return "v2";
}
});
InvocationContext context = newInvocationContext();
Object returnValue = chain.invoke(context, testCommand);
assertEquals("v1", returnValue);
}
public void testComposeAsync() throws Exception {
CompletableFuture<Object> f = new CompletableFuture<>();
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextAndHandle(ctx, command, (rCtx, rCommand, rv, t) -> asyncValue(f));
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return "v1";
}
});
InvocationContext context = newInvocationContext();
CompletableFuture<Object> invokeFuture = chain.invokeAsync(context, testCommand);
assertFalse(invokeFuture.isDone());
f.complete("v2");
assertEquals("v2", invokeFuture.get(10, SECONDS));
}
public void testInvokeNextAsync() throws Exception {
CompletableFuture<Object> f = new CompletableFuture<>();
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return asyncInvokeNext(ctx, command, f);
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return "v1";
}
});
InvocationContext context = newInvocationContext();
CompletableFuture<Object> invokeFuture = chain.invokeAsync(context, testCommand);
assertFalse(invokeFuture.isDone());
f.complete("v");
assertEquals("v1", invokeFuture.get(10, SECONDS));
}
public void testInvokeNextSubCommand() {
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNext(ctx, testSubCommand);
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return command instanceof LockControlCommand ? "subCommand" : "command";
}
});
InvocationContext context = newInvocationContext();
Object returnValue = chain.invoke(context, testCommand);
assertEquals("subCommand", returnValue);
}
public void testInvokeNextAsyncSubCommand() throws Exception {
CompletableFuture<Object> f = new CompletableFuture<>();
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return asyncInvokeNext(ctx, testSubCommand, f);
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return command instanceof LockControlCommand ? "subCommand" : "command";
}
});
InvocationContext context = newInvocationContext();
CompletableFuture<Object> invokeFuture = chain.invokeAsync(context, testCommand);
assertFalse(invokeFuture.isDone());
f.complete("v");
assertEquals("subCommand", invokeFuture.get(10, SECONDS));
}
public void testAsyncStageCompose() throws Exception {
CompletableFuture<Object> f = new CompletableFuture<>();
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextAndHandle(ctx, command, (rCtx, rCommand, rv, t) -> "v1");
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return asyncValue(f);
}
});
InvocationContext context = newInvocationContext();
CompletableFuture<Object> invokeFuture = chain.invokeAsync(context, testCommand);
assertFalse(invokeFuture.isDone());
f.complete("v2");
assertEquals("v1", invokeFuture.get(10, SECONDS));
}
public void testAsyncStageComposeAsyncStage() throws Exception {
CompletableFuture<Object> f1 = new CompletableFuture<>();
CompletableFuture<Object> f2 = new CompletableFuture<>();
CompletableFuture<Object> f3 = new CompletableFuture<>();
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextAndHandle(ctx, command, (rCtx, rCommand, rv, t) -> {
InvocationSuccessFunction function = (rCtx1, rCommand1, rv1) -> asyncValue(f3);
return asyncValue(f2).addCallback(rCtx, rCommand, function);
});
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return asyncValue(f1);
}
});
InvocationContext context = newInvocationContext();
CompletableFuture<Object> invokeFuture = chain.invokeAsync(context, testCommand);
assertFalse(invokeFuture.isDone());
f1.complete("v1");
assertFalse(invokeFuture.isDone());
f2.complete("v2");
assertFalse(invokeFuture.isDone());
f3.complete("v3");
assertEquals("v3", invokeFuture.get(10, SECONDS));
}
public void testAsyncInvocationManyHandlers() throws Exception {
sideEffects.set("");
CompletableFuture<Object> f = new CompletableFuture<>();
AsyncInterceptorChain chain = makeChainWithManyHandlers(f);
CompletableFuture<Object> invokeFuture = chain.invokeAsync(newInvocationContext(), testCommand);
f.complete("");
assertHandlers(invokeFuture);
}
public void testSyncInvocationManyHandlers() throws Exception {
sideEffects.set("");
CompletableFuture<Object> f = CompletableFuture.completedFuture("");
AsyncInterceptorChain chain = makeChainWithManyHandlers(f);
CompletableFuture<Object> invokeFuture = chain.invokeAsync(newInvocationContext(), testCommand);
assertHandlers(invokeFuture);
}
private void assertHandlers(CompletableFuture<Object> invokeFuture)
throws InterruptedException, ExecutionException {
assertEquals("|handle|thenApply", invokeFuture.get());
assertEquals("|whenComplete|handle|thenAccept|thenApply", sideEffects.get());
}
public void testAsyncInvocationManyHandlersSyncException() throws Exception {
sideEffects.set("");
CompletableFuture<Object> f = CompletableFutures.completedExceptionFuture(new TestException(""));
AsyncInterceptorChain chain = makeChainWithManyHandlers(f);
CompletableFuture<Object> invokeFuture = chain.invokeAsync(newInvocationContext(), testCommand);
assertExceptionHandlers(invokeFuture);
}
public void testAsyncInvocationManyHandlersAsyncException() throws Exception {
sideEffects.set("");
CompletableFuture<Object> f = new CompletableFuture<>();
AsyncInterceptorChain chain = makeChainWithManyHandlers(f);
CompletableFuture<Object> invokeFuture = chain.invokeAsync(newInvocationContext(), testCommand);
f.completeExceptionally(new TestException(""));
assertExceptionHandlers(invokeFuture);
}
private void assertExceptionHandlers(CompletableFuture<Object> invokeFuture) {
String expectedMessage = "|whenComplete|handle|exceptionally";
expectExecutionException(TestException.class, Pattern.quote(expectedMessage), invokeFuture);
assertEquals("|whenComplete|handle|exceptionally", sideEffects.get());
}
private AsyncInterceptorChain makeChainWithManyHandlers(CompletableFuture<Object> f) {
return newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) ->
afterInvokeNext(ctx, rCtx, command, rCommand, rv, null, "|thenApply"));
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) ->
afterInvokeNext(ctx, rCtx, command, rCommand, rv, null, "|thenAccept"));
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextAndExceptionally(ctx, command, (rCtx, rCommand, t) ->
afterInvokeNext(ctx, rCtx, command, rCommand, null, t, "|exceptionally"));
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextAndHandle(ctx, command, (rCtx, rCommand, rv, t) ->
afterInvokeNext(ctx, rCtx, command, rCommand, rv, t, "|handle"));
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextAndFinally(ctx, command, (rCtx, rCommand, rv, t) ->
afterInvokeNext(ctx, rCtx, command, rCommand, rv, t, "|whenComplete"));
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return asyncValue(f);
}
});
}
private String afterInvokeNext(Object rv, Throwable t, String text) {
sideEffects.set(sideEffects.get() + text);
if (t == null) {
return rv.toString() + text;
} else {
throw new TestException(t.getMessage() + text);
}
}
private String afterInvokeNext(VisitableCommand expectedCommand, VisitableCommand command, Object rv, Throwable t,
String text) {
assertEquals(expectedCommand, command);
return afterInvokeNext(rv, t, text);
}
private String afterInvokeNext(InvocationContext expectedCtx, InvocationContext ctx,
VisitableCommand expectedCommand, VisitableCommand command, Object rv, Throwable t,
String text) {
assertEquals(expectedCtx, ctx);
return afterInvokeNext(expectedCommand, command, rv, t, text);
}
public void testDeadlockWithAsyncStage() throws Exception {
CompletableFuture<Object> f1 = new CompletableFuture<>();
CompletableFuture<Object> f2 = new CompletableFuture<>();
AsyncInterceptorChain chain = newInterceptorChain(new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> rv + " " + awaitFuture(f2));
}
}, new BaseAsyncInterceptor() {
@Override
public Object visitCommand(InvocationContext ctx, VisitableCommand command) throws Throwable {
// Add a handler to force the return value to be a full AsyncInvocationStage
InvocationSuccessFunction function = (rCtx, rCommand, rv) -> rv;
return asyncValue(f1).addCallback(ctx, command, function);
}
});
InvocationContext context = newInvocationContext();
CompletableFuture<Object> invokeFuture = chain.invokeAsync(context, testCommand);
assertFalse(invokeFuture.isDone());
Future<Boolean> fork = fork(() -> f1.complete("v1"));
Thread.sleep(100);
assertFalse(fork.isDone());
assertFalse(invokeFuture.isDone());
f2.complete("v2");
fork.get(10, SECONDS);
assertEquals("v1 v2", invokeFuture.getNow(null));
}
private Object awaitFuture(CompletableFuture<Object> f2) {
try {
return f2.get(10, SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw CompletableFutures.asCompletionException(e);
}
}
private SingleKeyNonTxInvocationContext newInvocationContext() {
// Actual implementation doesn't matter, we are only testing the BaseAsyncInvocationContext methods
return new SingleKeyNonTxInvocationContext(null);
}
private AsyncInterceptorChain newInterceptorChain(AsyncInterceptor... interceptors) {
ComponentMetadataRepo componentMetadataRepo = new ComponentMetadataRepo();
componentMetadataRepo.initialize(Collections.emptyList(), InterceptorChainTest.class.getClassLoader());
AsyncInterceptorChain chain = new AsyncInterceptorChainImpl(componentMetadataRepo);
for (AsyncInterceptor i : interceptors) {
chain.appendInterceptor(i, false);
}
return chain;
}
}