diff --git a/opentracing-span-propagation/src/main/java/nl/talsmasoftware/context/opentracing/ContextScopeManager.java b/opentracing-span-propagation/src/main/java/nl/talsmasoftware/context/opentracing/ContextScopeManager.java new file mode 100644 index 00000000..3ab63e27 --- /dev/null +++ b/opentracing-span-propagation/src/main/java/nl/talsmasoftware/context/opentracing/ContextScopeManager.java @@ -0,0 +1,136 @@ +/* + * Copyright 2016-2019 Talsma ICT + * + * 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 nl.talsmasoftware.context.opentracing; + +import io.opentracing.Scope; +import io.opentracing.ScopeManager; +import io.opentracing.Span; +import nl.talsmasoftware.context.Context; +import nl.talsmasoftware.context.ContextManager; +import nl.talsmasoftware.context.threadlocal.AbstractThreadLocalContext; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Our own implementation of the opentracing {@linkplain ScopeManager}. + * + *

+ * Manages opentracing {@linkplain Scope} and allows it to be nested within another active scope, + * taking care to restore the previous value when closing an active scope. + * + *

+ * This manager is based on our {@linkplain nl.talsmasoftware.context.threadlocal.AbstractThreadLocalContext} + * implementation. Compared to the 'standard' {@linkplain io.opentracing.util.ThreadLocalScopeManager} + * this implementation has the following advantages: + *

    + *
  1. Close is explicitly idempotent; closing more than once has no additional side-effects + * (even when finishOnClose is set to {@code true}).
  2. + *
  3. More predictable behaviour for out-of-order closing of scopes. + * Although this is explicitly unsupported by the opentracing specification, + * we think having consistent and predictable behaviour is an advantage. + *
  4. Support for {@link nl.talsmasoftware.context.observer.ContextObserver}. + * See https://github.com/opentracing/opentracing-java/issues/334 explicitly wanting this. + *
+ * + *

+ * Please note that this scope manager is not somehow automatically enabled. + * You will have to provide an instance to your tracer of choice when initializing it. + * + *

+ * The active span that is automatically propagated when using this + * {@code opentracing-span-propagation} library in combination with + * the context aware support classes is from the registered ScopeManager + * from the {@linkplain io.opentracing.util.GlobalTracer}. + * + * @since 1.0.6 + */ +public class ContextScopeManager implements ScopeManager, ContextManager { + /** + * Makes the given span the new active span. + * + * @param span The span to become the active span. + * @param finishSpanOnClose Whether the span should automatically finish when closing the resulting scope. + * @return The new active scope (must be closed from the same thread). + */ + @Override + public Scope activate(Span span, boolean finishSpanOnClose) { + return new ThreadLocalSpanContext(getClass(), span, finishSpanOnClose); + } + + /** + * The currently active {@link Scope} containing the active span {@link Scope#span()}. + * + * @return the active scope, or {@code null} if none could be found. + */ + @Override + public Scope active() { + return ThreadLocalSpanContext.current(); + } + + /** + * Initializes a new context for the given {@linkplain Span}. + * + * @param value The span to activate. + * @return The new active 'Scope'. + * @see #activate(Span, boolean) + */ + @Override + public Context initializeNewContext(Span value) { + return new ThreadLocalSpanContext(getClass(), value, false); + } + + /** + * @return The active span context (this is identical to the active scope). + * @see #active() + */ + @Override + public Context getActiveContext() { + return ThreadLocalSpanContext.current(); + } + + /** + * @return String representation for this context manager. + */ + public String toString() { + return getClass().getSimpleName(); + } + + private static final class ThreadLocalSpanContext extends AbstractThreadLocalContext implements Scope { + private final AtomicBoolean finishOnClose; + + private ThreadLocalSpanContext(Class> contextManagerType, Span newValue, boolean finishOnClose) { + super(contextManagerType, newValue); + this.finishOnClose = new AtomicBoolean(finishOnClose); + } + + private static ThreadLocalSpanContext current() { + return current(ThreadLocalSpanContext.class); + } + + @Override + public Span span() { + return value; + } + + @Override + public void close() { + super.close(); + if (finishOnClose.compareAndSet(true, false) && value != null) { + value.finish(); + } + } + } +} diff --git a/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerObserver.java b/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerObserver.java new file mode 100644 index 00000000..aed7daba --- /dev/null +++ b/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerObserver.java @@ -0,0 +1,109 @@ +/* + * Copyright 2016-2019 Talsma ICT + * + * 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 nl.talsmasoftware.context.opentracing; + +import io.opentracing.Span; +import io.opentracing.mock.MockSpan; +import nl.talsmasoftware.context.ContextManager; +import nl.talsmasoftware.context.observer.ContextObserver; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.synchronizedList; + +public class ContextScopeManagerObserver implements ContextObserver { + static final List observed = synchronizedList(new ArrayList()); + + @Override + public Class> getObservedContextManager() { + return ContextScopeManager.class; + } + + @Override + public void onActivate(Span activatedContextValue, Span previousContextValue) { + observed.add(new Event(Event.Type.ACTIVATE, activatedContextValue)); + } + + @Override + public void onDeactivate(Span deactivatedContextValue, Span restoredContextValue) { + observed.add(new Event(Event.Type.DEACTIVATE, deactivatedContextValue)); + } + + static class Event { + enum Type {ACTIVATE, DEACTIVATE} + + final Thread thread; + final Type type; + final Span value; + + Event(Type type, Span value) { + this.thread = Thread.currentThread(); + this.type = type; + this.value = value; + } + + @Override + public String toString() { + return "Event{" + type + ", thread=" + thread.getName() + ", span=" + value + '}'; + } + } + + static class EventMatcher extends BaseMatcher { + Thread inThread; + Event.Type type; + Matcher spanMatcher; + + private EventMatcher(Event.Type type, Matcher spanMatcher) { + this.type = type; + this.spanMatcher = spanMatcher; + } + + static EventMatcher activated(Matcher span) { + return new EventMatcher(Event.Type.ACTIVATE, span); + } + + static EventMatcher deactivated(Matcher span) { + return new EventMatcher(Event.Type.DEACTIVATE, span); + } + + EventMatcher inThread(Thread thread) { + EventMatcher copy = new EventMatcher(type, spanMatcher); + copy.inThread = thread; + return copy; + } + + @Override + public boolean matches(Object actual) { + if (!(actual instanceof Event)) return actual == null; + Event actualEv = (Event) actual; + return (inThread == null || inThread.equals(actualEv.thread)) + && type.equals(actualEv.type) + && spanMatcher.matches(actualEv.value); + } + + @Override + public void describeTo(Description description) { + description.appendText("Event "); + if (inThread != null) description.appendText("in thread ").appendText(inThread.getName()); + description.appendValue(type).appendText(" "); + spanMatcher.describeTo(description); + } + } +} diff --git a/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerTest.java b/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerTest.java new file mode 100644 index 00000000..14706d3d --- /dev/null +++ b/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2016-2019 Talsma ICT + * + * 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 nl.talsmasoftware.context.opentracing; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.mock.MockTracer; +import io.opentracing.util.GlobalTracer; +import io.opentracing.util.GlobalTracerTestUtil; +import nl.talsmasoftware.context.Context; +import nl.talsmasoftware.context.ContextManagers; +import nl.talsmasoftware.context.executors.ContextAwareExecutorService; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static nl.talsmasoftware.context.opentracing.ContextScopeManagerObserver.EventMatcher.activated; +import static nl.talsmasoftware.context.opentracing.ContextScopeManagerObserver.EventMatcher.deactivated; +import static nl.talsmasoftware.context.opentracing.MockSpanMatcher.withOperationName; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; + +public class ContextScopeManagerTest { + MockTracer mockTracer; + ContextScopeManager scopeManager; + ExecutorService threadpool; + + @Before + public void registerMockGlobalTracer() { + GlobalTracerTestUtil.resetGlobalTracer(); + assertThat("Pre-existing GlobalTracer", GlobalTracer.isRegistered(), is(false)); + scopeManager = new ContextScopeManager(); + GlobalTracer.register(mockTracer = new MockTracer(scopeManager)); + threadpool = new ContextAwareExecutorService(Executors.newCachedThreadPool()); + } + + @After + public void cleanup() { + threadpool.shutdown(); + ContextManagers.clearActiveContexts(); + GlobalTracerTestUtil.resetGlobalTracer(); + ContextScopeManagerObserver.observed.clear(); + } + + @Test + public void testObservedSpans() { + assertThat(ContextScopeManagerObserver.observed, is(empty())); + Scope parent = GlobalTracer.get().buildSpan("parent").startActive(true); + Scope inner = GlobalTracer.get().buildSpan("inner").startActive(true); + inner.close(); + parent.close(); + + assertThat(ContextScopeManagerObserver.observed, contains( + activated(withOperationName("parent")), + activated(withOperationName("inner")), + deactivated(withOperationName("inner")), + deactivated(withOperationName("parent")) + )); + } + + @Test + public void testConcurrency() throws InterruptedException { + List threads = new ArrayList(); + final Scope parent = GlobalTracer.get().buildSpan("parent").startActive(true); + final CountDownLatch latch1 = new CountDownLatch(10), latch2 = new CountDownLatch(10); + for (int i = 0; i < 10; i++) { + final int nr = i; + threads.add(new Thread() { + @Override + public void run() { + Scope inner = GlobalTracer.get().buildSpan("inner" + nr).asChildOf(parent.span()).startActive(true); + waitFor(latch1); + inner.close(); + waitFor(latch2); + parent.close(); + } + }); + } + assertThat(ContextScopeManagerObserver.observed, contains( + activated(withOperationName("parent")) + )); + + assertThat(GlobalTracer.get().activeSpan(), equalTo(parent.span())); + for (Thread t : threads) t.start(); + for (Thread t : threads) t.join(); + assertThat(GlobalTracer.get().activeSpan(), is(nullValue())); + assertThat(ContextScopeManagerObserver.observed, contains( + activated(withOperationName("parent")), + activated(withOperationName(startsWith("inner"))), + activated(withOperationName(startsWith("inner"))), + activated(withOperationName(startsWith("inner"))), + activated(withOperationName(startsWith("inner"))), + activated(withOperationName(startsWith("inner"))), + activated(withOperationName(startsWith("inner"))), + activated(withOperationName(startsWith("inner"))), + activated(withOperationName(startsWith("inner"))), + activated(withOperationName(startsWith("inner"))), + activated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName(startsWith("inner"))), + deactivated(withOperationName("parent")) + )); + } + + @Test + public void testInitializeNewContext() { + Span span = GlobalTracer.get().buildSpan("span").start(); + Context context = scopeManager.initializeNewContext(span); + assertThat(scopeManager.getActiveContext().getValue(), is(span)); + assertThat(scopeManager.active().span(), is(span)); + assertThat(GlobalTracer.get().activeSpan(), is(span)); + context.close(); + span.finish(); + } + + @Test + public void testSimpleToStringWhenLogged() { + assertThat(scopeManager, hasToString(scopeManager.getClass().getSimpleName())); + } + + @Test + public void testPredictableOutOfOrderClosing() { + Scope first = GlobalTracer.get().buildSpan("first").startActive(true); + Scope second = GlobalTracer.get().buildSpan("second").startActive(true); + first.close(); + assertThat(GlobalTracer.get().activeSpan(), is(second.span())); + second.close(); + assertThat(GlobalTracer.get().activeSpan(), is(nullValue())); // first was already closed + } + + private static void waitFor(CountDownLatch latch) { + try { + latch.countDown(); + latch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + Assert.fail("Interrupted during test.."); + } + } +} diff --git a/opentracing-span-propagation/src/test/resources/META-INF/services/nl.talsmasoftware.context.observer.ContextObserver b/opentracing-span-propagation/src/test/resources/META-INF/services/nl.talsmasoftware.context.observer.ContextObserver new file mode 100644 index 00000000..bf52192a --- /dev/null +++ b/opentracing-span-propagation/src/test/resources/META-INF/services/nl.talsmasoftware.context.observer.ContextObserver @@ -0,0 +1 @@ +nl.talsmasoftware.context.opentracing.ContextScopeManagerObserver