Skip to content

Commit

Permalink
ArC - introduce the CurrentContextFactory abstraction
Browse files Browse the repository at this point in the history
- this API allows quarkus to supply an optimized backend for a context of a normal scope
- currently it's only used by the request context
- an extension can provide a custom factory via CurrentContextFactoryBuildItem
  • Loading branch information
mkouba committed Apr 11, 2022
1 parent c8730c6 commit db7c71d
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,8 @@ public BeanContainerBuildItem generateResources(ArcConfig config, ArcRecorder re
LiveReloadBuildItem liveReloadBuildItem,
BuildProducer<GeneratedResourceBuildItem> generatedResource,
BuildProducer<BytecodeTransformerBuildItem> bytecodeTransformer,
List<ReflectiveBeanClassBuildItem> reflectiveBeanClasses) throws Exception {
List<ReflectiveBeanClassBuildItem> reflectiveBeanClasses,
Optional<CurrentContextFactoryBuildItem> currentContextFactory) throws Exception {

for (ValidationErrorBuildItem validationError : validationErrors) {
for (Throwable error : validationError.getValues()) {
Expand Down Expand Up @@ -593,7 +594,8 @@ public void registerSubclass(DotName beanClassName, String subclassName) {
reflectiveClasses.produce(new ReflectiveClassBuildItem(true, false, binding.name().toString()));
}

ArcContainer container = recorder.getContainer(shutdown);
ArcContainer container = recorder.initContainer(shutdown,
currentContextFactory.isPresent() ? currentContextFactory.get().getFactory() : null);
BeanContainer beanContainer = recorder.initBeanContainer(container,
beanContainerListenerBuildItems.stream().map(BeanContainerListenerBuildItem::getBeanContainerListener)
.collect(Collectors.toList()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkus.arc.deployment;

import io.quarkus.arc.CurrentContextFactory;
import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.runtime.RuntimeValue;

/**
* An extension can provide a custom {@link CurrentContextFactory}.
*/
public final class CurrentContextFactoryBuildItem extends SimpleBuildItem {

private final RuntimeValue<CurrentContextFactory> factory;

public CurrentContextFactoryBuildItem(RuntimeValue<CurrentContextFactory> factory) {
this.factory = factory;
}

public RuntimeValue<CurrentContextFactory> getFactory() {
return factory;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.CurrentContextFactory;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableBean.Kind;
import io.quarkus.arc.impl.ArcContainerImpl;
Expand All @@ -36,8 +37,9 @@ public class ArcRecorder {
*/
public static volatile Map<String, Supplier<?>> supplierMap;

public ArcContainer getContainer(ShutdownContext shutdown) throws Exception {
ArcContainer container = Arc.initialize();
public ArcContainer initContainer(ShutdownContext shutdown, RuntimeValue<CurrentContextFactory> currentContextFactory)
throws Exception {
ArcContainer container = Arc.initialize(currentContextFactory != null ? currentContextFactory.getValue() : null);
shutdown.addShutdownTask(new Runnable() {
@Override
public void run() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ public final class Arc {

private static final AtomicReference<ArcContainerImpl> INSTANCE = new AtomicReference<>();

public static ArcContainer initialize() {
return initialize(null);
}

/**
*
* @param currentContextFactory
* @return the initialized container
*/
public static ArcContainer initialize() {
public static ArcContainer initialize(CurrentContextFactory currentContextFactory) {
ArcContainerImpl container = INSTANCE.get();
if (container == null) {
synchronized (INSTANCE) {
container = INSTANCE.get();
if (container == null) {
container = new ArcContainerImpl();
// Set the container instance first because Arc.container() can be used within ArcContainerImpl.init()
container = new ArcContainerImpl(currentContextFactory);
INSTANCE.set(container);
container.init();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,11 @@ public interface ArcContainer {
* @return the default executor service
*/
ExecutorService getExecutorService();

/**
*
* @return the factory
* @see CurrentContext
*/
CurrentContextFactory getCurrentContextFactory();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.arc;

import io.quarkus.arc.InjectableContext.ContextState;

/**
* Represents the current context of a normal scope.
*
* @param <T>
* @see CurrentContextFactory
*/
public interface CurrentContext<T extends ContextState> {

/**
*
* @return the current state
*/
T get();

/**
* Sets the current state.
*
* @param state
*/
void set(T state);

/**
* Removes the current state.
*/
void remove();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.arc;

import io.quarkus.arc.InjectableContext.ContextState;
import java.lang.annotation.Annotation;

/**
* This factory can be used to create a new {@link CurrentContext} for a normal scope, e.g. for
* {@link javax.enterprise.context.RequestScoped}. It's usually not necessary for shared contexts, such as
* {@link javax.enterprise.context.ApplicationScoped}.
*/
public interface CurrentContextFactory {

<T extends ContextState> CurrentContext<T> create(Class<? extends Annotation> scope);

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ default boolean isNormal() {
return getScope().isAnnotationPresent(NormalScope.class);
}

/**
*
*/
interface ContextState {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.Components;
import io.quarkus.arc.ComponentsProvider;
import io.quarkus.arc.CurrentContextFactory;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableContext;
import io.quarkus.arc.InjectableDecorator;
Expand Down Expand Up @@ -46,6 +47,7 @@
import javax.enterprise.context.Destroyed;
import javax.enterprise.context.Initialized;
import javax.enterprise.context.NormalScope;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.event.Event;
import javax.enterprise.inject.AmbiguousResolutionException;
import javax.enterprise.inject.Any;
Expand Down Expand Up @@ -95,7 +97,9 @@ public class ArcContainerImpl implements ArcContainer {

private volatile ExecutorService executorService;

public ArcContainerImpl() {
private final CurrentContextFactory currentContextFactory;

public ArcContainerImpl(CurrentContextFactory currentContextFactory) {
id = String.valueOf(ID_GENERATOR.incrementAndGet());
running = new AtomicBoolean(true);
List<InjectableBean<?>> beans = new ArrayList<>();
Expand All @@ -105,10 +109,12 @@ public ArcContainerImpl() {
List<InjectableObserverMethod<?>> observers = new ArrayList<>();
Map<Class<? extends Annotation>, Set<Annotation>> transitiveInterceptorBindings = new HashMap<>();
Map<String, Set<String>> qualifierNonbindingMembers = new HashMap<>();
this.currentContextFactory = currentContextFactory == null ? new ThreadLocalCurrentContextFactory()
: currentContextFactory;

applicationContext = new ApplicationContext();
singletonContext = new SingletonContext();
requestContext = new RequestContext();
requestContext = new RequestContext(this.currentContextFactory.create(RequestScoped.class));
contexts = new HashMap<>();
putContext(requestContext);
putContext(applicationContext);
Expand Down Expand Up @@ -342,6 +348,11 @@ public void setExecutor(ExecutorService executor) {
this.executorService = executor;
}

@Override
public CurrentContextFactory getCurrentContextFactory() {
return currentContextFactory;
}

@Override
public String toString() {
return "ArcContainerImpl [id=" + id + ", running=" + running + ", beans=" + beans.size() + ", observers="
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.arc.impl;

import io.quarkus.arc.ContextInstanceHandle;
import io.quarkus.arc.CurrentContext;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.ManagedContext;
import io.quarkus.arc.impl.EventImpl.Notifier;
Expand Down Expand Up @@ -33,14 +34,14 @@ class RequestContext implements ManagedContext {

private static final Logger LOGGER = Logger.getLogger(RequestContext.class.getPackage().getName());

// It's a normal scope so there may be no more than one mapped instance per contextual type per thread
private final ThreadLocal<RequestContextState> currentContext = new ThreadLocal<>();
private final CurrentContext<RequestContextState> currentContext;

private final LazyValue<Notifier<Object>> initializedNotifier;
private final LazyValue<Notifier<Object>> beforeDestroyedNotifier;
private final LazyValue<Notifier<Object>> destroyedNotifier;

public RequestContext() {
public RequestContext(CurrentContext<RequestContextState> currentContext) {
this.currentContext = currentContext;
this.initializedNotifier = new LazyValue<>(RequestContext::createInitializedNotifier);
this.beforeDestroyedNotifier = new LazyValue<>(RequestContext::createBeforeDestroyedNotifier);
this.destroyedNotifier = new LazyValue<>(RequestContext::createDestroyedNotifier);
Expand All @@ -62,17 +63,16 @@ public <T> T getIfActive(Contextual<T> contextual, Function<Contextual<T>, Creat
}
RequestContextState ctxState = currentContext.get();
if (ctxState == null) {
// Thread local not set - context is not active!
// Context is not active!
return null;
}
Map<Contextual<?>, ContextInstanceHandle<?>> ctxMap = currentContext.get().value;
ContextInstanceHandle<T> instance = (ContextInstanceHandle<T>) ctxMap.get(contextual);
ContextInstanceHandle<T> instance = (ContextInstanceHandle<T>) ctxState.map.get(contextual);
if (instance == null) {
CreationalContext<T> creationalContext = creationalContextFun.apply(contextual);
// Bean instance does not exist - create one if we have CreationalContext
instance = new ContextInstanceHandleImpl<T>((InjectableBean<T>) contextual,
contextual.create(creationalContext), creationalContext);
ctxMap.put(contextual, instance);
ctxState.map.put(contextual, instance);
}
return instance.get();
}
Expand All @@ -82,7 +82,6 @@ public <T> T get(Contextual<T> contextual, CreationalContext<T> creationalContex
T result = getIfActive(contextual,
CreationalContextImpl.unwrap(Objects.requireNonNull(creationalContext, "CreationalContext must not be null")));
if (result == null) {
// Thread local not set - context is not active!
throw new ContextNotActiveException();
}
return result;
Expand All @@ -96,12 +95,11 @@ public <T> T get(Contextual<T> contextual) {
if (!Scopes.scopeMatches(this, bean)) {
throw Scopes.scopeDoesNotMatchException(this, bean);
}
Map<Contextual<?>, ContextInstanceHandle<?>> ctx = currentContext.get().value;
if (ctx == null) {
// Thread local not set - context is not active!
RequestContextState state = currentContext.get();
if (state == null) {
throw new ContextNotActiveException();
}
ContextInstanceHandle<T> instance = (ContextInstanceHandle<T>) ctx.get(contextual);
ContextInstanceHandle<T> instance = (ContextInstanceHandle<T>) state.map.get(contextual);
return instance == null ? null : instance.get();
}

Expand All @@ -112,12 +110,12 @@ public boolean isActive() {

@Override
public void destroy(Contextual<?> contextual) {
Map<Contextual<?>, ContextInstanceHandle<?>> ctx = currentContext.get().value;
if (ctx == null) {
// Thread local not set - context is not active!
RequestContextState state = currentContext.get();
if (state == null) {
// Context is not active
throw new ContextNotActiveException();
}
ContextInstanceHandle<?> instance = ctx.remove(contextual);
ContextInstanceHandle<?> instance = state.map.remove(contextual);
if (instance != null) {
instance.destroy();
}
Expand All @@ -131,7 +129,7 @@ public void activate(ContextState initialState) {
fireIfNotEmpty(initializedNotifier);
} else {
if (initialState instanceof RequestContextState) {
currentContext.set(((RequestContextState) initialState));
currentContext.set((RequestContextState) initialState);
} else {
throw new IllegalArgumentException("Invalid initial state: " + initialState.getClass().getName());
}
Expand All @@ -140,20 +138,16 @@ public void activate(ContextState initialState) {

@Override
public ContextState getState() {
RequestContextState ctx = currentContext.get();
if (ctx == null) {
RequestContextState state = currentContext.get();
if (state == null) {
// Thread local not set - context is not active!
throw new ContextNotActiveException();
}
return ctx;
return state;
}

public ContextState getStateIfActive() {
RequestContextState ctx = currentContext.get();
if (ctx == null) {
return null;
}
return ctx;
return currentContext.get();
}

@Override
Expand All @@ -175,31 +169,26 @@ public void destroy(ContextState state) {
if (state instanceof RequestContextState) {
RequestContextState reqState = ((RequestContextState) state);
reqState.isValid.set(false);
destroy(reqState.value);
} else {
throw new IllegalArgumentException("Invalid state: " + state.getClass().getName());
}
}

private void destroy(Map<Contextual<?>, ContextInstanceHandle<?>> currentContext) {
if (currentContext != null) {
synchronized (currentContext) {
synchronized (state) {
Map<Contextual<?>, ContextInstanceHandle<?>> map = ((RequestContextState) state).map;
// Fire an event with qualifier @BeforeDestroyed(RequestScoped.class) if there are any observers for it
try {
fireIfNotEmpty(beforeDestroyedNotifier);
} catch (Exception e) {
LOGGER.warn("An error occurred during delivery of the @BeforeDestroyed(RequestScoped.class) event", e);
}
//Performance: avoid an iterator on the map elements
currentContext.forEach(this::destroyContextElement);
map.forEach(this::destroyContextElement);
// Fire an event with qualifier @Destroyed(RequestScoped.class) if there are any observers for it
try {
fireIfNotEmpty(destroyedNotifier);
} catch (Exception e) {
LOGGER.warn("An error occurred during delivery of the @Destroyed(RequestScoped.class) event", e);
}
currentContext.clear();
map.clear();
}
} else {
throw new IllegalArgumentException("Invalid state implementation: " + state.getClass().getName());
}
}

Expand Down Expand Up @@ -238,17 +227,17 @@ private static Notifier<Object> createDestroyedNotifier() {

static class RequestContextState implements ContextState {

private final ConcurrentMap<Contextual<?>, ContextInstanceHandle<?>> value;
private final Map<Contextual<?>, ContextInstanceHandle<?>> map;
private final AtomicBoolean isValid;

RequestContextState(ConcurrentMap<Contextual<?>, ContextInstanceHandle<?>> value) {
this.value = value;
this.map = Objects.requireNonNull(value);
this.isValid = new AtomicBoolean(true);
}

@Override
public Map<InjectableBean<?>, Object> getContextualInstances() {
return value.values().stream()
return map.values().stream()
.collect(Collectors.toUnmodifiableMap(ContextInstanceHandle::getBean, ContextInstanceHandle::get));
}

Expand Down
Loading

0 comments on commit db7c71d

Please sign in to comment.