Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.x: Global instance handling change (and relevant changes to testing) #9193

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions all/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,10 @@
<groupId>io.helidon.common.processor</groupId>
<artifactId>helidon-common-processor-class-model</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.testing</groupId>
<artifactId>helidon-testing</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common.testing</groupId>
<artifactId>helidon-common-testing-junit5</artifactId>
Expand Down Expand Up @@ -924,6 +928,10 @@
<groupId>io.helidon.http.media</groupId>
<artifactId>helidon-http-media-multipart</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.testing</groupId>
<artifactId>helidon-testing-junit5</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver</artifactId>
Expand Down
10 changes: 10 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,16 @@
<artifactId>helidon-http-media-multipart</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.testing</groupId>
<artifactId>helidon-testing</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.testing</groupId>
<artifactId>helidon-testing-junit5</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver</artifactId>
Expand Down
4 changes: 4 additions & 0 deletions common/config/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-mapper</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-context</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down
11 changes: 11 additions & 0 deletions common/config/src/main/java/io/helidon/common/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ static Config empty() {
return EmptyConfig.EMPTY;
}

/**
* Create a new instance of configuration from the default configuration sources.
* In case there is no {@link io.helidon.common.config.spi.ConfigProvider} available, returns
* {@link #empty()}.
*
* @return a new configuration
*/
static Config create() {
return GlobalConfig.create();
}

/**
* Returns the fully-qualified key of the {@code Config} node.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
* Copyright (c) 2022, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,12 +19,12 @@
import java.util.List;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.LazyValue;
import io.helidon.common.config.spi.ConfigProvider;
import io.helidon.common.context.ContextSingleton;

/**
* Global configuration can be set by a user before any Helidon code is invoked, to override default discovery
Expand All @@ -37,18 +37,9 @@
*/
public final class GlobalConfig {
private static final Config EMPTY = Config.empty();
private static final LazyValue<Config> DEFAULT_CONFIG = LazyValue.create(() -> {
List<ConfigProvider> providers = HelidonServiceLoader.create(ServiceLoader.load(ConfigProvider.class))
.asList();
// no implementations available, use empty configuration
if (providers.isEmpty()) {
return EMPTY;
}
// there is a valid provider, let's use its default configuration
return providers.get(0)
.create();
});
private static final AtomicReference<Config> CONFIG = new AtomicReference<>();
private static final LazyValue<Config> DEFAULT_CONFIG = LazyValue.create(GlobalConfig::create);
private static final ContextSingleton<Config> CONTEXT_VALUE = ContextSingleton.create(GlobalConfig.class,
Config.class);

private GlobalConfig() {
}
Expand All @@ -59,7 +50,7 @@ private GlobalConfig() {
* @return {@code true} if there is a global configuration set already, {@code false} otherwise
*/
public static boolean configured() {
return CONFIG.get() != null;
return CONTEXT_VALUE.isPresent();
}

/**
Expand All @@ -70,7 +61,7 @@ public static boolean configured() {
* @see #config(java.util.function.Supplier, boolean)
*/
public static Config config() {
return configured() ? CONFIG.get() : DEFAULT_CONFIG.get();
return CONTEXT_VALUE.value().orElseGet(DEFAULT_CONFIG);
}

/**
Expand All @@ -86,7 +77,7 @@ public static Config config(Supplier<Config> config) {
/**
* Set global configuration.
*
* @param config configuration to use
* @param config configuration to use
* @param overwrite whether to overwrite an existing configured value
* @return current global config
*/
Expand All @@ -96,8 +87,22 @@ public static Config config(Supplier<Config> config, boolean overwrite) {
if (overwrite || !configured()) {
// there is a certain risk we may do this twice, if two components try to set global config in parallel.
// as the result was already unclear (as order matters), we do not need to be 100% thread safe here
CONFIG.set(config.get());
CONTEXT_VALUE.set(config.get());
return config();
} else {
return CONTEXT_VALUE.get(config);
}
return CONFIG.get();
}

static Config create() {
List<ConfigProvider> providers = HelidonServiceLoader.create(ServiceLoader.load(ConfigProvider.class))
.asList();
// no implementations available, use empty configuration
if (providers.isEmpty()) {
return EMPTY;
}
// there is a valid provider, let's use its default configuration
return providers.getFirst()
.create();
}
}
3 changes: 2 additions & 1 deletion common/config/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
* Copyright (c) 2022, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,7 @@
*/
module io.helidon.common.config {

requires io.helidon.common.context;
requires transitive io.helidon.common;
requires transitive io.helidon.common.mapper;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.common.context;

import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;

/**
* This is similar to {@link java.lang.ThreadLocal}, except it uses {@code static} context to store a value.
tomas-langer marked this conversation as resolved.
Show resolved Hide resolved
* Static context is either a context available under special classifier {@value #STATIC_CONTEXT_CLASSIFIER}, or the
* {@link io.helidon.common.context.Contexts#globalContext()} (if the qualifier is not registered).
* <p>
* The intention of this type is to have singletons for production runtime, while maintaining a separation during
* testing, where Helidon test extensions run each part of the test class in a test context.
* <p>
* Note that the instance is not simply registered in the context, but it uses this class, and the "owner type" as a classifier
tomas-langer marked this conversation as resolved.
Show resolved Hide resolved
* to avoid conflicts.
*
* @param <T> type of the value stored in context
*/
public final class ContextSingleton<T> {
/**
* Classifier used to register a context that is to serve as the static context.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classifier used to register contexts as "singleton" capable.

*/
public static final String STATIC_CONTEXT_CLASSIFIER = "helidon-static-context";
tomas-langer marked this conversation as resolved.
Show resolved Hide resolved

private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ContextSingletonClassifier<T> classifier;
private final Supplier<T> supplier;

private ContextSingleton(ContextSingletonClassifier<T> qualifier, Supplier<T> supplier) {
this.classifier = qualifier;
this.supplier = supplier;
}

/**
* Create a new context value.
*
* @param ownerClass type owning this context singleton
* @param clazz type of the value
* @param <T> type of the value
* @return a new context value with nothing set.
*/
public static <T> ContextSingleton<T> create(Class<?> ownerClass, Class<T> clazz) {
Objects.requireNonNull(ownerClass);
Objects.requireNonNull(clazz);

return new ContextSingleton<>(new ContextSingletonClassifier<>(ownerClass, clazz), null);
}

/**
* Create a new context value with a supplier of instances for each context.
*
* @param ownerClass type owning this context singleton
* @param clazz type of the value
* @param value value supplier
* @param <T> type of the value
* @return a new context value
*/
public static <T> ContextSingleton<T> create(Class<?> ownerClass, Class<T> clazz, Supplier<T> value) {
Objects.requireNonNull(ownerClass);
Objects.requireNonNull(clazz);
Objects.requireNonNull(value);

return new ContextSingleton<>(new ContextSingletonClassifier<>(ownerClass, clazz), value);
}

/**
* Set the value in current context.
*
* @param value value to use
*/
public void set(T value) {
lock.writeLock().lock();
try {
context().register(classifier, value);
} finally {
lock.writeLock().unlock();
}
}

/**
* Get the current value in this context, or call the provided supplier to get the value and register
* it, if not yet registered.
*
* @return current value (or the new one obtained from supplier)
* @throws NoSuchElementException in case there is no value, and no supplier
* @see #set(Object)
*/
public T get() throws NoSuchElementException {
return get(this.supplier);
}

/**
* Get the current value in this context, or call the provided supplier to get the value and register
* it, if not yet registered.
*
* @param supplier supplier to call if the current value is not present
* @return current value (or the new one obtained from supplier)
* @throws NoSuchElementException in case there is no value, and the supplier returned {@code null}
* @see #set(Object)
*/
public T get(Supplier<T> supplier) {
var current = current();
if (current.isPresent()) {
return current.get();
}
lock.writeLock().lock();
try {
current = current();
if (current.isPresent()) {
return current.get();
}
if (supplier != null) {
var value = supplier.get();
set(value);
return value;
}
throw new NoSuchElementException("There is no value available in the current context, "
+ "and supplier was not provided when creating this instance for: "
+ " " + classifier);
} finally {
lock.writeLock().unlock();
}
}

/**
* Current value as an {@link java.util.Optional}.
*
* @return current value, or empty if not configured
*/
public Optional<T> value() {
return current();
}

/**
* Whether there is a value in current context.
*
* @return {@code true} if there is a value
*/
public boolean isPresent() {
return current().isPresent();
}

private static Context context() {
Context globalContext = Contexts.globalContext();

// this is the context we expect to get (and set global instances)
return Contexts.context()
.orElse(globalContext)
.get(STATIC_CONTEXT_CLASSIFIER, Context.class)
.orElse(globalContext);
}

private Optional<T> current() {
lock.readLock().lock();
try {
return context().get(classifier, classifier.valueType());
} finally {
lock.readLock().unlock();
}
}

@SuppressWarnings("rawtypes")
private record ContextSingletonClassifier<T>(Class<ContextSingleton> contextSingleton,
Class<?> ownerClass,
Class<T> valueType) {
private ContextSingletonClassifier(Class<?> ownerClass, Class<T> valueType) {
this(ContextSingleton.class, ownerClass, valueType);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2020 Oracle and/or its affiliates.
* Copyright (c) 2019, 2024 Oracle and/or its affiliates.
tomas-langer marked this conversation as resolved.
Show resolved Hide resolved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2021 Oracle and/or its affiliates.
* Copyright (c) 2019, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -108,6 +108,11 @@ public <T> Optional<T> get(Object classifier, Class<T> type) {
}
}

@Override
public String toString() {
return contextId;
}

long nextChildId() {
return contextCounter.getAndUpdate(operand -> (operand == Long.MAX_VALUE) ? 1 : (operand + 1));
}
Expand Down
Loading