diff --git a/context-propagation/src/test/java/io/micrometer/context/DefaultContextSnapshotDepreactionTests.java b/context-propagation/src/test/java/io/micrometer/context/DefaultContextSnapshotDepreactionTests.java index 9c41899..fc288d8 100644 --- a/context-propagation/src/test/java/io/micrometer/context/DefaultContextSnapshotDepreactionTests.java +++ b/context-propagation/src/test/java/io/micrometer/context/DefaultContextSnapshotDepreactionTests.java @@ -20,9 +20,9 @@ import java.util.Map; import io.micrometer.context.ContextSnapshot.Scope; -import io.micrometer.context.observation.Observation; -import io.micrometer.context.observation.ObservationScopeThreadLocalHolder; -import io.micrometer.context.observation.ObservationThreadLocalAccessor; +import io.micrometer.scopedvalue.ScopeHolder; +import io.micrometer.scopedvalue.ScopedValue; +import io.micrometer.scopedvalue.ScopedValueThreadLocalAccessor; import org.assertj.core.api.BDDAssertions; import org.junit.jupiter.api.Test; @@ -287,29 +287,31 @@ void toString_should_include_values() { @Test void should_work_with_scope_based_thread_local_accessor() { - this.registry.registerContextAccessor(new TestContextAccessor()); - this.registry.registerThreadLocalAccessor(new ObservationThreadLocalAccessor()); - - String key = ObservationThreadLocalAccessor.KEY; - Observation observation = new Observation(); - Map sourceContext = Collections.singletonMap(key, observation); - - then(ObservationScopeThreadLocalHolder.getCurrentObservation()).isNull(); - try (Scope scope1 = ContextSnapshot.setAllThreadLocalsFrom(sourceContext, this.registry)) { - then(ObservationScopeThreadLocalHolder.getCurrentObservation()).isSameAs(observation); - try (Scope scope2 = ContextSnapshot.setAllThreadLocalsFrom(Collections.emptyMap(), this.registry)) { - then(ObservationScopeThreadLocalHolder.getCurrentObservation()).isSameAs(observation); - // TODO: This should work like this in the future - // then(ObservationScopeThreadLocalHolder.getCurrentObservation()).as("We're - // resetting the observation").isNull(); - // then(ObservationScopeThreadLocalHolder.getValue()).as("This is the - // 'null' scope").isNotNull(); + TestContextAccessor accessor = new TestContextAccessor(); + this.registry.registerContextAccessor(accessor); + this.registry.registerThreadLocalAccessor(new ScopedValueThreadLocalAccessor()); + + ScopedValue value = ScopedValue.create("value"); + + assertThat(ScopeHolder.currentValue()).isNull(); + + Map sourceContext = Collections.singletonMap(ScopedValueThreadLocalAccessor.KEY, value); + + try (ContextSnapshot.Scope outer = ContextSnapshot.setAllThreadLocalsFrom(sourceContext, this.registry)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(value); + try (ContextSnapshot.Scope inner = ContextSnapshot.setAllThreadLocalsFrom(Collections.emptyMap(), + this.registry)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(value); + // The new API allows the following to happen when clearing behavior is + // specified on ContextSnapshotFactory + // assertThat(ScopeHolder.currentValue().get()).isNull(); } - then(ObservationScopeThreadLocalHolder.getCurrentObservation()).as("We're back to previous observation") - .isSameAs(observation); + assertThat(ScopeHolder.currentValue()).isEqualTo(value); } - then(ObservationScopeThreadLocalHolder.getCurrentObservation()).as("There was no observation at the beginning") - .isNull(); + assertThat(ScopeHolder.currentValue()).isNull(); + + this.registry.removeContextAccessor(accessor); + this.registry.removeThreadLocalAccessor(ScopedValueThreadLocalAccessor.KEY); } } diff --git a/context-propagation/src/test/java/io/micrometer/context/DefaultContextSnapshotTests.java b/context-propagation/src/test/java/io/micrometer/context/DefaultContextSnapshotTests.java index f0db32d..65219bf 100644 --- a/context-propagation/src/test/java/io/micrometer/context/DefaultContextSnapshotTests.java +++ b/context-propagation/src/test/java/io/micrometer/context/DefaultContextSnapshotTests.java @@ -20,9 +20,6 @@ import java.util.Map; import io.micrometer.context.ContextSnapshot.Scope; -import io.micrometer.context.observation.Observation; -import io.micrometer.context.observation.ObservationScopeThreadLocalHolder; -import io.micrometer.context.observation.ObservationThreadLocalAccessor; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -36,16 +33,12 @@ * Unit tests for {@link DefaultContextSnapshot}. * * @author Rossen Stoyanchev + * @author Dariusz Jędrzejczyk */ public class DefaultContextSnapshotTests { private final ContextRegistry registry = new ContextRegistry(); - private final ContextSnapshotFactory snapshotFactory = ContextSnapshotFactory.builder() - .contextRegistry(registry) - .clearMissing(false) - .build(); - @ParameterizedTest(name = "clearMissing={0}") @ValueSource(booleans = { true, false }) void should_propagate_thread_local(boolean clearMissing) { @@ -347,32 +340,6 @@ void should_clear_missing_thread_local() { assertThat(fooThreadLocal.get()).isEqualTo("present"); } - @Test - void should_work_with_scope_based_thread_local_accessor() { - registry.registerContextAccessor(new TestContextAccessor()); - registry.registerThreadLocalAccessor(new ObservationThreadLocalAccessor()); - - String key = ObservationThreadLocalAccessor.KEY; - Observation observation = new Observation(); - Map sourceContext = Collections.singletonMap(key, observation); - - then(ObservationScopeThreadLocalHolder.getCurrentObservation()).isNull(); - try (Scope scope1 = snapshotFactory.setThreadLocalsFrom(sourceContext)) { - then(ObservationScopeThreadLocalHolder.getCurrentObservation()).isSameAs(observation); - try (Scope scope2 = snapshotFactory.setThreadLocalsFrom(Collections.emptyMap())) { - then(ObservationScopeThreadLocalHolder.getCurrentObservation()) - .as("We're resetting the observation") - .isNull(); - then(ObservationScopeThreadLocalHolder.getValue()).as("This is the 'null' scope").isNotNull(); - } - then(ObservationScopeThreadLocalHolder.getCurrentObservation()).as("We're back to previous observation") - .isSameAs(observation); - } - then(ObservationScopeThreadLocalHolder.getCurrentObservation()) - .as("There was no observation at the beginning") - .isNull(); - } - @Test void should_clear_only_selected_thread_locals_when_filter_in_set() { ThreadLocal fooThreadLocal = new ThreadLocal<>(); diff --git a/context-propagation/src/test/java/io/micrometer/context/ScopedValueSnapshotTests.java b/context-propagation/src/test/java/io/micrometer/context/ScopedValueSnapshotTests.java new file mode 100644 index 0000000..b45b88e --- /dev/null +++ b/context-propagation/src/test/java/io/micrometer/context/ScopedValueSnapshotTests.java @@ -0,0 +1,157 @@ +/** + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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.micrometer.context; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import io.micrometer.scopedvalue.Scope; +import io.micrometer.scopedvalue.ScopedValue; +import io.micrometer.scopedvalue.ScopeHolder; +import io.micrometer.scopedvalue.ScopedValueThreadLocalAccessor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ContextSnapshotFactory} when used in scoped scenarios. + * + * @author Dariusz Jędrzejczyk + */ +public class ScopedValueSnapshotTests { + + private final ContextRegistry registry = new ContextRegistry(); + + private final ContextSnapshotFactory snapshotFactory = ContextSnapshotFactory.builder() + .contextRegistry(registry) + .build(); + + @BeforeEach + void initializeThreadLocalAccessors() { + registry.registerThreadLocalAccessor(new ScopedValueThreadLocalAccessor()); + } + + @AfterEach + void cleanupThreadLocals() { + ScopeHolder.remove(); + registry.removeThreadLocalAccessor(ScopedValueThreadLocalAccessor.KEY); + } + + @Test + void scopeWorksInAnotherThreadWhenWrapping() throws Exception { + AtomicReference valueInNewThread = new AtomicReference<>(); + ScopedValue scopedValue = ScopedValue.create("hello"); + + assertThat(ScopeHolder.currentValue()).isNull(); + + try (Scope scope = Scope.open(scopedValue)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); + Runnable wrapped = snapshotFactory.captureAll() + .wrap(() -> valueInNewThread.set(ScopeHolder.currentValue())); + Thread t = new Thread(wrapped); + t.start(); + t.join(); + } + + assertThat(valueInNewThread.get()).isEqualTo(scopedValue); + assertThat(ScopeHolder.currentValue()).isNull(); + } + + @Test + void nestedScopeWorksInAnotherThreadWhenWrapping() throws Exception { + AtomicReference value1InNewThreadBefore = new AtomicReference<>(); + AtomicReference value1InNewThreadAfter = new AtomicReference<>(); + AtomicReference value2InNewThread = new AtomicReference<>(); + + ScopedValue v1 = ScopedValue.create("val1"); + ScopedValue v2 = ScopedValue.create("val2"); + + assertThat(ScopeHolder.currentValue()).isNull(); + + Thread t; + + try (Scope v1Scope = Scope.open(v1)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + try (Scope v2scope1T1 = Scope.open(v2)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + try (Scope v2scope2T1 = Scope.open(v2)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + Runnable runnable = () -> { + value1InNewThreadBefore.set(ScopeHolder.currentValue()); + try (Scope v2scopeT2 = Scope.open(v2)) { + value2InNewThread.set(ScopeHolder.currentValue()); + } + value1InNewThreadAfter.set(ScopeHolder.currentValue()); + }; + + Runnable wrapped = snapshotFactory.captureAll().wrap(runnable); + t = new Thread(wrapped); + t.start(); + + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + assertThat(ScopeHolder.get()).isEqualTo(v2scope2T1); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + assertThat(ScopeHolder.get()).isEqualTo(v2scope1T1); + } + + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + + try (Scope childScope3 = Scope.open(v2)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + assertThat(ScopeHolder.get()).isEqualTo(childScope3); + } + + t.join(); + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + } + + assertThat(value1InNewThreadBefore.get()).isEqualTo(v2); + assertThat(value1InNewThreadAfter.get()).isEqualTo(v2); + assertThat(value2InNewThread.get()).isEqualTo(v2); + assertThat(ScopeHolder.currentValue()).isNull(); + } + + @Test + void shouldProperlyClearInNestedScope() { + TestContextAccessor accessor = new TestContextAccessor(); + ContextSnapshotFactory snapshotFactory = ContextSnapshotFactory.builder() + .contextRegistry(registry) + .clearMissing(true) + .build(); + registry.registerContextAccessor(accessor); + ScopedValue value = ScopedValue.create("value"); + + assertThat(ScopeHolder.currentValue()).isNull(); + + Map sourceContext = Collections.singletonMap(ScopedValueThreadLocalAccessor.KEY, value); + + try (ContextSnapshot.Scope outer = snapshotFactory.setThreadLocalsFrom(sourceContext)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(value); + try (ContextSnapshot.Scope inner = snapshotFactory.setThreadLocalsFrom(Collections.emptyMap())) { + assertThat(ScopeHolder.currentValue().get()).isNull(); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(value); + } + assertThat(ScopeHolder.currentValue()).isNull(); + + registry.removeContextAccessor(accessor); + } + +} diff --git a/context-propagation/src/test/java/io/micrometer/context/observation/NullObservation.java b/context-propagation/src/test/java/io/micrometer/context/observation/NullObservation.java deleted file mode 100644 index 1f078d1..0000000 --- a/context-propagation/src/test/java/io/micrometer/context/observation/NullObservation.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright 2023 the original author or authors. - * - * 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 - * - * https://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.micrometer.context.observation; - -class NullObservation extends Observation { - - Scope openScope() { - return new RevertingScope(null); - } - -} diff --git a/context-propagation/src/test/java/io/micrometer/context/observation/Observation.java b/context-propagation/src/test/java/io/micrometer/context/observation/Observation.java deleted file mode 100644 index b597907..0000000 --- a/context-propagation/src/test/java/io/micrometer/context/observation/Observation.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright 2023 the original author or authors. - * - * 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 - * - * https://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.micrometer.context.observation; - -public class Observation { - - Scope openScope() { - return new RevertingScope(this); - } - -} diff --git a/context-propagation/src/test/java/io/micrometer/context/observation/ObservationThreadLocalAccessor.java b/context-propagation/src/test/java/io/micrometer/context/observation/ObservationThreadLocalAccessor.java deleted file mode 100644 index e35f66f..0000000 --- a/context-propagation/src/test/java/io/micrometer/context/observation/ObservationThreadLocalAccessor.java +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright 2023 the original author or authors. - * - * 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 - * - * https://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.micrometer.context.observation; - -import java.util.logging.Level; -import java.util.logging.Logger; - -import io.micrometer.context.ThreadLocalAccessor; - -/** - * Example {@link ThreadLocalAccessor} implementation. - */ -public class ObservationThreadLocalAccessor implements ThreadLocalAccessor { - - private static final Logger log = Logger.getLogger(ObservationThreadLocalAccessor.class.getName()); - - public static final String KEY = "micrometer.observation"; - - @Override - public Object key() { - return KEY; - } - - @Override - public Observation getValue() { - return ObservationScopeThreadLocalHolder.getCurrentObservation(); - } - - @Override - public void setValue(Observation value) { - value.openScope(); - } - - @Override - public void setValue() { - new NullObservation().openScope(); - } - - @Override - public void restore(Observation value) { - Scope scope = ObservationScopeThreadLocalHolder.getValue(); - if (scope == null) { - String msg = "There is no current scope in thread local. This situation should not happen"; - log.log(Level.WARNING, msg); - assert false : msg; - } - Scope previousObservationScope = scope.getPreviousObservationScope(); - if (previousObservationScope == null || value != previousObservationScope.getCurrentObservation()) { - Observation previousObservation = previousObservationScope != null - ? previousObservationScope.getCurrentObservation() : null; - String msg = "Observation <" + value - + "> to which we're restoring is not the same as the one set as this scope's parent observation <" - + previousObservation - + "> . Most likely a manually created Observation has a scope opened that was never closed. This may lead to thread polluting and memory leaks"; - log.log(Level.WARNING, msg); - assert false : msg; - } - close(); - } - - @Override - public void restore() { - close(); - } - - private void close() { - Scope scope = ObservationScopeThreadLocalHolder.getValue(); - if (scope != null) { - scope.close(); - } - } - -} diff --git a/context-propagation/src/test/java/io/micrometer/context/observation/RevertingScope.java b/context-propagation/src/test/java/io/micrometer/context/observation/RevertingScope.java deleted file mode 100644 index 4a70235..0000000 --- a/context-propagation/src/test/java/io/micrometer/context/observation/RevertingScope.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright 2023 the original author or authors. - * - * 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 - * - * https://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.micrometer.context.observation; - -class RevertingScope implements Scope { - - private final Scope previousScope; - - private final Observation observation; - - RevertingScope(Observation observation) { - this.previousScope = ObservationScopeThreadLocalHolder.getValue(); - this.observation = observation; - ObservationScopeThreadLocalHolder.setValue(this); - } - - @Override - public void close() { - ObservationScopeThreadLocalHolder.setValue(this.previousScope); - } - - @Override - public Scope getPreviousObservationScope() { - return this.previousScope; - } - - @Override - public Observation getCurrentObservation() { - return this.observation; - } - -} diff --git a/context-propagation/src/test/java/io/micrometer/context/observation/Scope.java b/context-propagation/src/test/java/io/micrometer/context/observation/Scope.java deleted file mode 100644 index c6f2753..0000000 --- a/context-propagation/src/test/java/io/micrometer/context/observation/Scope.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2023 the original author or authors. - * - * 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 - * - * https://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.micrometer.context.observation; - -import java.io.Closeable; - -interface Scope extends Closeable { - - Observation getCurrentObservation(); - - Scope getPreviousObservationScope(); - - @Override - void close(); - -} diff --git a/context-propagation/src/test/java/io/micrometer/scopedvalue/Scope.java b/context-propagation/src/test/java/io/micrometer/scopedvalue/Scope.java new file mode 100644 index 0000000..0879edc --- /dev/null +++ b/context-propagation/src/test/java/io/micrometer/scopedvalue/Scope.java @@ -0,0 +1,63 @@ +/** + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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.micrometer.scopedvalue; + +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; + +/** + * Represents a scope in which a {@link ScopedValue} is set for a particular Thread and + * maintains a hierarchy between this instance and the parent. + */ +public class Scope implements AutoCloseable { + + private static final Logger log = Logger.getLogger(Scope.class.getName()); + + final ScopedValue scopedValue; + + final Scope parentScope; + + private Scope(ScopedValue scopedValue, Scope parentScope) { + log.log(INFO, () -> String.format("%s: open scope[%s]", scopedValue.get(), hashCode())); + this.scopedValue = scopedValue; + this.parentScope = parentScope; + } + + /** + * Create a new scope and set the value for this Thread. + * @return newly created {@link Scope} + */ + public static Scope open(ScopedValue value) { + Scope scope = new Scope(value, ScopeHolder.get()); + ScopeHolder.set(scope); + return scope; + } + + @Override + public void close() { + if (parentScope == null) { + log.log(INFO, () -> String.format("%s: remove scope[%s]", scopedValue.get(), hashCode())); + ScopeHolder.remove(); + } + else { + log.log(INFO, () -> String.format("%s: close scope[%s] -> restore %s scope[%s]", scopedValue.get(), + hashCode(), parentScope.scopedValue.get(), parentScope.hashCode())); + ScopeHolder.set(parentScope); + } + } + +} diff --git a/context-propagation/src/test/java/io/micrometer/context/observation/ObservationScopeThreadLocalHolder.java b/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopeHolder.java similarity index 50% rename from context-propagation/src/test/java/io/micrometer/context/observation/ObservationScopeThreadLocalHolder.java rename to context-propagation/src/test/java/io/micrometer/scopedvalue/ScopeHolder.java index 70aaa8a..d9c14aa 100644 --- a/context-propagation/src/test/java/io/micrometer/context/observation/ObservationScopeThreadLocalHolder.java +++ b/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopeHolder.java @@ -13,26 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micrometer.context.observation; +package io.micrometer.scopedvalue; -public class ObservationScopeThreadLocalHolder { +import org.assertj.core.util.VisibleForTesting; - private static final ThreadLocal holder = new ThreadLocal<>(); +/** + * Thread-local storage for the current value in scope for the current Thread. + */ +public class ScopeHolder { + + private static final ThreadLocal SCOPE = new ThreadLocal<>(); + + public static ScopedValue currentValue() { + Scope scope = SCOPE.get(); + return scope == null ? null : scope.scopedValue; + } - public static void setValue(Scope value) { - holder.set(value); + public static Scope get() { + return SCOPE.get(); } - public static Scope getValue() { - return holder.get(); + static void set(Scope scope) { + SCOPE.set(scope); } - public static Observation getCurrentObservation() { - Scope scope = holder.get(); - if (scope != null) { - return scope.getCurrentObservation(); - } - return null; + @VisibleForTesting + public static void remove() { + SCOPE.remove(); } } diff --git a/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValue.java b/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValue.java new file mode 100644 index 0000000..db4c072 --- /dev/null +++ b/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValue.java @@ -0,0 +1,62 @@ +/** + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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.micrometer.scopedvalue; + +import java.util.Objects; + +/** + * Serves as an abstraction of a value which can be in the current Thread-local + * {@link Scope scope} that maintains the hierarchy between the parent a new scope with + * potentially a different value. + * + * @author Dariusz Jędrzejczyk + */ +public class ScopedValue { + + private final String value; + + private ScopedValue(String value) { + this.value = value; + } + + /** + * Creates a new instance, which can be set in scope via {@link Scope#open()}. + * @param value {@code String} value associated with created {@link ScopedValue} + * @return new instance + */ + public static ScopedValue create(String value) { + Objects.requireNonNull(value, "value can't be null"); + return new ScopedValue(value); + } + + /** + * Creates a dummy instance used for nested scopes, in which the value should be + * virtually absent, but allows reverting to the previous value in scope. + * @return new instance representing an empty scope + */ + static ScopedValue nullValue() { + return new ScopedValue(null); + } + + /** + * {@code String} value associated with this instance. + * @return associated value + */ + public String get() { + return value; + } + +} diff --git a/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValueTest.java b/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValueTest.java new file mode 100644 index 0000000..b63edd3 --- /dev/null +++ b/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValueTest.java @@ -0,0 +1,92 @@ +/** + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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.micrometer.scopedvalue; + +import io.micrometer.scopedvalue.Scope; +import io.micrometer.scopedvalue.ScopedValue; +import io.micrometer.scopedvalue.ScopeHolder; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ScopedValue}. + * + * @author Dariusz Jędrzejczyk + */ +class ScopedValueTest { + + @Test + void basicScopeWorks() { + assertThat(ScopeHolder.currentValue()).isNull(); + + ScopedValue scopedValue = ScopedValue.create("hello"); + try (Scope scope = Scope.open(scopedValue)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); + } + + assertThat(ScopeHolder.currentValue()).isNull(); + } + + @Test + void emptyScopeWorks() { + assertThat(ScopeHolder.currentValue()).isNull(); + + ScopedValue scopedValue = ScopedValue.create("hello"); + try (Scope scope = Scope.open(scopedValue)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); + try (Scope emptyScope = Scope.open(ScopedValue.nullValue())) { + assertThat(ScopeHolder.currentValue().get()).isNull(); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); + } + + assertThat(ScopeHolder.currentValue()).isNull(); + } + + @Test + void multiLevelScopesWithDifferentValues() { + ScopedValue v1 = ScopedValue.create("val1"); + ScopedValue v2 = ScopedValue.create("val2"); + + try (Scope v1scope1 = Scope.open(v1)) { + try (Scope v1scope2 = Scope.open(v1)) { + try (Scope v2scope1 = Scope.open(v2)) { + try (Scope v2scope2 = Scope.open(v2)) { + try (Scope v1scope3 = Scope.open(v1)) { + try (Scope nullScope = Scope.open(ScopedValue.nullValue())) { + assertThat(ScopeHolder.currentValue().get()).isNull(); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope3); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + assertThat(ScopeHolder.get()).isEqualTo(v2scope2); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + assertThat(ScopeHolder.get()).isEqualTo(v2scope1); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope2); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope1); + } + + assertThat(ScopeHolder.currentValue()).isNull(); + } + +} diff --git a/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValueThreadLocalAccessor.java b/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValueThreadLocalAccessor.java new file mode 100644 index 0000000..99d0262 --- /dev/null +++ b/context-propagation/src/test/java/io/micrometer/scopedvalue/ScopedValueThreadLocalAccessor.java @@ -0,0 +1,77 @@ +/** + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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.micrometer.scopedvalue; + +import io.micrometer.context.ThreadLocalAccessor; + +/** + * Accessor for {@link ScopedValue}. + * + * @author Dariusz Jędrzejczyk + */ +public class ScopedValueThreadLocalAccessor implements ThreadLocalAccessor { + + /** + * The key used for registrations in {@link io.micrometer.context.ContextRegistry}. + */ + public static final String KEY = "svtla"; + + @Override + public Object key() { + return KEY; + } + + @Override + public ScopedValue getValue() { + return ScopeHolder.currentValue(); + } + + @Override + public void setValue(ScopedValue value) { + Scope.open(value); + } + + @Override + public void setValue() { + Scope.open(ScopedValue.nullValue()); + } + + @Override + public void restore(ScopedValue previousValue) { + Scope currentScope = ScopeHolder.get(); + if (currentScope != null) { + if (currentScope.parentScope == null || currentScope.parentScope.scopedValue != previousValue) { + throw new RuntimeException("Restoring to a different previous scope than expected!"); + } + currentScope.close(); + } + else { + throw new RuntimeException("Restoring to previous scope, but current is missing."); + } + } + + @Override + public void restore() { + Scope currentScope = ScopeHolder.get(); + if (currentScope != null) { + currentScope.close(); + } + else { + throw new RuntimeException("Restoring to previous scope, but current is missing."); + } + } + +}