INTERCEPTING = ThreadLocal.withInitial(() -> false);
/**
@@ -117,7 +115,11 @@ public void subscribe(UniSubscriber super T> subscriber) {
return;
}
- CallScoper callScoper = IGuiceContext.get(CallScoper.class);
+ CallScoper callScoper = existingScoper();
+ if (callScoper == null) {
+ AbstractUni.subscribe(upstream, subscriber);
+ return;
+ }
boolean startedHere = false;
if (!callScoper.isStartedScope()) {
diff --git a/src/main/java/com/guicedee/client/services/lifecycle/IGuicePreDestroy.java b/src/main/java/com/guicedee/client/services/lifecycle/IGuicePreDestroy.java
index b94caf6..272f3f8 100644
--- a/src/main/java/com/guicedee/client/services/lifecycle/IGuicePreDestroy.java
+++ b/src/main/java/com/guicedee/client/services/lifecycle/IGuicePreDestroy.java
@@ -23,7 +23,7 @@
*
* Purpose: release resources and stop background work before the injector is torn down.
* Trigger: invoked during {@link com.guicedee.client.IGuiceContext#destroy()}.
- * Order: ascending {@link #sortOrder()}, default 100.
+ * Order: ascending {@link #shutdownSortOrder()}, defaulting to sortOrder().
* Idempotency: implementations should be safe to invoke once and tolerate repeated calls.
*
* @author GedMarc
@@ -36,4 +36,9 @@ public interface IGuicePreDestroy> extends IDefaul
* Executes the pre-destroy logic.
*/
void onDestroy();
+
+ /** Allows infrastructure to start early and stop late without changing existing hooks. */
+ default Integer shutdownSortOrder() {
+ return sortOrder();
+ }
}
diff --git a/src/test/java/com/guicedee/client/test/UniBootstrapIsolationTest.java b/src/test/java/com/guicedee/client/test/UniBootstrapIsolationTest.java
new file mode 100644
index 0000000..8c4b065
--- /dev/null
+++ b/src/test/java/com/guicedee/client/test/UniBootstrapIsolationTest.java
@@ -0,0 +1,77 @@
+package com.guicedee.client.test;
+
+import com.google.inject.*;
+import com.guicedee.client.IGuiceContext;
+import com.guicedee.client.scopes.CallScoper;
+import com.guicedee.client.scopes.mutiny.CallScopeUniInterceptor;
+import io.smallrye.mutiny.Uni;
+import org.junit.jupiter.api.*;
+import java.lang.reflect.Proxy;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import static org.junit.jupiter.api.Assertions.*;
+
+class UniBootstrapIsolationTest {
+ Map previous;
+ @BeforeEach void save() {previous=new HashMap<>(IGuiceContext.contexts);IGuiceContext.contexts.clear();}
+ @AfterEach void restore() {IGuiceContext.contexts.clear();IGuiceContext.contexts.putAll(previous);}
+ IGuiceContext context(Optional injector,AtomicInteger boots) {
+ return (IGuiceContext)Proxy.newProxyInstance(IGuiceContext.class.getClassLoader(),new Class[]{IGuiceContext.class},(proxy,method,args) -> {
+ if(method.getName().equals("existingInjector"))return injector;
+ if(method.getName().equals("getConfig"))return Proxy.newProxyInstance(com.guicedee.client.services.IGuiceConfig.class.getClassLoader(),
+ new Class[]{com.guicedee.client.services.IGuiceConfig.class},(p,m,a) -> {
+ if(m.getName().equals("isServiceLoadWithClassPath"))return false;
+ throw new AssertionError("Unexpected configuration operation: "+m.getName());
+ });
+ if(method.getName().equals("inject")) {boots.incrementAndGet();throw new IllegalStateException("fixture bootstrap must not run");}
+ throw new AssertionError("Unexpected context operation: "+method.getName());
+ });
+ }
+ @Test void creatingUniWithoutContextDoesNotDiscoverOrBootstrapProviders() {
+ assertEquals("value",Uni.createFrom().item("value").await().indefinitely());
+ assertTrue(IGuiceContext.contexts.isEmpty());
+ }
+ @Test void registeredButUninitializedContextIsNeverBootstrappedByInterceptor() {
+ var boots=new AtomicInteger();IGuiceContext.contexts.put("default",context(Optional.empty(),boots));
+ var source=Uni.createFrom().item("value");
+ assertSame(source,new CallScopeUniInterceptor().onUniCreation(source));
+ assertEquals("value",source.await().indefinitely());assertEquals(0,boots.get());
+ }
+ @Test void failureFromExistingScopeProviderIsNotSwallowedAsMissingBootstrap() {
+ var source=Uni.createFrom().item("value");var boots=new AtomicInteger();
+ var injector=Guice.createInjector(new AbstractModule(){protected void configure(){
+ bind(CallScoper.class).toProvider(() -> {throw new IllegalStateException("fixture scope failure");});
+ }});
+ IGuiceContext.contexts.put("default",context(Optional.of(injector),boots));
+ assertThrows(ProvisionException.class,() -> new CallScopeUniInterceptor().onUniCreation(source));assertEquals(0,boots.get());
+ }
+ @Test void initializedScopeIsResolvedWithoutInvokingBootstrap() {
+ var boots=new AtomicInteger();var resolutions=new AtomicInteger();var scoper=new CallScoper();
+ var injector=Guice.createInjector(new AbstractModule(){protected void configure(){bind(CallScoper.class).toProvider(() -> {resolutions.incrementAndGet();return scoper;});}});
+ IGuiceContext.contexts.put("default",context(Optional.of(injector),boots));
+ assertEquals("ready",Uni.createFrom().item("ready").await().indefinitely());
+ assertTrue(resolutions.get()>0);assertEquals(0,boots.get());
+ }
+ @Test void activeScopeSnapshotCrossesContextsAndIsRemovedAfterCompletion() throws Exception {
+ var scoper=new CallScoper();var boots=new AtomicInteger();
+ var injector=Guice.createInjector(new AbstractModule(){protected void configure(){bind(CallScoper.class).toInstance(scoper);}});
+ IGuiceContext.contexts.put("default",context(Optional.of(injector),boots));
+ var vertx=io.vertx.core.Vertx.vertx();
+ try {
+ var captured=new java.util.concurrent.CompletableFuture>();
+ vertx.getOrCreateContext().runOnContext(ignored -> {
+ try {
+ scoper.enterQuietly();scoper.seed(String.class,"original-actor");
+ var uni=Uni.createFrom().item("read").map(value -> (String)scoper.getValues().get(Key.get(String.class)));
+ scoper.exitQuietly();captured.complete(uni);
+ }catch(Throwable failed){captured.completeExceptionally(failed);}
+ });
+ var uni=captured.get(5,java.util.concurrent.TimeUnit.SECONDS);var result=new java.util.concurrent.CompletableFuture();
+ var target=vertx.getOrCreateContext();target.runOnContext(ignored -> uni.subscribe().with(result::complete,result::completeExceptionally));
+ assertEquals("original-actor",result.get(5,java.util.concurrent.TimeUnit.SECONDS));
+ var empty=new java.util.concurrent.CompletableFuture();target.runOnContext(ignored -> empty.complete(!scoper.isStartedScope()));
+ assertTrue(empty.get(5,java.util.concurrent.TimeUnit.SECONDS));assertEquals(0,boots.get());
+ } finally {vertx.close().toCompletionStage().toCompletableFuture().get(5,java.util.concurrent.TimeUnit.SECONDS);}
+ }
+
+}