diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Constants.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Constants.java index f796e0b671..b1081c90f1 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Constants.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Constants.java @@ -39,6 +39,8 @@ public final class Constants { "org.msgpack", "com.fasterxml.jackson", "org.yaml.snakeyaml", + // disruptor + "com.lmax.disruptor", // okHttp "okhttp3", "okio", diff --git a/dd-trace-ot/dd-trace-ot.gradle b/dd-trace-ot/dd-trace-ot.gradle index 76f49be48c..c4ace1382b 100644 --- a/dd-trace-ot/dd-trace-ot.gradle +++ b/dd-trace-ot/dd-trace-ot.gradle @@ -37,6 +37,7 @@ dependencies { compile group: 'org.msgpack', name: 'jackson-dataformat-msgpack', version: '0.8.16' compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.11.0' // Last version to support Java7 compile group: 'com.github.jnr', name: 'jnr-unixsocket', version: '0.22' + compile group: 'com.lmax', name: 'disruptor', version: '3.4.2' // We have autoservices defined in test subtree, looks like we need this to be able to properly rebuild this testAnnotationProcessor deps.autoservice diff --git a/dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java b/dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java index ef821856e3..856a738e5d 100644 --- a/dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java +++ b/dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java @@ -40,7 +40,6 @@ import java.util.SortedSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicInteger; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -89,8 +88,6 @@ public class DDTracer implements io.opentracing.Tracer, Closeable, datadog.trace private final DatadogHttpCodec.Injector injector; private final DatadogHttpCodec.Extractor extractor; - private final AtomicInteger traceCount; - /** By default, report to local agent and collect all traces. */ public DDTracer() { this(Config.get()); @@ -240,12 +237,9 @@ public class DDTracer implements io.opentracing.Tracer, Closeable, datadog.trace if (this.writer instanceof DDAgentWriter) { final DDApi api = ((DDAgentWriter) this.writer).getApi(); - traceCount = api.getTraceCounter(); if (sampler instanceof DDApi.ResponseListener) { api.addResponseListener((DDApi.ResponseListener) this.sampler); } - } else { - traceCount = new AtomicInteger(0); } registerClassLoader(ClassLoader.getSystemClassLoader()); @@ -385,7 +379,7 @@ public class DDTracer implements io.opentracing.Tracer, Closeable, datadog.trace /** Increment the reported trace count, but do not write a trace. */ void incrementTraceCount() { - traceCount.incrementAndGet(); + writer.incrementTraceCount(); } @Override diff --git a/dd-trace-ot/src/main/java/datadog/trace/common/util/DaemonThreadFactory.java b/dd-trace-ot/src/main/java/datadog/trace/common/util/DaemonThreadFactory.java new file mode 100644 index 0000000000..25d08887ce --- /dev/null +++ b/dd-trace-ot/src/main/java/datadog/trace/common/util/DaemonThreadFactory.java @@ -0,0 +1,24 @@ +package datadog.trace.common.util; + +import java.util.concurrent.ThreadFactory; + +/** A {@link ThreadFactory} implementation that starts all {@link Thread} as daemons. */ +public final class DaemonThreadFactory implements ThreadFactory { + private final String threadName; + + /** + * Constructs a new {@code DaemonThreadFactory}. + * + * @param threadName used to prefix all thread names. + */ + public DaemonThreadFactory(final String threadName) { + this.threadName = threadName; + } + + @Override + public Thread newThread(final Runnable r) { + final Thread thread = new Thread(r, threadName); + thread.setDaemon(true); + return thread; + } +} diff --git a/dd-trace-ot/src/main/java/datadog/trace/common/writer/DDAgentWriter.java b/dd-trace-ot/src/main/java/datadog/trace/common/writer/DDAgentWriter.java index a8c6e070ec..5a02cf5c36 100644 --- a/dd-trace-ot/src/main/java/datadog/trace/common/writer/DDAgentWriter.java +++ b/dd-trace-ot/src/main/java/datadog/trace/common/writer/DDAgentWriter.java @@ -3,103 +3,154 @@ package datadog.trace.common.writer; import static datadog.trace.api.Config.DEFAULT_AGENT_HOST; import static datadog.trace.api.Config.DEFAULT_AGENT_UNIX_DOMAIN_SOCKET; import static datadog.trace.api.Config.DEFAULT_TRACE_AGENT_PORT; +import static java.util.concurrent.TimeUnit.SECONDS; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.lmax.disruptor.EventFactory; +import com.lmax.disruptor.EventHandler; +import com.lmax.disruptor.EventTranslator; +import com.lmax.disruptor.EventTranslatorOneArg; +import com.lmax.disruptor.SleepingWaitStrategy; +import com.lmax.disruptor.dsl.Disruptor; +import com.lmax.disruptor.dsl.ProducerType; import datadog.opentracing.DDSpan; +import datadog.trace.common.util.DaemonThreadFactory; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; +import java.util.concurrent.Phaser; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import lombok.extern.slf4j.Slf4j; /** - * This writer write provided traces to the a DD agent which is most of time located on the same - * host. + * This writer buffers traces and sends them to the provided DDApi instance. * - *
- * - *
It handles writes asynchronuously so the calling threads are automatically released. However, - * if too much spans are collected the writers can reach a state where it is forced to drop incoming - * spans. + *
Written traces are passed off to a disruptor so as to avoid blocking the application's thread.
+ * If a flood of traces arrives that exceeds the disruptor ring size, the traces exceeding the
+ * threshold will be counted and sampled.
*/
@Slf4j
public class DDAgentWriter implements Writer {
+ private static final int DISRUPTOR_BUFFER_SIZE = 8192;
+ private static final int FLUSH_PAYLOAD_BYTES = 5_000_000; // 5 MB
+ private static final int FLUSH_PAYLOAD_DELAY = 1; // 1/second
- /** Maximum number of traces kept in memory */
- static final int DEFAULT_MAX_TRACES = 7000;
-
- /** Flush interval for the API in seconds */
- static final long FLUSH_TIME_SECONDS = 1;
-
- private final ThreadFactory agentWriterThreadFactory =
- new ThreadFactory() {
+ private static final EventTranslatorOneArg
- *
- *
- *
- *
- *
- * This class implements a specific behavior when it's full. Each new item added will replace an
- * exisiting one, at a random place/index. The class is backed by an ArrayList in order to perform
- * efficient random remove.
- *
- * @param > event, final long sequence, final List
> event, final long sequence) {
+ event.shouldFlush = true;
}
};
- /** Scheduled thread pool, acting like a cron */
- private final ScheduledExecutorService scheduledExecutor =
- Executors.newScheduledThreadPool(1, agentWriterThreadFactory);
+ private static final ThreadFactory DISRUPTOR_THREAD_FACTORY =
+ new DaemonThreadFactory("dd-trace-disruptor");
+ private static final ThreadFactory SCHEDULED_FLUSH_THREAD_FACTORY =
+ new DaemonThreadFactory("dd-trace-writer");
- /** The DD agent api */
private final DDApi api;
-
- /** In memory collection of traces waiting for departure */
- private final WriterQueue
> traces;
-
- private boolean queueFullReported = false;
+ private final int flushFrequencySeconds;
+ private final Disruptor
>(DEFAULT_MAX_TRACES));
+ this(api, DISRUPTOR_BUFFER_SIZE, FLUSH_PAYLOAD_DELAY);
}
- public DDAgentWriter(final DDApi api, final WriterQueue
> queue) {
- super();
- this.api = api;
- traces = queue;
- }
-
- /* (non-Javadoc)
- * @see datadog.trace.Writer#write(java.util.List)
+ /**
+ * Used in the tests.
+ *
+ * @param api
+ * @param disruptorSize Rounded up to next power of 2
+ * @param flushFrequencySeconds value < 1 disables scheduled flushes
*/
+ private DDAgentWriter(final DDApi api, final int disruptorSize, final int flushFrequencySeconds) {
+ this.api = api;
+ this.flushFrequencySeconds = flushFrequencySeconds;
+ disruptor =
+ new Disruptor<>(
+ new DisruptorEventFactory
>(),
+ Math.max(2, Integer.highestOneBit(disruptorSize - 1) << 1), // Next power of 2
+ DISRUPTOR_THREAD_FACTORY,
+ ProducerType.MULTI,
+ new SleepingWaitStrategy(0, TimeUnit.MILLISECONDS.toNanos(5)));
+ disruptor.handleEventsWith(new TraceConsumer());
+ scheduledWriterExecutor = Executors.newScheduledThreadPool(1, SCHEDULED_FLUSH_THREAD_FACTORY);
+ apiPhaser = new Phaser(); // Ensure API calls are completed when flushing
+ apiPhaser.register(); // Register on behalf of the scheduled executor thread.
+ }
+
@Override
public void write(final List
> payload = traces.getAll();
-
- if (log.isDebugEnabled()) {
- int nbSpans = 0;
- for (final List> trace : payload) {
- nbSpans += trace.size();
- }
- log.debug("Sending {} traces ({} spans) to the API (async)", payload.size(), nbSpans);
- }
-
- final boolean isSent = api.sendTraces(payload);
- if (isSent) {
- log.debug("Successfully sent {} traces to the API", payload.size());
- } else {
- log.debug("Failed to send {} traces to the API", payload.size());
- }
- } catch (final Throwable e) {
- log.debug("Failed to send traces to the API: {}", e.getMessage());
+ private void scheduleFlush() {
+ if (flushFrequencySeconds > 0) {
+ final ScheduledFuture> previous =
+ flushSchedule.getAndSet(
+ scheduledWriterExecutor.schedule(flushTask, flushFrequencySeconds, SECONDS));
+ if (previous != null) {
+ previous.cancel(true);
}
}
}
+
+ private final Runnable flushTask = new FlushTask();
+
+ private class FlushTask implements Runnable {
+ @Override
+ public void run() {
+ // Don't call flush() because it would block the thread also used for sending the traces.
+ disruptor.publishEvent(FLUSH_TRANSLATOR);
+ }
+ }
+
+ /** This class is intentionally not threadsafe. */
+ private class TraceConsumer implements EventHandler
> event, final long sequence, final boolean endOfBatch) {
+ final List
> traces) {
- final int totalSize = traceCount.getAndSet(0);
+ final List
> implements Wr
}
}
+ @Override
+ public void incrementTraceCount() {}
+
@Override
public void start() {
close();
@@ -64,6 +67,6 @@ public class ListWriter extends CopyOnWriteArrayList
> implements Wr
@Override
public String toString() {
- return "ListWriter { size=" + this.size() + " }";
+ return "ListWriter { size=" + size() + " }";
}
}
diff --git a/dd-trace-ot/src/main/java/datadog/trace/common/writer/LoggingWriter.java b/dd-trace-ot/src/main/java/datadog/trace/common/writer/LoggingWriter.java
index 0da4839d1d..cb95d07a72 100644
--- a/dd-trace-ot/src/main/java/datadog/trace/common/writer/LoggingWriter.java
+++ b/dd-trace-ot/src/main/java/datadog/trace/common/writer/LoggingWriter.java
@@ -20,6 +20,11 @@ public class LoggingWriter implements Writer {
}
}
+ @Override
+ public void incrementTraceCount() {
+ log.info("incrementTraceCount()");
+ }
+
@Override
public void close() {
log.info("close()");
diff --git a/dd-trace-ot/src/main/java/datadog/trace/common/writer/Writer.java b/dd-trace-ot/src/main/java/datadog/trace/common/writer/Writer.java
index 324e289da5..306074ae3e 100644
--- a/dd-trace-ot/src/main/java/datadog/trace/common/writer/Writer.java
+++ b/dd-trace-ot/src/main/java/datadog/trace/common/writer/Writer.java
@@ -2,12 +2,13 @@ package datadog.trace.common.writer;
import datadog.opentracing.DDSpan;
import datadog.trace.api.Config;
+import java.io.Closeable;
import java.util.List;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
/** A writer is responsible to send collected spans to some place */
-public interface Writer {
+public interface Writer extends Closeable {
/**
* Write a trace represented by the entire list of all the finished spans
@@ -23,8 +24,12 @@ public interface Writer {
* Indicates to the writer that no future writing will come and it should terminates all
* connections and tasks
*/
+ @Override
void close();
+ /** Count that a trace was captured for stats, but without reporting it. */
+ void incrementTraceCount();
+
@Slf4j
final class Builder {
diff --git a/dd-trace-ot/src/main/java/datadog/trace/common/writer/WriterQueue.java b/dd-trace-ot/src/main/java/datadog/trace/common/writer/WriterQueue.java
deleted file mode 100644
index 1ead96a243..0000000000
--- a/dd-trace-ot/src/main/java/datadog/trace/common/writer/WriterQueue.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package datadog.trace.common.writer;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ThreadLocalRandom;
-
-/**
- * A bounded queue implementation compatible with the Datadog agent behavior. The class is
- * thread-safe and can be used with concurrency.
- *
- *
>(capacity)
- def writer = new DDAgentWriter(Mock(DDApi), traces)
-
- when:
- for (def i = 0; i < capacity; i++) {
- writer.write([])
- }
-
- then:
- traces.size() == capacity
when:
writer.write(trace)
+ writer.write(trace)
+ writer.flush()
then:
- traces.size() == capacity
- traces.getAll().contains(trace)
+ 2 * api.serializeTrace(_) >> { trace -> callRealMethod() }
+ 1 * api.sendSerializedTraces(2, _, { it.size() == 2 })
+ 0 * _
+
+ cleanup:
+ writer.close()
where:
- trace = [newSpanOf(0)]
- capacity = 10
+ trace = [newSpanOf(0, "fixed-thread-name")]
+ }
+ def "test flood of traces"() {
+ setup:
+ def writer = new DDAgentWriter(api, disruptorSize, -1)
+ writer.start()
+ when:
+ (1..traceCount).each {
+ writer.write(trace)
+ }
+ writer.flush()
+
+ then:
+ _ * api.serializeTrace(_) >> { trace -> callRealMethod() }
+ 1 * api.sendSerializedTraces(traceCount, _, { it.size() < traceCount })
+ 0 * _
+
+ cleanup:
+ writer.close()
+
+ where:
+ trace = [newSpanOf(0, "fixed-thread-name")]
+ disruptorSize = 2
+ traceCount = 100 // Shouldn't trigger payload, but bigger than the disruptor size.
+ }
+
+ def "test flush by size"() {
+ setup:
+ def writer = new DDAgentWriter(api, DISRUPTOR_BUFFER_SIZE, -1)
+ def phaser = writer.apiPhaser
+ writer.start()
+ phaser.register()
+
+ when:
+ (1..6).each {
+ writer.write(trace)
+ }
+ // Wait for 2 flushes of 3 by size
+ phaser.awaitAdvanceInterruptibly(phaser.arrive())
+ phaser.awaitAdvanceInterruptibly(phaser.arriveAndDeregister())
+
+ then:
+ 6 * api.serializeTrace(_) >> { trace -> callRealMethod() }
+ 2 * api.sendSerializedTraces(3, _, { it.size() == 3 })
+
+ when:
+ (1..2).each {
+ writer.write(trace)
+ }
+ // Flush the remaining 2
+ writer.flush()
+
+ then:
+ 2 * api.serializeTrace(_) >> { trace -> callRealMethod() }
+ 1 * api.sendSerializedTraces(2, _, { it.size() == 2 })
+ 0 * _
+
+ cleanup:
+ writer.close()
+
+ where:
+ span = [newSpanOf(0, "fixed-thread-name")]
+ trace = (0..10000).collect { span }
+ }
+
+ def "test flush by time"() {
+ setup:
+ def writer = new DDAgentWriter(api)
+ def phaser = writer.apiPhaser
+ phaser.register()
+ writer.start()
+ writer.flush()
+
+ when:
+ (1..5).each {
+ writer.write(trace)
+ }
+ phaser.awaitAdvanceInterruptibly(phaser.arriveAndDeregister())
+
+ then:
+ 5 * api.serializeTrace(_) >> { trace -> callRealMethod() }
+ 1 * api.sendSerializedTraces(5, _, { it.size() == 5 })
+ 0 * _
+
+ cleanup:
+ writer.close()
+
+ where:
+ span = [newSpanOf(0, "fixed-thread-name")]
+ trace = (1..10).collect { span }
+ }
+
+ def "test default buffer size"() {
+ setup:
+ def writer = new DDAgentWriter(api, DISRUPTOR_BUFFER_SIZE, -1)
+ writer.start()
+
+ when:
+ (0..maxedPayloadTraceCount).each {
+ writer.write(minimalTrace)
+ def start = System.nanoTime()
+ // (consumer processes a trace in about 20 microseconds
+ while (System.nanoTime() - start < TimeUnit.MICROSECONDS.toNanos(100)) {
+ // Busywait because we don't want to fill up the ring buffer
+ }
+ }
+ writer.flush()
+
+ then:
+ (maxedPayloadTraceCount + 1) * api.serializeTrace(_) >> { trace -> callRealMethod() }
+ 1 * api.sendSerializedTraces(maxedPayloadTraceCount, _, { it.size() == maxedPayloadTraceCount })
+
+ cleanup:
+ writer.close()
+
+ where:
+ minimalContext = new DDSpanContext(
+ "1",
+ "1",
+ "0",
+ "",
+ "",
+ "",
+ PrioritySampling.UNSET,
+ "",
+ Collections.emptyMap(),
+ false,
+ "",
+ Collections.emptyMap(),
+ Mock(PendingTrace),
+ Mock(DDTracer))
+ minimalSpan = new DDSpan(0, minimalContext)
+ minimalTrace = [minimalSpan]
+ traceSize = DDApi.OBJECT_MAPPER.writeValueAsBytes(minimalTrace).length
+ maxedPayloadTraceCount = ((int) (DDAgentWriter.FLUSH_PAYLOAD_BYTES / traceSize)) + 1
}
def "check that are no interactions after close"() {
setup:
- def api = mock(DDApi)
def writer = new DDAgentWriter(api)
writer.start()
when:
writer.close()
writer.write([])
- Thread.sleep(flush_time_wait)
+ writer.flush()
then:
- verifyNoMoreInteractions(api)
-
- where:
- flush_time_wait = (int) (1.2 * (DDAgentWriter.FLUSH_TIME_SECONDS * 1_000))
+ 0 * _
+ writer.traceCount.get() == 0
}
}
diff --git a/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/DDApiTest.groovy b/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/DDApiTest.groovy
index 1cd0c7372c..f77e7f4b67 100644
--- a/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/DDApiTest.groovy
+++ b/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/DDApiTest.groovy
@@ -2,11 +2,9 @@ package datadog.trace.api.writer
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
import datadog.opentracing.SpanFactory
import datadog.trace.common.writer.DDApi
import datadog.trace.common.writer.DDApi.ResponseListener
-import org.msgpack.jackson.dataformat.MessagePackFactory
import spock.lang.Specification
import java.util.concurrent.atomic.AtomicReference
@@ -14,15 +12,20 @@ import java.util.concurrent.atomic.AtomicReference
import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer
class DDApiTest extends Specification {
- static mapper = new ObjectMapper(new MessagePackFactory())
+ static mapper = DDApi.OBJECT_MAPPER
def "sending an empty list of traces returns no errors"() {
setup:
def agent = httpServer {
handlers {
put("v0.4/traces") {
- def status = request.contentLength > 0 ? 200 : 500
- response.status(status).send()
+ if (request.contentType != "application/msgpack") {
+ response.status(400).send("wrong type: $request.contentType")
+ } else if (request.contentLength <= 0) {
+ response.status(400).send("no content")
+ } else {
+ response.status(200).send()
+ }
}
}
}
@@ -68,7 +71,6 @@ class DDApiTest extends Specification {
expect:
client.tracesUrl.toString() == "http://localhost:${agent.address.port}/v0.4/traces"
- client.getTraceCounter().addAndGet(traces.size()) >= 0
client.sendTraces(traces)
agent.lastRequest.contentType == "application/msgpack"
agent.lastRequest.headers.get("Datadog-Meta-Lang") == "java"
@@ -82,9 +84,9 @@ class DDApiTest extends Specification {
// Populate thread info dynamically as it is different when run via gradle vs idea.
where:
- traces | expectedRequestBody
- [] | []
- [SpanFactory.newSpanOf(1L).setTag("service.name", "my-service")] | [new TreeMap<>([
+ traces | expectedRequestBody
+ [] | []
+ [[SpanFactory.newSpanOf(1L).setTag("service.name", "my-service")]] | [[new TreeMap<>([
"duration" : 0,
"error" : 0,
"meta" : ["thread.name": Thread.currentThread().getName(), "thread.id": "${Thread.currentThread().id}"],
@@ -97,8 +99,8 @@ class DDApiTest extends Specification {
"start" : 1000,
"trace_id" : 1,
"type" : "fakeType"
- ])]
- [SpanFactory.newSpanOf(100L).setTag("resource.name", "my-resource")] | [new TreeMap<>([
+ ])]]
+ [[SpanFactory.newSpanOf(100L).setTag("resource.name", "my-resource")]] | [[new TreeMap<>([
"duration" : 0,
"error" : 0,
"meta" : ["thread.name": Thread.currentThread().getName(), "thread.id": "${Thread.currentThread().id}"],
@@ -111,7 +113,7 @@ class DDApiTest extends Specification {
"start" : 100000,
"trace_id" : 1,
"type" : "fakeType"
- ])]
+ ])]]
}
def "Api ResponseListeners see 200 responses"() {
@@ -130,18 +132,15 @@ class DDApiTest extends Specification {
}
def client = new DDApi("localhost", agent.address.port, null)
client.addResponseListener(responseListener)
- def traceCounter = client.getTraceCounter()
- traceCounter.set(3)
when:
- client.sendTraces([])
+ client.sendTraces([[], [], []])
then:
agentResponse.get() == '{"hello":"test"}'
agent.lastRequest.headers.get("Datadog-Meta-Lang") == "java"
agent.lastRequest.headers.get("Datadog-Meta-Lang-Version") == System.getProperty("java.version", "unknown")
agent.lastRequest.headers.get("Datadog-Meta-Tracer-Version") == "Stubbed-Test-Version"
agent.lastRequest.headers.get("X-Datadog-Trace-Count") == "3" // false data shows the value provided via traceCounter.
- traceCounter.get() == 0
cleanup:
agent.close()
@@ -200,11 +199,7 @@ class DDApiTest extends Specification {
"v0.3" | 30000 | false
}
- static List
>>() {})
- }
-
- static TreeMap
>> convertList(byte[] bytes) {
+ return mapper.readValue(bytes, new TypeReference
>>>() {})
}
}
diff --git a/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/SerializationTest.groovy b/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/SerializationTest.groovy
new file mode 100644
index 0000000000..1c8845f1d0
--- /dev/null
+++ b/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/SerializationTest.groovy
@@ -0,0 +1,62 @@
+package datadog.trace.api.writer
+
+
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.msgpack.core.MessagePack
+import org.msgpack.jackson.dataformat.MessagePackFactory
+import spock.lang.Shared
+import spock.lang.Specification
+
+import static java.util.Collections.singletonMap
+
+class SerializationTest extends Specification {
+ @Shared
+ def jsonMapper = new ObjectMapper()
+ @Shared
+ def mpMapper = new ObjectMapper(new MessagePackFactory())
+
+
+ def "test json mapper serialization"() {
+ setup:
+ def map = ["key1": "val1"]
+ def serializedMap = mapper.writeValueAsBytes(map)
+ def serializedList = "[${new String(serializedMap)}]".getBytes()
+
+ when:
+ def result = mapper.readValue(serializedList, new TypeReference
>>() {})
+
+ then:
+ result == [map]
+ new String(serializedList) == '[{"key1":"val1"}]'
+
+ where:
+ mapper = jsonMapper
+ }
+
+ def "test msgpack mapper serialization"() {
+ setup:
+ def serializedMaps = input.collect {
+ mapper.writeValueAsBytes(it)
+ }
+
+ def packer = MessagePack.newDefaultBufferPacker()
+ packer.packArrayHeader(serializedMaps.size())
+ serializedMaps.each {
+ packer.writePayload(it)
+ }
+ def serializedList = packer.toByteArray()
+
+ when:
+ def result = mapper.readValue(serializedList, new TypeReference
>>() {})
+
+ then:
+ result == input
+
+ where:
+ mapper = mpMapper
+
+ // GStrings get odd results in the serializer.
+ input = (1..1).collect { singletonMap("key$it".toString(), "val$it".toString()) }
+ }
+}
diff --git a/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/WriterQueueTest.groovy b/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/WriterQueueTest.groovy
deleted file mode 100644
index 13201b44dc..0000000000
--- a/dd-trace-ot/src/test/groovy/datadog/trace/api/writer/WriterQueueTest.groovy
+++ /dev/null
@@ -1,181 +0,0 @@
-package datadog.trace.api.writer
-
-import datadog.trace.common.writer.WriterQueue
-import spock.lang.Specification
-
-import java.util.concurrent.Phaser
-import java.util.concurrent.atomic.AtomicInteger
-
-class WriterQueueTest extends Specification {
-
- def "instantiate a empty queue throws an exception"() {
- when:
- new WriterQueue