Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</parent>
<groupId>com.guicedee</groupId>
<artifactId>client</artifactId>
<version>2.2.3-SNAPSHOT</version>
<name>GuicedEE Client</name>
<url>https://guicedee.com</url>
<licenses>
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/guicedee/client/IGuiceContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ static IGuiceContext instance() {
*/
Injector inject();

/** Non-starting lookup for infrastructure that must not bootstrap the application. */
default java.util.Optional<Injector> existingInjector() {
return java.util.Optional.empty();
}


/**
* Returns the Guice configuration backing this context.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,8 @@ public <T> Uni<T> onUniCreation(Uni<T> uni) {
}
INTERCEPTING.set(true);
try {
CallScoper callScoper;
try {
callScoper = IGuiceContext.get(CallScoper.class);
} catch (RuntimeException noContextYet) {
// Mutiny invokes interceptors very early (e.g. during
// UniCreate.<clinit>) and on any thread. When no Guice
// context is registered yet — during bootstrap, or in
// environments without com.guicedee:inject (such as unit
// tests) — skip call-scope propagation instead of failing.
return uni;
}
CallScoper callScoper = existingScoper();
if (callScoper == null) return uni;
if (callScoper.isStartedScope()) {
recordTouch(callScoper, "uni-creation", captureLocation());
}
Expand All @@ -66,6 +57,13 @@ public <T> Uni<T> onUniCreation(Uni<T> uni) {
}
}

private static CallScoper existingScoper() {
var context = IGuiceContext.contexts.get("default");
if (context == null) return null;
// An existing injector may resolve its scope; Uni creation must never invoke inject().
return context.existingInjector().map(injector -> injector.getInstance(CallScoper.class)).orElse(null);
}

private static final ThreadLocal<Boolean> INTERCEPTING = ThreadLocal.withInitial(() -> false);

/**
Expand Down Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* <p>
* 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
Expand All @@ -36,4 +36,9 @@ public interface IGuicePreDestroy<J extends IGuicePreDestroy<J>> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String,IGuiceContext> 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> 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<Uni<String>>();
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<String>();
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<Boolean>();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);}
}

}
Loading