.");
+ this.delegate = delegate;
+ this.spanManager = spanManager;
+ }
+
+ /**
+ * Propagates the {@link SpanManager#currentSpan() custom current span} into the runnable
+ * and performs cleanup afterwards.
+ *
+ * Note: The customCurrentSpan is merely propagated.
+ * The specified span is explicitly not finished by the runnable.
+ *
+ * @param runnable The runnable to be executed.
+ * @param customCurrentSpan The span to be propagated.
+ * @return The wrapped runnable to execute with the custom span as current span.
+ */
+ private Runnable runnableWithCurrentSpan(Runnable runnable, Span customCurrentSpan) {
+ return new RunnableWithManagedSpan(runnable, spanManager, customCurrentSpan);
+ }
+
+ /**
+ * Propagates the {@link SpanManager#currentSpan() custom current span} into the callable
+ * and performs cleanup afterwards.
+ *
+ * Note: The customCurrentSpan is merely propagated.
+ * The specified span is explicitly not finished by the callable.
+ *
+ * @param The callable result type.
+ * @param callable The callable to be executed.
+ * @param customCurrentSpan The span to be propagated.
+ * @return The wrapped callable to execute with the custom span as current span.
+ */
+ private Callable callableWithCurrentSpan(Callable callable, Span customCurrentSpan) {
+ return new CallableWithManagedSpan(callable, spanManager, customCurrentSpan);
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ delegate.execute(runnableWithCurrentSpan(command, spanManager.currentSpan()));
+ }
+
+ @Override
+ public Future> submit(Runnable task) {
+ return delegate.submit(runnableWithCurrentSpan(task, spanManager.currentSpan()));
+ }
+
+ @Override
+ public Future submit(Runnable task, T result) {
+ return delegate.submit(runnableWithCurrentSpan(task, spanManager.currentSpan()), result);
+ }
+
+ @Override
+ public Future submit(Callable task) {
+ return delegate.submit(callableWithCurrentSpan(task, spanManager.currentSpan()));
+ }
+
+ @Override
+ public List> invokeAll(Collection extends Callable> tasks) throws InterruptedException {
+ return delegate.invokeAll(tasksWithCurrentSpan(tasks, spanManager.currentSpan()));
+ }
+
+ @Override
+ public List> invokeAll(Collection extends Callable> tasks, long timeout, TimeUnit unit)
+ throws InterruptedException {
+ return delegate.invokeAll(tasksWithCurrentSpan(tasks, spanManager.currentSpan()), timeout, unit);
+ }
+
+ @Override
+ public T invokeAny(Collection extends Callable> tasks) throws InterruptedException, ExecutionException {
+ return delegate.invokeAny(tasksWithCurrentSpan(tasks, spanManager.currentSpan()));
+ }
+
+ @Override
+ public T invokeAny(Collection extends Callable> tasks, long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return delegate.invokeAny(tasksWithCurrentSpan(tasks, spanManager.currentSpan()), timeout, unit);
+ }
+
+ @Override
+ public void shutdown() {
+ delegate.shutdown();
+ }
+
+ @Override
+ public List shutdownNow() {
+ return delegate.shutdownNow();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return delegate.isShutdown();
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return delegate.isTerminated();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+ return delegate.awaitTermination(timeout, unit);
+ }
+
+ private Collection extends Callable> tasksWithCurrentSpan(
+ Collection extends Callable> tasks, Span customCurrentSpan) {
+ if (tasks == null) throw new NullPointerException("Collection of tasks is .");
+ Collection> result = new ArrayList>(tasks.size());
+ for (Callable task : tasks) result.add(callableWithCurrentSpan(task, customCurrentSpan));
+ return result;
+ }
+
+}
diff --git a/src/main/java/io/opentracing/contrib/spanmanager/concurrent/SpanPropagatingExecutors.java b/src/main/java/io/opentracing/contrib/spanmanager/concurrent/SpanPropagatingExecutors.java
new file mode 100644
index 0000000..262cb00
--- /dev/null
+++ b/src/main/java/io/opentracing/contrib/spanmanager/concurrent/SpanPropagatingExecutors.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright 2017 The OpenTracing 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
+ *
+ * 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.opentracing.contrib.spanmanager.concurrent;
+
+import io.opentracing.contrib.spanmanager.SpanManager;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Factory-methods similar to standard java {@link Executors}:
+ *
+ * - {@link #newFixedThreadPool(int, SpanManager)}
+ * - {@link #newSingleThreadExecutor(SpanManager)}
+ * - {@link #newCachedThreadPool(SpanManager)}
+ * - Variants of the above with additional {@link ThreadFactory} argument:
+ * {@link #newFixedThreadPool(int, ThreadFactory, SpanManager)},
+ * {@link #newSingleThreadExecutor(ThreadFactory, SpanManager)},
+ * {@link #newCachedThreadPool(ThreadFactory, SpanManager)}
+ *
+ *
+ *
+ * @see SpanPropagatingExecutorService
+ */
+public final class SpanPropagatingExecutors {
+
+ /**
+ * Private constructor to avoid instantiation of this utility class.
+ */
+ private SpanPropagatingExecutors() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * This method returns a {@link Executors#newFixedThreadPool(int) fixed threadpool} that propagates
+ * the current span into the started threads.
+ *
+ * @param nThreads the number of threads in the pool
+ * @param spanManager the manager for span propagation.
+ * @return the newly created thread pool
+ * @see Executors#newFixedThreadPool(int)
+ */
+ public static SpanPropagatingExecutorService newFixedThreadPool(int nThreads, SpanManager spanManager) {
+ return new SpanPropagatingExecutorService(Executors.newFixedThreadPool(nThreads), spanManager);
+ }
+
+ /**
+ * This method returns a {@link Executors#newFixedThreadPool(int, ThreadFactory) fixed threadpool} that propagates
+ * the current span into the started threads.
+ *
+ * @param nThreads the number of threads in the pool
+ * @param threadFactory the factory to use when creating new threads
+ * @param spanManager the manager for span propagation.
+ * @return the newly created thread pool
+ * @see Executors#newFixedThreadPool(int, ThreadFactory)
+ */
+ public static SpanPropagatingExecutorService newFixedThreadPool(
+ int nThreads, ThreadFactory threadFactory, SpanManager spanManager) {
+ return new SpanPropagatingExecutorService(Executors.newFixedThreadPool(nThreads, threadFactory), spanManager);
+ }
+
+ /**
+ * This method returns a {@link Executors#newSingleThreadExecutor() single-threaded executor} that propagates
+ * the current span into the started thread.
+ *
+ * @param spanManager the manager for span propagation.
+ * @return the newly created single-theaded executor
+ * @see Executors#newSingleThreadExecutor()
+ */
+ public static SpanPropagatingExecutorService newSingleThreadExecutor(SpanManager spanManager) {
+ return new SpanPropagatingExecutorService(Executors.newSingleThreadExecutor(), spanManager);
+ }
+
+ /**
+ * This method returns a {@link Executors#newSingleThreadExecutor(ThreadFactory) single-threaded executor}
+ * that propagates the current span into the started thread.
+ *
+ * @param threadFactory the factory to use when creating new threads
+ * @param spanManager the manager for span propagation.
+ * @return the newly created single-theaded executor
+ * @see Executors#newSingleThreadExecutor(ThreadFactory)
+ */
+ public static SpanPropagatingExecutorService newSingleThreadExecutor(
+ ThreadFactory threadFactory, SpanManager spanManager) {
+ return new SpanPropagatingExecutorService(Executors.newSingleThreadExecutor(threadFactory), spanManager);
+ }
+
+ /**
+ * This method returns a {@link Executors#newCachedThreadPool() cached threadpool} that propagates
+ * the current span into the started threads.
+ *
+ * @param spanManager the manager for span propagation.
+ * @return the newly created thread pool
+ * @see Executors#newCachedThreadPool()
+ */
+ public static SpanPropagatingExecutorService newCachedThreadPool(SpanManager spanManager) {
+ return new SpanPropagatingExecutorService(Executors.newCachedThreadPool(), spanManager);
+ }
+
+ /**
+ * This method returns a {@link Executors#newCachedThreadPool(ThreadFactory) cached threadpool} that propagates
+ * the current span into the started threads.
+ *
+ * @param threadFactory the factory to use when creating new threads
+ * @param spanManager the manager for span propagation.
+ * @return the newly created thread pool
+ * @see Executors#newCachedThreadPool(ThreadFactory)
+ */
+ public static SpanPropagatingExecutorService newCachedThreadPool(ThreadFactory threadFactory, SpanManager spanManager) {
+ return new SpanPropagatingExecutorService(Executors.newCachedThreadPool(threadFactory), spanManager);
+ }
+
+}
diff --git a/src/main/java/io/opentracing/contrib/spanmanager/tracer/AutoReleasingManagedSpan.java b/src/main/java/io/opentracing/contrib/spanmanager/tracer/AutoReleasingManagedSpan.java
new file mode 100644
index 0000000..eb0e465
--- /dev/null
+++ b/src/main/java/io/opentracing/contrib/spanmanager/tracer/AutoReleasingManagedSpan.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright 2017 The OpenTracing 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
+ *
+ * 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.opentracing.contrib.spanmanager.tracer;
+
+import io.opentracing.Span;
+import io.opentracing.SpanContext;
+import io.opentracing.contrib.spanmanager.SpanManager;
+
+import java.util.Map;
+
+/**
+ * A {@link Span} that automatically {@link #release() releases}
+ * when {@link #finish() finished} or {@link #close() closed}.
+ *
+ * All other methods are forwarded to the actual managed Span.
+ */
+final class AutoReleasingManagedSpan implements Span, SpanManager.ManagedSpan {
+
+ private final SpanManager.ManagedSpan managedSpan;
+
+ AutoReleasingManagedSpan(SpanManager.ManagedSpan managedSpan) {
+ if (managedSpan == null) throw new NullPointerException("Managed span was .");
+ this.managedSpan = managedSpan;
+ }
+
+ @Override
+ public Span getSpan() {
+ return managedSpan.getSpan();
+ }
+
+ /**
+ * Releases this current ManagedSpan.
+ */
+ @Override
+ public void release() {
+ managedSpan.release();
+ }
+
+ /**
+ * {@link Span#finish() Finishes} the delegate and {@link SpanManager.ManagedSpan#release() releases} this ManagedSpan.
+ */
+ @Override
+ public void finish() {
+ try {
+ getSpan().finish();
+ } finally {
+ release();
+ }
+ }
+
+ /**
+ * {@link Span#finish(long) Finishes} the delegate and {@link SpanManager.ManagedSpan#release() releases} this ManagedSpan.
+ */
+ @Override
+ public void finish(long finishMicros) {
+ try {
+ getSpan().finish(finishMicros);
+ } finally {
+ release();
+ }
+ }
+
+ /**
+ * {@link Span#close() Closes} the delegate and {@link SpanManager.ManagedSpan#release() releases} this ManagedSpan.
+ */
+ @Override
+ public void close() {
+ try {
+ getSpan().close();
+ } finally {
+ release();
+ }
+ }
+
+ // Default behaviour is forwarded to the actual managed Span:
+
+ @Override
+ public SpanContext context() {
+ return getSpan().context();
+ }
+
+ @Override
+ public Span setTag(String key, String value) {
+ getSpan().setTag(key, value);
+ return this;
+ }
+
+ @Override
+ public Span setTag(String key, boolean value) {
+ getSpan().setTag(key, value);
+ return this;
+ }
+
+ @Override
+ public Span setTag(String key, Number value) {
+ getSpan().setTag(key, value);
+ return this;
+ }
+
+ @Override
+ public Span log(Map fields) {
+ getSpan().log(fields);
+ return this;
+ }
+
+ @Override
+ public Span log(long timestampMicroseconds, Map fields) {
+ getSpan().log(timestampMicroseconds, fields);
+ return this;
+ }
+
+ @Override
+ public Span log(String event) {
+ getSpan().log(event);
+ return this;
+ }
+
+ @Override
+ public Span log(long timestampMicroseconds, String event) {
+ getSpan().log(timestampMicroseconds, event);
+ return this;
+ }
+
+ @Override
+ public Span setBaggageItem(String key, String value) {
+ getSpan().setBaggageItem(key, value);
+ return this;
+ }
+
+ @Override
+ public String getBaggageItem(String key) {
+ return getSpan().getBaggageItem(key);
+ }
+
+ @Override
+ public Span setOperationName(String operationName) {
+ getSpan().setOperationName(operationName);
+ return this;
+ }
+
+ @SuppressWarnings("deprecation") // We simply delegate this method as we're told.
+ @Override
+ public Span log(String eventName, Object payload) {
+ getSpan().log(eventName, payload);
+ return this;
+ }
+
+ @SuppressWarnings("deprecation") // We simply delegate this method as we're told.
+ @Override
+ public Span log(long timestampMicroseconds, String eventName, Object payload) {
+ getSpan().log(timestampMicroseconds, eventName, payload);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + '{' + managedSpan + '}';
+ }
+
+}
diff --git a/src/main/java/io/opentracing/contrib/spanmanager/tracer/ManagedSpanBuilder.java b/src/main/java/io/opentracing/contrib/spanmanager/tracer/ManagedSpanBuilder.java
new file mode 100644
index 0000000..b350c33
--- /dev/null
+++ b/src/main/java/io/opentracing/contrib/spanmanager/tracer/ManagedSpanBuilder.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright 2017 The OpenTracing 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
+ *
+ * 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.opentracing.contrib.spanmanager.tracer;
+
+import io.opentracing.References;
+import io.opentracing.Span;
+import io.opentracing.SpanContext;
+import io.opentracing.Tracer.SpanBuilder;
+import io.opentracing.contrib.spanmanager.SpanManager;
+
+import java.util.Map;
+
+/**
+ * {@link SpanBuilder} that automatically {@link SpanManager#manage(Span) activates} newly started spans.
+ *
+ * The activated ManagedSpan is wrapped in an {@linkplain AutoReleasingManagedSpan}
+ * to automatically release when finished.
+ * All other methods are forwarded to the delegate span builder.
+ *
+ * @see SpanManager
+ * @see AutoReleasingManagedSpan#finish()
+ */
+final class ManagedSpanBuilder implements SpanBuilder {
+
+ SpanBuilder delegate;
+ private final SpanManager spanManager;
+
+ ManagedSpanBuilder(SpanBuilder delegate, SpanManager spanManager) {
+ if (delegate == null) throw new NullPointerException("Delegate SpanBuilder was .");
+ if (spanManager == null) throw new NullPointerException("Span manager was .");
+ this.delegate = delegate;
+ this.spanManager = spanManager;
+ }
+
+ /**
+ * Replaces the {@link #delegate} SpanBuilder by a delegated-method result.
+ *
+ * @param spanBuilder The builder returned from the delegate (normally '== delegate').
+ * @return This re-wrapped ManagedSpanBuilder.
+ */
+ SpanBuilder rewrap(SpanBuilder spanBuilder) {
+ if (spanBuilder != null) {
+ this.delegate = spanBuilder;
+ }
+ return this;
+ }
+
+ /**
+ * Starts the built Span and {@link SpanManager#manage(Span) activates} it.
+ *
+ * @return a new 'current' Span that releases itself upon finish or close calls.
+ * @see SpanManager#manage(Span)
+ * @see AutoReleasingManagedSpan#release()
+ */
+ @Override
+ public Span start() {
+ return new AutoReleasingManagedSpan(spanManager.manage(delegate.start()));
+ }
+
+ // All other methods are forwarded to the delegate SpanBuilder.
+
+ public SpanBuilder asChildOf(SpanContext parent) {
+ return addReference(References.CHILD_OF, parent);
+ }
+
+ public SpanBuilder asChildOf(Span parent) {
+ return addReference(References.CHILD_OF, parent.context());
+ }
+
+ public SpanBuilder addReference(String referenceType, SpanContext context) {
+ if (context instanceof ManagedSpanBuilder) { // Weird that SpanBuilder extends Context!
+ context = ((ManagedSpanBuilder) context).delegate;
+ }
+ return rewrap(delegate.addReference(referenceType, context));
+ }
+
+ public SpanBuilder withTag(String key, String value) {
+ return rewrap(delegate.withTag(key, value));
+ }
+
+ public SpanBuilder withTag(String key, boolean value) {
+ return rewrap(delegate.withTag(key, value));
+ }
+
+ public SpanBuilder withTag(String key, Number value) {
+ return rewrap(delegate.withTag(key, value));
+ }
+
+ public SpanBuilder withStartTimestamp(long microseconds) {
+ return rewrap(delegate.withStartTimestamp(microseconds));
+ }
+
+ public Iterable> baggageItems() {
+ return delegate.baggageItems();
+ }
+
+}
diff --git a/src/main/java/io/opentracing/contrib/spanmanager/tracer/ManagedSpanTracer.java b/src/main/java/io/opentracing/contrib/spanmanager/tracer/ManagedSpanTracer.java
new file mode 100644
index 0000000..49f964a
--- /dev/null
+++ b/src/main/java/io/opentracing/contrib/spanmanager/tracer/ManagedSpanTracer.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2017 The OpenTracing 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
+ *
+ * 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.opentracing.contrib.spanmanager.tracer;
+
+import io.opentracing.Span;
+import io.opentracing.SpanContext;
+import io.opentracing.Tracer;
+import io.opentracing.contrib.spanmanager.SpanManager;
+import io.opentracing.propagation.Format;
+
+/**
+ * Convenience {@link Tracer} that automates managing the {@linkplain SpanManager#currentSpan() current span}:
+ *
+ * - It is a wrapper that forwards all calls to another {@link Tracer} implementation.
+ * - {@linkplain Span Spans} created with this tracer are
+ * automatically {@link SpanManager#manage(Span) managed} when started,
+ * - and automatically {@link SpanManager.ManagedSpan#release() released} when they finish.
+ *
+ *
+ * Implementation note: This {@link Tracer} wraps the {@linkplain SpanBuilder} and {@linkplain Span}
+ * in {@linkplain ManagedSpanBuilder} and {@linkplain AutoReleasingManagedSpan} respectively
+ * because no {@link Span} lifecycle callbacks are available in the opentracing API.
+ * If there were, the {@linkplain ManagedSpanTracer} could be a lot simpler.
+ * However, lifecycle callbacks in the API form a considerable performance risk.
+ *
+ * @see SpanManager
+ */
+public final class ManagedSpanTracer implements Tracer {
+
+ private final Tracer delegate;
+ private final SpanManager spanManager;
+
+ /**
+ * Automatically manages created spans from delegate using the the specified {@link SpanManager}.
+ *
+ * @param delegate The tracer to be wrapped.
+ * @param spanManager The manager providing span propagation.
+ */
+ public ManagedSpanTracer(Tracer delegate, SpanManager spanManager) {
+ if (delegate == null) throw new NullPointerException("Delegate Tracer is .");
+ if (spanManager == null) throw new NullPointerException("Span manager is .");
+ this.delegate = delegate;
+ this.spanManager = spanManager;
+ }
+
+ public void inject(SpanContext spanContext, Format format, C carrier) {
+ if (spanContext instanceof ManagedSpanBuilder) { // Weird that SpanBuilder extends Context!
+ spanContext = ((ManagedSpanBuilder) spanContext).delegate;
+ }
+ delegate.inject(spanContext, format, carrier);
+ }
+
+ public SpanContext extract(Format format, C carrier) {
+ return delegate.extract(format, carrier);
+ }
+
+ public SpanBuilder buildSpan(String operationName) {
+ return new ManagedSpanBuilder(delegate.buildSpan(operationName), spanManager);
+ }
+
+ @Override
+ public String toString() {
+ return "ManagedSpanTracer{" + delegate + '}';
+ }
+
+}
diff --git a/src/test/java/io/opentracing/contrib/spanmanager/DefaultSpanManagerTest.java b/src/test/java/io/opentracing/contrib/spanmanager/DefaultSpanManagerTest.java
new file mode 100644
index 0000000..e6506a0
--- /dev/null
+++ b/src/test/java/io/opentracing/contrib/spanmanager/DefaultSpanManagerTest.java
@@ -0,0 +1,192 @@
+/**
+ * Copyright 2017 The OpenTracing 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
+ *
+ * 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.opentracing.contrib.spanmanager;
+
+import io.opentracing.NoopSpan;
+import io.opentracing.Span;
+import io.opentracing.contrib.spanmanager.SpanManager.ManagedSpan;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.mock;
+
+public class DefaultSpanManagerTest {
+
+ SpanManager manager;
+
+ @Before
+ public void resetManager() {
+ manager = DefaultSpanManager.getInstance();
+ manager.clear();
+ }
+
+ @Test
+ public void testBasicStackBehaviour() {
+ Span span1 = mock(Span.class);
+ Span span2 = mock(Span.class);
+ Span span3 = mock(Span.class);
+
+ assertThat("empty stack", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+
+ ManagedSpan managed1 = manager.manage(span1);
+ assertThat("pushed span1", manager.currentSpan(), is(sameInstance(span1)));
+
+ ManagedSpan managed2 = manager.manage(span2);
+ assertThat("pushed span2", manager.currentSpan(), is(sameInstance(span2)));
+
+ ManagedSpan managed3 = manager.manage(span3);
+ assertThat("pushed span3", manager.currentSpan(), is(sameInstance(span3)));
+
+ managed3.release();
+ assertThat("popped span3", manager.currentSpan(), is(sameInstance(span2)));
+
+ managed2.release();
+ assertThat("popped span2", manager.currentSpan(), is(sameInstance(span1)));
+
+ managed1.release();
+ assertThat("popped span1", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+ }
+
+ @Test
+ public void testMultipleReleases() {
+ Span span1 = mock(Span.class);
+ Span span2 = mock(Span.class);
+
+ assertThat("empty stack", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+
+ ManagedSpan managed1 = manager.manage(span1);
+ assertThat("pushed span1", manager.currentSpan(), is(sameInstance(span1)));
+
+ ManagedSpan managed2 = manager.manage(span2);
+ assertThat("pushed span2", manager.currentSpan(), is(sameInstance(span2)));
+
+ managed2.release();
+ managed2.release();
+ assertThat("popped span2", manager.currentSpan(), is(sameInstance(span1)));
+
+ managed1.release();
+ managed2.release();
+ managed1.release();
+ managed2.release();
+ assertThat("popped span1", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+ }
+
+
+ @Test
+ public void testTemporaryNoSpan() {
+ Span span1 = mock(Span.class);
+ Span span2 = null;
+ Span span3 = mock(Span.class);
+
+ assertThat("empty stack", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+
+ ManagedSpan managed1 = manager.manage(span1);
+ assertThat("pushed span1", manager.currentSpan(), is(sameInstance(span1)));
+
+ ManagedSpan managed2 = manager.manage(span2);
+ assertThat("pushed span2", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+
+ ManagedSpan managed3 = manager.manage(span3);
+ assertThat("pushed span3", manager.currentSpan(), is(sameInstance(span3)));
+
+ managed3.release();
+ assertThat("popped span3", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+
+ managed2.release();
+ assertThat("popped span2", manager.currentSpan(), is(sameInstance(span1)));
+
+ managed1.release();
+ assertThat("popped span1", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+ }
+
+ @Test
+ public void testOutOfOrderRelease() {
+ Span span1 = mock(Span.class);
+ Span span2 = mock(Span.class);
+ Span span3 = mock(Span.class);
+
+ assertThat("empty stack", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+
+ ManagedSpan managed1 = manager.manage(span1);
+ assertThat("pushed span1", manager.currentSpan(), is(sameInstance(span1)));
+
+ ManagedSpan managed2 = manager.manage(span2);
+ assertThat("pushed span2", manager.currentSpan(), is(sameInstance(span2)));
+
+ ManagedSpan managed3 = manager.manage(span3);
+ assertThat("pushed span3", manager.currentSpan(), is(sameInstance(span3)));
+
+ // Pop2: Span1 -> Span2(X) -> Span3 : currentSpan stays Span3
+ managed2.release();
+ assertThat("released span2", manager.currentSpan(), is(sameInstance(span3)));
+
+ managed3.release();
+ assertThat("skipped span2 (already-released)", manager.currentSpan(), is(sameInstance(span1)));
+
+ managed1.release();
+ assertThat("popped span1", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+ }
+
+ /**
+ * Note: This is not a normal use-case!
+ * The {@link ManagedSpan} is intended to be created and used in the scope of a try-with-resources block
+ * (so within the scope of a single thread).
+ *
+ * This test is merely here to guarantee predictable behaviour when it happens.
+ */
+ @Test
+ public void testReleaseFromOtherThreads() throws InterruptedException {
+ Span span1 = mock(Span.class);
+ Span span2 = mock(Span.class);
+ Span span3 = mock(Span.class);
+
+ assertThat("empty stack", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+
+ ManagedSpan managed1 = manager.manage(span1);
+ assertThat("pushed span1", manager.currentSpan(), is(sameInstance(span1)));
+
+ final ManagedSpan managed2 = manager.manage(span2);
+ assertThat("pushed span2", manager.currentSpan(), is(sameInstance(span2)));
+
+ ManagedSpan managed3 = manager.manage(span3);
+ assertThat("pushed span3", manager.currentSpan(), is(sameInstance(span3)));
+
+ // Schedule 10 threads to release managed2
+ Thread[] releasers = new Thread[10];
+ for (int i = 0; i < releasers.length; i++) {
+ releasers[i] = new Thread() {
+ @Override
+ public void run() {
+ managed2.release();
+ }
+ };
+ }
+
+ // Schedule managed2.release() 10x
+ for (int i = 0; i < releasers.length; i++) releasers[i].start();
+
+ managed3.release();
+
+ // Wait for managed2.releases
+ for (int i = 0; i < releasers.length; i++) releasers[i].join();
+
+ assertThat("popped span2+3", manager.currentSpan(), is(sameInstance(span1)));
+
+ managed1.release();
+ assertThat("popped span1", manager.currentSpan(), is(instanceOf(NoopSpan.class)));
+ }
+
+}
diff --git a/src/test/java/io/opentracing/contrib/spanmanager/concurrent/SpanPropagatingExecutorServiceMockTest.java b/src/test/java/io/opentracing/contrib/spanmanager/concurrent/SpanPropagatingExecutorServiceMockTest.java
new file mode 100644
index 0000000..ec0cb6b
--- /dev/null
+++ b/src/test/java/io/opentracing/contrib/spanmanager/concurrent/SpanPropagatingExecutorServiceMockTest.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright 2017 The OpenTracing 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
+ *
+ * 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.opentracing.contrib.spanmanager.concurrent;
+
+import io.opentracing.Span;
+import io.opentracing.contrib.spanmanager.SpanManager;
+import org.hamcrest.Matchers;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.*;
+
+public class SpanPropagatingExecutorServiceMockTest {
+
+ ExecutorService mockExecutorService;
+ SpanManager mockSpanManager;
+ Span mockSpan;
+
+ SpanPropagatingExecutorService service;
+
+ @Before
+ public void setUp() {
+ mockExecutorService = mock(ExecutorService.class);
+ mockSpanManager = mock(SpanManager.class);
+ mockSpan = mock(Span.class);
+ when(mockSpanManager.currentSpan()).thenReturn(mockSpan);
+
+ service = new SpanPropagatingExecutorService(mockExecutorService, mockSpanManager);
+ }
+
+ @After
+ public void verifyMocks() {
+ verifyNoMoreInteractions(mockExecutorService, mockSpanManager, mockSpan);
+ }
+
+ @Test
+ public void testExecuteRunnable() {
+ Runnable runnable = mock(Runnable.class);
+
+ service.execute(runnable);
+
+ verify(mockSpanManager).currentSpan(); // current span must be propagated
+ verify(mockExecutorService).execute(any(RunnableWithManagedSpan.class)); // into RunnableWithManagedSpan
+ }
+
+ @Test
+ public void testSubmitRunnable() {
+ Runnable runnable = mock(Runnable.class);
+ Future future = mock(Future.class);
+ when(mockExecutorService.submit(any(Runnable.class))).thenReturn(future);
+
+ assertThat(service.submit(runnable), is(sameInstance(future)));
+
+ verify(mockSpanManager).currentSpan(); // current span must be propagated
+ verify(mockExecutorService).submit(any(RunnableWithManagedSpan.class)); // into RunnableWithManagedSpan
+ }
+
+ @Test
+ public void testSubmitCallable() {
+ Callable callable = mock(Callable.class);
+ Future future = mock(Future.class);
+ when(mockExecutorService.submit(any(Callable.class))).thenReturn(future);
+
+ assertThat(service.submit(callable), is(sameInstance(future)));
+
+ verify(mockSpanManager).currentSpan(); // current span must be propagated
+ verify(mockExecutorService).submit(any(CallableWithManagedSpan.class)); // into CallableWithManagedSpan
+ }
+
+ @Test
+ public void testInvokeAll() throws InterruptedException {
+ Collection> callables = Arrays.>asList(mock(Callable.class), mock(Callable.class));
+ List> futures = mock(List.class);
+ when(mockExecutorService.invokeAll((Collection>) anyCollection())).thenReturn(futures);
+
+ assertThat(service.invokeAll(callables), is(sameInstance(futures)));
+
+ verify(mockSpanManager, times(1)).currentSpan(); // current span must be obtained once
+ verify(mockExecutorService).invokeAll(anyCollection());
+ }
+
+ @Test
+ public void testConstructorWithoutDelegate() {
+ try {
+ new SpanPropagatingExecutorService(null, mock(SpanManager.class));
+ fail("Exception with message expected.");
+ } catch (RuntimeException expected) {
+ assertThat(expected.getMessage(), is(Matchers.notNullValue()));
+ }
+ }
+
+ @Test
+ public void testConstructorWithoutSpanManager() {
+ try {
+ new SpanPropagatingExecutorService(mock(ExecutorService.class), null);
+ fail("Exception with message expected.");
+ } catch (RuntimeException expected) {
+ assertThat(expected.getMessage(), is(Matchers.notNullValue()));
+ }
+ }
+
+}
diff --git a/src/test/java/io/opentracing/contrib/spanmanager/concurrent/SpanPropagatingExecutorServiceTest.java b/src/test/java/io/opentracing/contrib/spanmanager/concurrent/SpanPropagatingExecutorServiceTest.java
new file mode 100644
index 0000000..54d0141
--- /dev/null
+++ b/src/test/java/io/opentracing/contrib/spanmanager/concurrent/SpanPropagatingExecutorServiceTest.java
@@ -0,0 +1,164 @@
+/**
+ * Copyright 2017 The OpenTracing 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
+ *
+ * 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.opentracing.contrib.spanmanager.concurrent;
+
+import io.opentracing.NoopSpan;
+import io.opentracing.Span;
+import io.opentracing.contrib.spanmanager.DefaultSpanManager;
+import io.opentracing.contrib.spanmanager.SpanManager;
+import io.opentracing.contrib.spanmanager.SpanManager.ManagedSpan;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.*;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.mock;
+
+public class SpanPropagatingExecutorServiceTest {
+
+ static final ExecutorService threadpool = Executors.newCachedThreadPool();
+ static final SpanManager spanManager = DefaultSpanManager.getInstance();
+
+ SpanPropagatingExecutorService subject;
+
+ @Before
+ public void setUp() {
+ subject = new SpanPropagatingExecutorService(threadpool, spanManager);
+ spanManager.clear();
+ }
+
+ @After
+ public void tearDown() {
+ spanManager.clear();
+ }
+
+ @AfterClass
+ public static void shutdownThreadpool() {
+ assertThat(threadpool.shutdownNow(), equalTo(Collections.emptyList()));
+ }
+
+ @Test
+ public void testExecuteRunnable() throws ExecutionException, InterruptedException {
+ CurrentSpanRunnable runnable = new CurrentSpanRunnable();
+ ManagedSpan callerManagedSpan = spanManager.manage(mock(Span.class));
+ try {
+
+ // Execute runnable and wait for completion.
+ FutureTask future = new FutureTask(runnable, null);
+ subject.execute(future);
+ future.get();
+
+ assertThat("Current span in thread", runnable.span, is(sameInstance(callerManagedSpan.getSpan())));
+ } finally {
+ callerManagedSpan.release();
+ }
+ }
+
+ @Test
+ public void testSubmitRunnable() throws ExecutionException, InterruptedException {
+ CurrentSpanRunnable runnable = new CurrentSpanRunnable();
+ ManagedSpan callerManagedSpan = spanManager.manage(mock(Span.class));
+ try {
+
+ subject.submit(runnable).get(); // submit and block.
+ assertThat("Current span in thread", runnable.span, is(sameInstance(callerManagedSpan.getSpan())));
+
+ } finally {
+ callerManagedSpan.release();
+ }
+ }
+
+ @Test
+ public void testSubmitCallable() throws ExecutionException, InterruptedException {
+ Callable callable = new CurrentSpanCallable();
+ ManagedSpan callerManagedSpan = spanManager.manage(mock(Span.class));
+ try {
+
+ Future threadSpan = subject.submit(callable);
+ assertThat("Current span in thread", threadSpan.get(), is(sameInstance(callerManagedSpan.getSpan())));
+
+ } finally {
+ callerManagedSpan.release();
+ }
+ }
+
+ @Test
+ public void testInvokeAll() throws ExecutionException, InterruptedException {
+ Collection> callables = Arrays.>asList(
+ new CurrentSpanCallable(), new CurrentSpanCallable(), new CurrentSpanCallable());
+ ManagedSpan callerManagedSpan = spanManager.manage(mock(Span.class));
+ try {
+
+ List> futures = subject.invokeAll(callables);
+ assertThat("Futures", futures, hasSize(equalTo(callables.size())));
+ for (Future threadSpan : futures) {
+ assertThat("Current span in thread", threadSpan.get(), is(sameInstance(callerManagedSpan.getSpan())));
+ }
+
+ } finally {
+ callerManagedSpan.release();
+ }
+ }
+
+
+ @Test
+ public void testExecuteRunnableWithoutCurrentSpan() throws ExecutionException, InterruptedException {
+ CurrentSpanRunnable runnable = new CurrentSpanRunnable();
+
+ // Execute runnable and wait for completion.
+ FutureTask future = new FutureTask(runnable, null);
+ subject.execute(future);
+ future.get();
+
+ assertThat("Current span in thread", runnable.span, allOf(notNullValue(), instanceOf(NoopSpan.class)));
+ }
+
+ @Test
+ public void testSubmitRunnableWithoutCurrentSpan() throws ExecutionException, InterruptedException {
+ CurrentSpanRunnable runnable = new CurrentSpanRunnable();
+ subject.submit(runnable).get(); // submit and block.
+ assertThat("Current span in thread", runnable.span, allOf(notNullValue(), instanceOf(NoopSpan.class)));
+ }
+
+ @Test
+ public void testSubmitCallableWithoutCurrentSpan() throws ExecutionException, InterruptedException {
+ Future threadSpan = subject.submit(new CurrentSpanCallable());
+ assertThat("Current span in thread", threadSpan.get(), allOf(notNullValue(), instanceOf(NoopSpan.class)));
+ }
+
+ static class CurrentSpanRunnable implements Runnable {
+ Span span = null;
+
+ @Override
+ public void run() {
+ span = spanManager.currentSpan();
+ }
+ }
+
+ static class CurrentSpanCallable implements Callable {
+ @Override
+ public Span call() {
+ return spanManager.currentSpan();
+ }
+ }
+
+}
diff --git a/travis/publish.sh b/travis/publish.sh
new file mode 100755
index 0000000..b6ef300
--- /dev/null
+++ b/travis/publish.sh
@@ -0,0 +1,131 @@
+#
+# Copyright 2017 The OpenTracing 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
+#
+# 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.
+#
+
+set -euo pipefail
+set -x
+
+build_started_by_tag() {
+ if [ "${TRAVIS_TAG}" == "" ]; then
+ echo "[Publishing] This build was not started by a tag, publishing snapshot"
+ return 1
+ else
+ echo "[Publishing] This build was started by the tag ${TRAVIS_TAG}, publishing release"
+ return 0
+ fi
+}
+
+is_pull_request() {
+ if [ "${TRAVIS_PULL_REQUEST}" != "false" ]; then
+ echo "[Not Publishing] This is a Pull Request"
+ return 0
+ else
+ echo "[Publishing] This is not a Pull Request"
+ return 1
+ fi
+}
+
+is_travis_branch_master() {
+ if [ "${TRAVIS_BRANCH}" = master ]; then
+ echo "[Publishing] Travis branch is master"
+ return 0
+ else
+ echo "[Not Publishing] Travis branch is not master"
+ return 1
+ fi
+}
+
+check_travis_branch_equals_travis_tag() {
+ #Weird comparison comparing branch to tag because when you 'git push --tags'
+ #the branch somehow becomes the tag value
+ #github issue: https://github.com/travis-ci/travis-ci/issues/1675
+ if [ "${TRAVIS_BRANCH}" != "${TRAVIS_TAG}" ]; then
+ echo "Travis branch does not equal Travis tag, which it should, bailing out."
+ echo " github issue: https://github.com/travis-ci/travis-ci/issues/1675"
+ exit 1
+ else
+ echo "[Publishing] Branch (${TRAVIS_BRANCH}) same as Tag (${TRAVIS_TAG})"
+ fi
+}
+
+check_release_tag() {
+ tag="${TRAVIS_TAG}"
+ if [[ "$tag" =~ ^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+$ ]]; then
+ echo "Build started by version tag $tag. During the release process tags like this"
+ echo "are created by the 'release' Maven plugin. Nothing to do here."
+ exit 0
+ elif [[ ! "$tag" =~ ^release-[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+$ ]]; then
+ echo "You must specify a tag of the format 'release-0.0.0' to release this project."
+ echo "The provided tag ${tag} doesn't match that. Aborting."
+ exit 1
+ fi
+}
+
+is_release_commit() {
+ project_version=$(./mvnw help:evaluate -N -Dexpression=project.version|grep -v '\[')
+ if [[ "$project_version" =~ ^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+$ ]]; then
+ echo "Build started by release commit $project_version. Will synchronize to maven central."
+ return 0
+ else
+ return 1
+ fi
+}
+
+release_version() {
+ echo "${TRAVIS_TAG}" | sed 's/^release-//'
+}
+
+safe_checkout_master() {
+ # We need to be on a branch for release:perform to be able to create commits, and we want that branch to be master.
+ # But we also want to make sure that we build and release exactly the tagged version, so we verify that the remote
+ # master is where our tag is.
+ git checkout -B master
+ git fetch origin master:origin/master
+ commit_local_master="$(git show --pretty='format:%H' master)"
+ commit_remote_master="$(git show --pretty='format:%H' origin/master)"
+ if [ "$commit_local_master" != "$commit_remote_master" ]; then
+ echo "Master on remote 'origin' has commits since the version under release, aborting"
+ exit 1
+ fi
+}
+
+#----------------------
+# MAIN
+#----------------------
+
+if ! is_pull_request && build_started_by_tag; then
+ check_travis_branch_equals_travis_tag
+ check_release_tag
+fi
+
+./mvnw install -nsu
+
+# If we are on a pull request, our only job is to run tests, which happened above via ./mvnw install
+if is_pull_request; then
+ true
+# If we are on master, we will deploy the latest snapshot or release version
+# - If a release commit fails to deploy for a transient reason, delete the broken version from bintray and click rebuild
+elif is_travis_branch_master; then
+ ./mvnw --batch-mode -s ./.settings.xml -Prelease -nsu -DskipTests deploy
+
+ # If the deployment succeeded, sync it to Maven Central. Note: this needs to be done once per project, not module, hence -N
+ if is_release_commit; then
+ ./mvnw --batch-mode -s ./.settings.xml -nsu -N io.zipkin.centralsync-maven-plugin:centralsync-maven-plugin:sync
+ fi
+
+# If we are on a release tag, the following will update any version references and push a version tag for deployment.
+elif build_started_by_tag; then
+ safe_checkout_master
+ ./mvnw --batch-mode -s ./.settings.xml -Prelease -nsu -DreleaseVersion="$(release_version)" -Darguments="-DskipTests" release:prepare
+fi
+