From dac35f13be7b1834832165332a4cc8b1cb27e9d6 Mon Sep 17 00:00:00 2001 From: jack-berg <34418638+jack-berg@users.noreply.github.com> Date: Mon, 4 Jan 2021 15:50:20 -0600 Subject: [PATCH 1/3] refactor infinite tracing for improved clarity and simplicity, add support for flaky code test configuration --- .../main/java/com/newrelic/BackoffPolicy.java | 8 - .../com/newrelic/ChannelClosingException.java | 2 +- .../java/com/newrelic/ChannelFactory.java | 36 --- .../java/com/newrelic/ChannelManager.java | 205 ++++++++++++++++++ .../java/com/newrelic/ChannelSupplier.java | 59 ----- .../com/newrelic/ChannelToStreamObserver.java | 54 ----- .../java/com/newrelic/ConnectionHeaders.java | 36 --- .../java/com/newrelic/ConnectionStatus.java | 69 ------ .../com/newrelic/DaemonThreadFactory.java | 21 -- .../com/newrelic/DefaultBackoffPolicy.java | 24 -- .../com/newrelic/DisconnectionHandler.java | 40 ---- .../com/newrelic/FlakyHeaderInterceptor.java | 46 ---- .../java/com/newrelic/GrpcSpanConverter.java | 45 ---- .../java/com/newrelic/HeadersInterceptor.java | 45 ++-- .../java/com/newrelic/InfiniteTracing.java | 135 +++++++++--- .../com/newrelic/InfiniteTracingConfig.java | 19 ++ .../main/java/com/newrelic/LoopForever.java | 30 --- .../java/com/newrelic/ResponseObserver.java | 130 ++++++----- .../main/java/com/newrelic/SpanConverter.java | 7 - .../main/java/com/newrelic/SpanDelivery.java | 75 ------- .../java/com/newrelic/SpanEventConsumer.java | 138 ------------ .../java/com/newrelic/SpanEventSender.java | 138 ++++++++++++ .../com/newrelic/StreamObserverFactory.java | 29 --- .../com/newrelic/StreamObserverSupplier.java | 23 -- .../java/com/newrelic/ChannelFactoryTest.java | 66 ------ .../java/com/newrelic/ChannelManagerTest.java | 160 ++++++++++++++ .../com/newrelic/ChannelSupplierTest.java | 79 ------- .../newrelic/ChannelToStreamObserverTest.java | 119 ---------- .../com/newrelic/ConnectionHeadersTest.java | 26 --- .../com/newrelic/ConnectionStatusTest.java | 124 ----------- .../com/newrelic/DaemonThreadFactoryTest.java | 26 --- .../newrelic/DefaultBackoffPolicyTest.java | 29 --- .../newrelic/DisconnectionHandlerTest.java | 60 ----- .../newrelic/FlakyHeaderInterceptorTest.java | 81 ------- .../com/newrelic/GrpcSpanConverterTest.java | 60 ----- .../com/newrelic/HeadersInterceptorTest.java | 43 ++-- .../com/newrelic/InfiniteTracingTest.java | 86 ++++++++ .../newrelic/MockForwardingClientCall.java | 37 ---- .../com/newrelic/ResponseObserverTest.java | 187 ++++++---------- .../java/com/newrelic/SpanDeliveryTest.java | 193 ----------------- .../com/newrelic/SpanEventConsumerTest.java | 87 -------- .../com/newrelic/SpanEventSenderTest.java | 196 +++++++++++++++++ .../newrelic/StreamObserverSupplierTest.java | 41 ---- .../agent/config/InfiniteTracingConfig.java | 2 + .../config/InfiniteTracingConfigImpl.java | 6 + .../agent/service/ServiceManagerImpl.java | 7 +- .../UpdateInfiniteTracingAfterConnect.java | 4 +- ...UpdateInfiniteTracingAfterConnectTest.java | 3 +- 48 files changed, 1123 insertions(+), 2013 deletions(-) delete mode 100644 infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ChannelFactory.java create mode 100644 infinite-tracing/src/main/java/com/newrelic/ChannelManager.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ChannelSupplier.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ChannelToStreamObserver.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ConnectionHeaders.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ConnectionStatus.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/DaemonThreadFactory.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/DefaultBackoffPolicy.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/DisconnectionHandler.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/FlakyHeaderInterceptor.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/GrpcSpanConverter.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/LoopForever.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/SpanConverter.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/SpanDelivery.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/SpanEventConsumer.java create mode 100644 infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/StreamObserverFactory.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/StreamObserverSupplier.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ChannelFactoryTest.java create mode 100644 infinite-tracing/src/test/java/com/newrelic/ChannelManagerTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ChannelSupplierTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ChannelToStreamObserverTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ConnectionHeadersTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ConnectionStatusTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/DaemonThreadFactoryTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/DefaultBackoffPolicyTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/DisconnectionHandlerTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/FlakyHeaderInterceptorTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/GrpcSpanConverterTest.java create mode 100644 infinite-tracing/src/test/java/com/newrelic/InfiniteTracingTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/MockForwardingClientCall.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/SpanDeliveryTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/SpanEventConsumerTest.java create mode 100644 infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/StreamObserverSupplierTest.java diff --git a/infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java b/infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java deleted file mode 100644 index 6e8f00bc94..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.newrelic; - -import io.grpc.Status; - -public interface BackoffPolicy { - boolean shouldReconnect(Status status); - void backoff(); -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelClosingException.java b/infinite-tracing/src/main/java/com/newrelic/ChannelClosingException.java index bd18b6190e..55bccc3c9e 100644 --- a/infinite-tracing/src/main/java/com/newrelic/ChannelClosingException.java +++ b/infinite-tracing/src/main/java/com/newrelic/ChannelClosingException.java @@ -4,5 +4,5 @@ * This is a signaling exception to the call that the channel is closing. By using this exception, we can pass the required * information to the {@link io.grpc.stub.StreamObserver#onError} call so it knows not to consider this an actual error. */ -public class ChannelClosingException extends Exception { +class ChannelClosingException extends Exception { } diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelFactory.java b/infinite-tracing/src/main/java/com/newrelic/ChannelFactory.java deleted file mode 100644 index 3d1c59427a..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ChannelFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.newrelic; - -import com.google.common.annotations.VisibleForTesting; -import io.grpc.ClientInterceptor; -import io.grpc.ManagedChannel; -import io.grpc.okhttp.OkHttpChannelBuilder; - -public class ChannelFactory { - private final InfiniteTracingConfig config; - private final ClientInterceptor[] interceptors; - - public ChannelFactory(InfiniteTracingConfig config, ClientInterceptor... interceptors) { - this.config = config; - this.interceptors = interceptors; - } - - public ManagedChannel createChannel() { - OkHttpChannelBuilder okHttpChannelBuilder = newOkHttpChannelBuilder() - .defaultLoadBalancingPolicy("pick_first") - .intercept(interceptors); - - if (config.getUsePlaintext()) { - okHttpChannelBuilder.usePlaintext(); - } else { - okHttpChannelBuilder.useTransportSecurity(); - } - return okHttpChannelBuilder.build(); - } - - @VisibleForTesting - OkHttpChannelBuilder newOkHttpChannelBuilder() { - return OkHttpChannelBuilder - .forAddress(config.getHost(), config.getPort()); - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java b/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java new file mode 100644 index 0000000000..868de0e36f --- /dev/null +++ b/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java @@ -0,0 +1,205 @@ +package com.newrelic; + +import com.google.common.annotations.VisibleForTesting; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import com.newrelic.trace.v1.IngestServiceGrpc; +import com.newrelic.trace.v1.IngestServiceGrpc.IngestServiceStub; +import com.newrelic.trace.v1.V1; +import io.grpc.ManagedChannel; +import io.grpc.okhttp.OkHttpChannelBuilder; +import io.grpc.stub.ClientCallStreamObserver; + +import javax.annotation.concurrent.GuardedBy; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +class ChannelManager { + + private final Logger logger; + private final InfiniteTracingConfig config; + private final MetricAggregator aggregator; + + private final Object lock = new Object(); + @GuardedBy("lock") private boolean isShutdownForever; + @GuardedBy("lock") private CountDownLatch backoffLatch; + @GuardedBy("lock") private ManagedChannel managedChannel; + @GuardedBy("lock") private ClientCallStreamObserver spanObserver; + @GuardedBy("lock") private ResponseObserver responseObserver; + @GuardedBy("lock") private String agentRunToken; + @GuardedBy("lock") private Map requestMetadata; + + ChannelManager(InfiniteTracingConfig config, MetricAggregator aggregator, String agentRunToken, Map requestMetadata) { + this.logger = config.getLogger(); + this.config = config; + this.aggregator = aggregator; + this.agentRunToken = agentRunToken; + this.requestMetadata = requestMetadata; + } + + /** + * Update metadata included on gRPC requests. + * + * @param agentRunToken the agent run token + * @param requestMetadata any extra metadata headers that must be included + */ + void updateMetadata(String agentRunToken, Map requestMetadata) { + synchronized (lock) { + this.agentRunToken = agentRunToken; + this.requestMetadata = requestMetadata; + } + } + + /** + * Obtain a span observer. Creates a channel if one is not open. Creates a span observer if one + * does not exist. If the channel has been shutdown and is backing off via + * {@link #shutdownChannelAndBackoff(int)}, awaits the backoff period before recreating the channel. + * + * @return a span observer + */ + ClientCallStreamObserver getSpanObserver() { + // Obtain the lock, and await the backoff if in progress + CountDownLatch latch; + synchronized (lock) { + latch = backoffLatch; + } + if (latch != null) { + try { + logger.log(Level.FINE, "Awaiting backoff."); + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted while awaiting backoff."); + } + } + + // Obtain the lock, and possibly recreate the channel or the span observer + synchronized (lock) { + if (isShutdownForever) { + throw new RuntimeException("No longer accepting connections to gRPC."); + } + if (managedChannel == null) { + logger.log(Level.FINE, "Creating gRPC channel."); + managedChannel = buildChannel(); + } + if (spanObserver == null) { + logger.log(Level.FINE, "Creating gRPC span observer."); + IngestServiceStub ingestServiceStub = buildStub(managedChannel); + responseObserver = buildResponseObserver(); + spanObserver = (ClientCallStreamObserver) ingestServiceStub.recordSpan(responseObserver); + aggregator.incrementCounter("Supportability/InfiniteTracing/Connect"); + } + return spanObserver; + } + } + + @VisibleForTesting + IngestServiceStub buildStub(ManagedChannel managedChannel) { + return IngestServiceGrpc.newStub(managedChannel); + } + + @VisibleForTesting + ResponseObserver buildResponseObserver() { + return new ResponseObserver(logger, this, aggregator); + } + + /** + * Cancel the span observer. The next time {@link #getSpanObserver()} is called the span observer + * will be recreated. This cancels the span observer with a {@link ChannelClosingException}, which + * {@link ResponseObserver#onError(Throwable)} detects and ignores. + */ + void cancelSpanObserver() { + synchronized (lock) { + if (spanObserver == null) { + return; + } + logger.log(Level.FINE, "Canceling gRPC span observer."); + spanObserver.cancel("CLOSING_CONNECTION", new ChannelClosingException()); + spanObserver = null; + responseObserver = null; + } + } + + /** + * Shutdown the channel, cancel the span observer, and backoff. The next time {@link #getSpanObserver()} + * is called, it will await the backoff and the channel will be recreated. + * + * @param backoffSeconds the number of seconds to await before the channel can be recreated + */ + void shutdownChannelAndBackoff(int backoffSeconds) { + logger.log(Level.FINE, "Shutting down gRPC channel and backing off for {0} seconds.", backoffSeconds); + CountDownLatch latch; + synchronized (lock) { + if (backoffLatch != null) { + logger.log(Level.FINE, "Backoff already in progress."); + return; + } + backoffLatch = new CountDownLatch(1); + latch = backoffLatch; + + if (managedChannel != null) { + managedChannel.shutdown(); + managedChannel = null; + } + cancelSpanObserver(); + } + + try { + TimeUnit.SECONDS.sleep(backoffSeconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted while backing off."); + } + + synchronized (lock) { + latch.countDown(); + backoffLatch = null; + } + logger.log(Level.FINE, "Backoff complete."); + } + + /** + * Shutdown the channel and do not recreate it. The next time {@link #getSpanObserver()} is called + * an exception will be thrown. + */ + void shutdownChannelForever() { + synchronized (lock) { + logger.log(Level.FINE, "Shutting down gRPC channel forever."); + shutdownChannelAndBackoff(0); + this.isShutdownForever = true; + } + } + + @VisibleForTesting + ManagedChannel buildChannel() { + Map headers; + synchronized (lock) { + headers = requestMetadata != null ? new HashMap<>(requestMetadata) : new HashMap(); + headers.put("agent_run_token", agentRunToken); + } + + headers.put("license_key", config.getLicenseKey()); + if (config.getFlakyPercentage() != null) { + logger.log(Level.WARNING, "Infinite tracing is configured with a flaky percentage! There will be errors!"); + headers.put("flaky", config.getFlakyPercentage().toString()); + if (config.getFlakyCode() != null) { + headers.put("flaky_code", config.getFlakyCode().toString()); + } + } + + OkHttpChannelBuilder channelBuilder = OkHttpChannelBuilder + .forAddress(config.getHost(), config.getPort()) + .defaultLoadBalancingPolicy("pick_first") + .intercept(new HeadersInterceptor(headers)); + if (config.getUsePlaintext()) { + channelBuilder.usePlaintext(); + } else { + channelBuilder.useTransportSecurity(); + } + return channelBuilder.build(); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelSupplier.java b/infinite-tracing/src/main/java/com/newrelic/ChannelSupplier.java deleted file mode 100644 index 533c144340..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ChannelSupplier.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.api.agent.Logger; -import io.grpc.ManagedChannel; - -import java.util.logging.Level; - -/** - * Provides a {@link ManagedChannel}, recreating it only if necessary. - * - * Not thread-safe. - */ -public class ChannelSupplier implements Supplier { - private final ConnectionStatus connectionStatus; - private final Logger logger; - private final ChannelFactory channelFactory; - private volatile ManagedChannel channel; - - public ChannelSupplier(ChannelFactory channelFactory, ConnectionStatus connectionStatus, Logger logger) { - this.connectionStatus = connectionStatus; - this.logger = logger; - this.channelFactory = channelFactory; - channel = null; - } - - @Override - public ManagedChannel get() { - ConnectionStatus.BlockResult blockResult = getBlockResult(); - - if (blockResult == ConnectionStatus.BlockResult.GO_AWAY_FOREVER) { - throw new RuntimeException("No longer attempting to connect."); - } - - if (blockResult == ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION || channel == null) { - logger.log(Level.FINE, "Attempting to connect to the Trace Observer."); - - if (channel != null) { - ManagedChannel oldChannel = channel; - channel = null; - oldChannel.shutdown(); - } - channel = channelFactory.createChannel(); - connectionStatus.didConnect(); - } - - return channel; - } - - public ConnectionStatus.BlockResult getBlockResult() { - try { - return connectionStatus.blockOnConnection(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Thread interrupted while attempting to connect."); - } - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelToStreamObserver.java b/infinite-tracing/src/main/java/com/newrelic/ChannelToStreamObserver.java deleted file mode 100644 index 7ca0811b34..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ChannelToStreamObserver.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.newrelic; - -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Accepts a {@link ManagedChannel} and provides a valid {@link ClientCallStreamObserver}, creating it only when required. - * - * Not thread-safe. - */ -public class ChannelToStreamObserver implements Function> { - private final StreamObserverFactory streamObserverFactory; - private final AtomicBoolean shouldRecreateCall; - private volatile ManagedChannel lastChannel; - private volatile ClientCallStreamObserver streamObserver; - - public ChannelToStreamObserver( - StreamObserverFactory streamObserverFactory, - AtomicBoolean shouldRecreateCall) { - this.streamObserverFactory = streamObserverFactory; - this.shouldRecreateCall = shouldRecreateCall; - } - - @Override - public ClientCallStreamObserver apply(ManagedChannel channel) { - if (channel == null) { - return null; - } - - if (lastChannel != channel || streamObserver == null || shouldRecreateCall.get()) { - recreateStreamObserver(channel); - } - - return streamObserver; - } - - private void recreateStreamObserver(ManagedChannel channel) { - lastChannel = channel; - clearStreamObserver(); - streamObserver = streamObserverFactory.buildStreamObserver(channel); - shouldRecreateCall.set(false); - } - - private void clearStreamObserver() { - if (this.streamObserver != null) { - ClientCallStreamObserver oldStreamObserver = this.streamObserver; - this.streamObserver = null; - oldStreamObserver.cancel("CLOSING_CONNECTION", new ChannelClosingException()); - } - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ConnectionHeaders.java b/infinite-tracing/src/main/java/com/newrelic/ConnectionHeaders.java deleted file mode 100644 index 9b06827537..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ConnectionHeaders.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.api.agent.Logger; - -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; - -public class ConnectionHeaders implements Supplier> { - private final ConnectionStatus connectionStatus; - private final Logger logger; - private final String licenseKey; - private volatile Map headers; - - public ConnectionHeaders(ConnectionStatus connectionStatus, Logger logger, String licenseKey) { - this.connectionStatus = connectionStatus; - this.logger = logger; - this.licenseKey = licenseKey; - } - - public void set(String newRunToken, Map headers) { - Map newHeaders = new HashMap<>(headers); - newHeaders.put("agent_run_token", newRunToken); - newHeaders.put("license_key", licenseKey); - this.headers = newHeaders; - - logger.log(Level.INFO, "New Relic connection successful. Attempting connection to the Trace Observer."); - connectionStatus.reattemptConnection(); - } - - @Override - public Map get() { - return headers; - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ConnectionStatus.java b/infinite-tracing/src/main/java/com/newrelic/ConnectionStatus.java deleted file mode 100644 index 8c39a53baf..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ConnectionStatus.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.Status; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; - -public class ConnectionStatus { - public ConnectionStatus(Logger logger) { - this.logger = logger; - } - - public enum BlockResult {ALREADY_CONNECTED, MUST_ATTEMPT_CONNECTION, GO_AWAY_FOREVER} - - private enum ConnectionState {CONNECT_NEEDED, CONNECTING, CONNECTED, BACKOFF_PAUSE, STOPPED_FOREVER} - - private final Logger logger; - private final AtomicReference currentState = new AtomicReference<>(ConnectionState.CONNECT_NEEDED); - - /** - * Blocks until a connection is either made, or this thread is responsible for making the connection. - * - * @return {@link BlockResult#MUST_ATTEMPT_CONNECTION} if this thread is responsible for connecting; {@link BlockResult#ALREADY_CONNECTED} if already connected. - */ - public BlockResult blockOnConnection() throws InterruptedException { - while (true) { - ConnectionState current = currentState.get(); - if (current == ConnectionState.CONNECTED) { - return BlockResult.ALREADY_CONNECTED; - } else if (current == ConnectionState.STOPPED_FOREVER) { - logger.log(Level.FINE, "No longer attempting to reconnect to gRPC"); - return BlockResult.GO_AWAY_FOREVER; - } else if (currentState.compareAndSet(ConnectionState.CONNECT_NEEDED, ConnectionState.CONNECTING)) { - return BlockResult.MUST_ATTEMPT_CONNECTION; - } - - Thread.sleep(1000); - } - } - - /** - * Threads that successfully connect should call this when complete. It will release other threads busy-waiting for a connection. - */ - public void didConnect() { - currentState.set(ConnectionState.CONNECTED); - } - - /** - * Tells all threads that all connections should be stopped and not resumed. - */ - public void shutDownForever() { - currentState.set(ConnectionState.STOPPED_FOREVER); - } - - /** - * Indicates whether or not this thread should follow the disconnect/backoff routine - */ - public boolean shouldReconnect() { - return currentState.compareAndSet(ConnectionState.CONNECTED, ConnectionState.BACKOFF_PAUSE); - } - - /** - * Tells all threads that the next thread should attempt to reconnect. - */ - public void reattemptConnection() { - currentState.set(ConnectionState.CONNECT_NEEDED); - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/DaemonThreadFactory.java b/infinite-tracing/src/main/java/com/newrelic/DaemonThreadFactory.java deleted file mode 100644 index a0419f6391..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/DaemonThreadFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.newrelic; - -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; - -public class DaemonThreadFactory implements ThreadFactory { - private final String serviceName; - private final AtomicInteger counter = new AtomicInteger(0); - - public DaemonThreadFactory(String serviceName) { - this.serviceName = serviceName; - } - - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable); - thread.setName("New Relic " + serviceName + " #" + counter.incrementAndGet()); - thread.setDaemon(true); - return thread; - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/DefaultBackoffPolicy.java b/infinite-tracing/src/main/java/com/newrelic/DefaultBackoffPolicy.java deleted file mode 100644 index e5896e03de..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/DefaultBackoffPolicy.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.newrelic; - -import io.grpc.Status; - -import java.util.concurrent.TimeUnit; - -public class DefaultBackoffPolicy implements BackoffPolicy { - - @Override - public boolean shouldReconnect(Status status) { - // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#unimplemented - return status != Status.UNIMPLEMENTED; - } - - @Override - public void backoff() { - try { - // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#other-errors-1 - TimeUnit.SECONDS.sleep(15); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/DisconnectionHandler.java b/infinite-tracing/src/main/java/com/newrelic/DisconnectionHandler.java deleted file mode 100644 index 2a2d90e771..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/DisconnectionHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.Status; - -import java.util.logging.Level; - -public class DisconnectionHandler { - private final ConnectionStatus connectionStatus; - private final BackoffPolicy backoffPolicy; - private final Logger logger; - - public DisconnectionHandler(ConnectionStatus connectionStatus, BackoffPolicy backoffPolicy, Logger logger) { - this.connectionStatus = connectionStatus; - this.backoffPolicy = backoffPolicy; - this.logger = logger; - } - - public void terminate() { - connectionStatus.shutDownForever(); - } - - public void handle(Status responseStatus) { - if (!connectionStatus.shouldReconnect()) { - return; - } - - if (!backoffPolicy.shouldReconnect(responseStatus)) { - if (responseStatus != null) { - logger.log(Level.WARNING, "Got gRPC status " + responseStatus.getCode().toString() + ", no longer permitting connections."); - } - terminate(); - } - - logger.log(Level.FINE, "Backing off due to gRPC errors."); - backoffPolicy.backoff(); - logger.log(Level.FINE, "Backoff complete, attempting connection."); - connectionStatus.reattemptConnection(); - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/FlakyHeaderInterceptor.java b/infinite-tracing/src/main/java/com/newrelic/FlakyHeaderInterceptor.java deleted file mode 100644 index de57ff55c3..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/FlakyHeaderInterceptor.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.newrelic; - -import io.grpc.*; -import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; -import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; -import io.grpc.Metadata.Key; - -import java.util.logging.Level; - -/** - * Injects the "flaky" header on each sent span, only if configured to - * do so. - */ -public class FlakyHeaderInterceptor implements ClientInterceptor { - - private static final Key FLAKY_HEADER = Key.of("flaky", Metadata.ASCII_STRING_MARSHALLER); - private final InfiniteTracingConfig config; - - public FlakyHeaderInterceptor(InfiniteTracingConfig config) { - this.config = config; - if (config.getFlakyPercentage() != null) { - config.getLogger().log(Level.WARNING, "Infinite tracing is configured with a flaky percentage! There will be errors!"); - } - } - - @Override - public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { - final Double flakyPercentage = config.getFlakyPercentage(); - if (flakyPercentage == null) { - return next.newCall(method, callOptions); - } - return new SimpleForwardingClientCall(next.newCall(method, callOptions)) { - @Override - public void start(Listener responseListener, Metadata headers) { - headers.put(FLAKY_HEADER, flakyPercentage.toString()); - super.start(new SimpleForwardingClientCallListener(responseListener) { - @Override - public void onHeaders(Metadata headers) { - super.onHeaders(headers); - } - }, headers); - } - }; - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/GrpcSpanConverter.java b/infinite-tracing/src/main/java/com/newrelic/GrpcSpanConverter.java deleted file mode 100644 index bbd16f4819..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/GrpcSpanConverter.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.trace.v1.V1; - -import java.util.HashMap; -import java.util.Map; - -public class GrpcSpanConverter implements SpanConverter { - public V1.Span convert(SpanEvent spanEvent) { - Map intrinsicAttributes = copyAttributes(spanEvent.getIntrinsics()); - Map userAttributes = copyAttributes(spanEvent.getUserAttributesCopy()); - Map agentAttributes = copyAttributes(spanEvent.getAgentAttributes()); - - intrinsicAttributes.put("appName", V1.AttributeValue.newBuilder().setStringValue(spanEvent.getAppName()).build()); - - return V1.Span.newBuilder() - .setTraceId(spanEvent.getTraceId()) - .putAllIntrinsics(intrinsicAttributes) - .putAllAgentAttributes(agentAttributes) - .putAllUserAttributes(userAttributes) - .build(); - } - - private Map copyAttributes(Map original) { - Map copy = new HashMap<>(); - if (original == null) { - return copy; - } - - for (Map.Entry entry : original.entrySet()) { - Object value = entry.getValue(); - if (value instanceof String) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setStringValue((String) value).build()); - } else if (value instanceof Long || value instanceof Integer) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setIntValue(((Number) value).longValue()).build()); - } else if (value instanceof Float || value instanceof Double) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setDoubleValue(((Number) value).doubleValue()).build()); - } else if (value instanceof Boolean) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setBoolValue((Boolean) value).build()); - } - } - return copy; - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/HeadersInterceptor.java b/infinite-tracing/src/main/java/com/newrelic/HeadersInterceptor.java index a002653dfc..080812cd0e 100644 --- a/infinite-tracing/src/main/java/com/newrelic/HeadersInterceptor.java +++ b/infinite-tracing/src/main/java/com/newrelic/HeadersInterceptor.java @@ -1,6 +1,5 @@ package com.newrelic; -import com.newrelic.agent.interfaces.backport.Supplier; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -12,30 +11,38 @@ import java.util.Map; -// this class is for adding headers to the outbound span event stream -public class HeadersInterceptor implements ClientInterceptor { +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; - private final Supplier> headersSupplier; +class HeadersInterceptor implements ClientInterceptor { - public HeadersInterceptor(Supplier> headersSupplier) { - this.headersSupplier = headersSupplier; + private final Map headers; + + HeadersInterceptor(Map headers) { + this.headers = headers; } + @Override public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { - return new ForwardingClientCall.SimpleForwardingClientCall(next.newCall(method, callOptions)) { + return new HeadersClientCall<>(method, callOptions, next); + } - @Override - public void start(Listener responseListener, Metadata headers) { - for (Map.Entry header: headersSupplier.get().entrySet()) { - headers.put(Metadata.Key.of(header.getKey().toLowerCase(), Metadata.ASCII_STRING_MARSHALLER), header.getValue()); - } - super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { - @Override - public void onHeaders(Metadata headers) { - super.onHeaders(headers); - } - }, headers); + private class HeadersClientCall extends ForwardingClientCall.SimpleForwardingClientCall { + + private HeadersClientCall(MethodDescriptor method, CallOptions callOptions, Channel next) { + super(next.newCall(method, callOptions)); + } + + @Override + public void start(Listener responseListener, Metadata metadata) { + for (Map.Entry header : headers.entrySet()) { + metadata.put(Metadata.Key.of(header.getKey().toLowerCase(), ASCII_STRING_MARSHALLER), header.getValue()); } - }; + super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { + @Override + public void onHeaders(Metadata headers) { + super.onHeaders(headers); + } + }, metadata); + } } } \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/InfiniteTracing.java b/infinite-tracing/src/main/java/com/newrelic/InfiniteTracing.java index 0367b5da0e..51ee9ddd7c 100644 --- a/infinite-tracing/src/main/java/com/newrelic/InfiniteTracing.java +++ b/infinite-tracing/src/main/java/com/newrelic/InfiniteTracing.java @@ -1,60 +1,129 @@ package com.newrelic; +import com.google.common.annotations.VisibleForTesting; import com.newrelic.agent.interfaces.backport.Consumer; import com.newrelic.agent.model.SpanEvent; +import com.newrelic.api.agent.Logger; import com.newrelic.api.agent.MetricAggregator; -import java.util.Collections; +import javax.annotation.concurrent.GuardedBy; import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; public class InfiniteTracing implements Consumer { - private final SpanEventConsumer spanEventConsumer; - /** - * Set up the Infinite Tracing library. - * @param config Required data to start a connection to Infinite Tracing. - * @param metricAggregator Aggregator to which information about the library will be recorded. - * @return An object that requires two final pieces: {@link #setConnectionMetadata} and {@link #start} - */ - @SuppressWarnings("unused") // this is the public API of the class - public static InfiniteTracing initialize(InfiniteTracingConfig config, MetricAggregator metricAggregator) { - SpanEventConsumer spanEventConsumer = SpanEventConsumer.builder(config, metricAggregator).build(); - return new InfiniteTracing(spanEventConsumer); - } + private final Logger logger; + private final InfiniteTracingConfig config; + private final MetricAggregator aggregator; + private final ExecutorService executorService; + private final BlockingQueue queue; - private InfiniteTracing(SpanEventConsumer spanEventConsumer) { - this.spanEventConsumer = spanEventConsumer; + private final Object lock = new Object(); + @GuardedBy("lock") private Future spanEventSenderFuture; + @GuardedBy("lock") private SpanEventSender spanEventSender; + @GuardedBy("lock") private ChannelManager channelManager; + + @VisibleForTesting + InfiniteTracing(InfiniteTracingConfig config, MetricAggregator aggregator, ExecutorService executorService, BlockingQueue queue) { + this.logger = config.getLogger(); + this.config = config; + this.aggregator = aggregator; + this.executorService = executorService; + this.queue = queue; } /** - * Call this method when the connection metadata changes, which is driven by the collector. + * Start sending spans to the Infinite Tracing Observer. If already running, update with the + * {@code agentRunToken} and {@code requestMetadata}. * - * @param newRunToken The new agent run token that should be supplied to the Trace Observer. - * @param headers The metadata that should be supplied to the Trace Observer as headers. + * @param agentRunToken the agent run token + * @param requestMetadata any extra metadata headers that must be included */ - public void setConnectionMetadata(String newRunToken, Map headers) { - spanEventConsumer.setConnectionMetadata(newRunToken, headers); + public void start(String agentRunToken, Map requestMetadata) { + synchronized (lock) { + if (spanEventSenderFuture != null) { + channelManager.updateMetadata(agentRunToken, requestMetadata); + channelManager.shutdownChannelAndBackoff(0); + return; + } + logger.log(Level.INFO, "Starting Infinite Tracing."); + channelManager = buildChannelManager(agentRunToken, requestMetadata); + spanEventSender = buildSpanEventSender(); + spanEventSenderFuture = executorService.submit(spanEventSender); + } } - /** - * Initiates the connection and acceptance of {@link SpanEvent} instances. - */ - @SuppressWarnings("unused") // this is the public API of the class - public void start() { - spanEventConsumer.start(); + @VisibleForTesting + ChannelManager buildChannelManager(String agentRunToken, Map requestMetadata) { + return new ChannelManager(config, aggregator, agentRunToken, requestMetadata); + } + + @VisibleForTesting + SpanEventSender buildSpanEventSender() { + return new SpanEventSender(config, queue, aggregator, channelManager); } /** - * Call this method whenever the run token changes. - * @deprecated use {@link #setConnectionMetadata} instead + * Stop sending spans to the Infinite Tracing Observer and cleanup resources. If not already running, + * return immediately. */ - @Deprecated - public void setRunToken(String newRunToken) { - setConnectionMetadata(newRunToken, Collections.emptyMap()); + public void stop() { + synchronized (lock) { + if (spanEventSenderFuture == null) { + return; + } + logger.log(Level.INFO, "Stopping Infinite Tracing."); + spanEventSenderFuture.cancel(true); + channelManager.shutdownChannelForever(); + spanEventSenderFuture = null; + spanEventSender = null; + channelManager = null; + } } @Override public void accept(SpanEvent spanEvent) { - spanEventConsumer.accept(spanEvent); + aggregator.incrementCounter("Supportability/InfiniteTracing/Span/Seen"); + if (!queue.offer(spanEvent)) { + logger.log(Level.FINEST, "Span event not accepted. The queue was full."); + } + } + + /** + * Initialize Infinite Tracing. Note, for spans to start being sent {@link #start(String, Map)} must + * be called. + * + * @param config the config + * @param aggregator the metric aggregator + * @return the instance + */ + public static InfiniteTracing initialize(InfiniteTracingConfig config, MetricAggregator aggregator) { + ExecutorService executorService = Executors.newSingleThreadExecutor(new DaemonThreadFactory("Infinite Tracing")); + return new InfiniteTracing(config, aggregator, executorService, new LinkedBlockingDeque(config.getMaxQueueSize())); } -} + + private static class DaemonThreadFactory implements ThreadFactory { + private final String serviceName; + private final AtomicInteger counter = new AtomicInteger(0); + + private DaemonThreadFactory(String serviceName) { + this.serviceName = serviceName; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setName("New Relic " + serviceName + " #" + counter.incrementAndGet()); + thread.setDaemon(true); + return thread; + } + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/InfiniteTracingConfig.java b/infinite-tracing/src/main/java/com/newrelic/InfiniteTracingConfig.java index 0551955aa1..4610257b2a 100644 --- a/infinite-tracing/src/main/java/com/newrelic/InfiniteTracingConfig.java +++ b/infinite-tracing/src/main/java/com/newrelic/InfiniteTracingConfig.java @@ -11,6 +11,7 @@ public class InfiniteTracingConfig { private final int port; private final Logger logger; private final Double flakyPercentage; + private final Long flakyCode; private final boolean usePlaintext; public InfiniteTracingConfig(Builder builder) { @@ -20,6 +21,7 @@ public InfiniteTracingConfig(Builder builder) { this.port = builder.port; this.logger = builder.logger; this.flakyPercentage = builder.flakyPercentage; + this.flakyCode = builder.flakyCode; this.usePlaintext = builder.usePlaintext; } @@ -51,6 +53,10 @@ public Double getFlakyPercentage() { return flakyPercentage; } + public Long getFlakyCode() { + return flakyCode; + } + public boolean getUsePlaintext() { return usePlaintext; } @@ -62,6 +68,7 @@ public static class Builder { private String host; private int port; private Double flakyPercentage; + private Long flakyCode; private boolean usePlaintext; /** @@ -115,6 +122,18 @@ public Builder flakyPercentage(Double flakyPercentage) { return this; } + /** + * The optional gRPC error status to trigger when {@link #flakyPercentage(Double)} is + * specified. + * + * @param flakyCode The gRPC error status code + * @see gRPC status codes + */ + public Builder flakyCode(Long flakyCode) { + this.flakyCode = flakyCode; + return this; + } + /** * The optional boolean connect using plaintext * diff --git a/infinite-tracing/src/main/java/com/newrelic/LoopForever.java b/infinite-tracing/src/main/java/com/newrelic/LoopForever.java deleted file mode 100644 index 511283f90a..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/LoopForever.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; - -import java.util.logging.Level; - -public class LoopForever implements Runnable { - private final Logger logger; - private final Runnable spanDeliveryConsumer; - - public LoopForever( - Logger logger, - Runnable spanDeliveryConsumer) { - this.logger = logger; - this.spanDeliveryConsumer = spanDeliveryConsumer; - } - - @Override - public void run() { - logger.log(Level.FINE, "Initializing {0}", this); - while (true) { - try { - spanDeliveryConsumer.run(); - } catch (Throwable t) { - logger.log(Level.SEVERE, t, "There was a problem with the Infinite Tracing span sender, and no further spans will be sent"); - return; - } - } - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java b/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java index ca72f74971..668c16d085 100644 --- a/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java +++ b/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java @@ -1,98 +1,122 @@ package com.newrelic; +import com.google.common.annotations.VisibleForTesting; import com.newrelic.api.agent.Logger; import com.newrelic.api.agent.MetricAggregator; import com.newrelic.trace.v1.V1; import io.grpc.Status; -import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; -public class ResponseObserver implements StreamObserver { - private final MetricAggregator metricAggregator; +class ResponseObserver implements StreamObserver { + + static final int DEFAULT_BACKOFF_SECONDS = 15; + static final int[] BACKOFF_SECONDS_SEQUENCE = new int[] { 15, 15, 30, 60, 120, 300 }; + private final Logger logger; - private final DisconnectionHandler disconnectionHandler; - private final AtomicBoolean shouldRecreateCall; + private final ChannelManager channelManager; + private final MetricAggregator aggregator; + private final AtomicInteger backoffSequenceIndex = new AtomicInteger(-1); - public ResponseObserver(MetricAggregator metricAggregator, Logger logger, DisconnectionHandler disconnectionHandler, - AtomicBoolean shouldRecreateCall) { - this.metricAggregator = metricAggregator; + ResponseObserver(Logger logger, ChannelManager channelManager, MetricAggregator aggregator) { this.logger = logger; - this.disconnectionHandler = disconnectionHandler; - this.shouldRecreateCall = shouldRecreateCall; + this.channelManager = channelManager; + this.aggregator = aggregator; } @Override public void onNext(V1.RecordStatus value) { - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Response"); + aggregator.incrementCounter("Supportability/InfiniteTracing/Response"); } @Override public void onError(Throwable t) { - if (isChannelClosing(t)) { - logger.log(Level.FINE, "Stopping current gRPC call because the channel is closing."); - return; - } + Status status = Status.fromThrowable(t); - if (isAlpnError(t)) { - logger.log(Level.SEVERE, t, "ALPN does not appear to be supported on this JVM. Please install a supported JCE provider or update Java to use Infinite Tracing"); - metricAggregator.incrementCounter("Supportability/InfiniteTracing/NoALPNSupport"); - disconnectionHandler.terminate(); + // If the span observer is knowingly cancelled, it is canceled with a ChannelClosingException, + // which is detected here and ignored. + if (status.getCause() instanceof ChannelClosingException) { + logger.log(Level.FINE, "Stopping gRPC response observer because span observer was closed by another thread."); return; } - if (!isConnectionTimeoutException(t)) { - logger.log(Level.WARNING, t, "Encountered gRPC exception"); + if (isOkHttpALPNError(status)) { + logger.log(Level.SEVERE, t, + "ALPN does not appear to be supported on this JVM. Please install a supported JCE provider or update Java to use Infinite Tracing."); + aggregator.incrementCounter("Supportability/InfiniteTracing/NoALPNSupport"); + channelManager.shutdownChannelForever(); + return; } - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Response/Error"); + aggregator.incrementCounter("Supportability/InfiniteTracing/Span/gRPC/" + status.getCode()); + aggregator.incrementCounter("Supportability/InfiniteTracing/Response/Error"); - Status status = null; - if (t instanceof StatusRuntimeException) { - StatusRuntimeException statusRuntimeException = (StatusRuntimeException) t; - status = statusRuntimeException.getStatus(); - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Span/gRPC/" + status.getCode()); + if (status.getCode() == Status.Code.UNIMPLEMENTED) { + // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#unimplemented + logger.log(Level.WARNING, "Received gRPC status {0}, no longer permitting connections.", status.getCode().toString()); + channelManager.shutdownChannelForever(); + return; } - disconnectionHandler.handle(status); - } - - @Override - public void onCompleted() { - logger.log(Level.FINE, "Completing gRPC call."); - shouldRecreateCall.set(true); - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Response/Completed"); + shutdownChannelAndBackoff(status); } /** - * Detects if the error received was another thread knowingly cancelling the gRPC call. + * Determine if the error status indicates that ALPN support is not provided by this JVM. */ - private boolean isChannelClosing(Throwable t) { - return t instanceof StatusRuntimeException && t.getCause() instanceof ChannelClosingException; + private static boolean isOkHttpALPNError(Status status) { + // See https://github.com/grpc/grpc-java/blob/v1.28.1/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java#L96 + // It's the only mechanism we have to identify the problem. + return status.getCause() instanceof RuntimeException + && status.getCause().getMessage().startsWith("TLS ALPN negotiation failed with protocols"); } /** - * Detects if the error received was a connection timeout exception. This can happen if the agent hasn't sent any spans for more than 15 seconds. + * Shutdown and backoff the gRPC channel. The amount of time to backoff before allowing reconnection + * is dictated by the {@code status}. + * + * @param status the error status */ - private boolean isConnectionTimeoutException(Throwable t) { - return t instanceof StatusRuntimeException - && t.getMessage().startsWith("INTERNAL: No error: A GRPC status of OK should have been sent"); + @VisibleForTesting + void shutdownChannelAndBackoff(Status status) { + int backoffSeconds; + Level logLevel = Level.WARNING; + + if (isConnectTimeoutError(status)) { + backoffSeconds = 0; + logLevel = Level.FINE; + } else if (status.getCode() == Status.Code.FAILED_PRECONDITION) { + // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#failed_precondition + int nextIndex = backoffSequenceIndex.incrementAndGet(); + backoffSeconds = nextIndex < BACKOFF_SECONDS_SEQUENCE.length + ? BACKOFF_SECONDS_SEQUENCE[nextIndex] + : BACKOFF_SECONDS_SEQUENCE[BACKOFF_SECONDS_SEQUENCE.length - 1]; + } else { + // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#other-errors-1 + backoffSeconds = DEFAULT_BACKOFF_SECONDS; + } + + logger.log(logLevel, status.asException(), "Received gRPC status {0}.", status.getCode().toString()); + channelManager.shutdownChannelAndBackoff(backoffSeconds); } /** - * Attempts to detect if the error received indicates that ALPN support is not provided by this JVM. + * Determine if the error status is a connection timeout exception. This can happen if the agent hasn't sent + * any spans for more than 15 seconds. */ - private boolean isAlpnError(Throwable t) { - return t instanceof StatusRuntimeException - && t.getCause() instanceof RuntimeException - && isOkHttpALPNException((RuntimeException) t.getCause()); + private static boolean isConnectTimeoutError(Status status) { + return status.getCode() == Status.Code.INTERNAL + && status.getDescription() != null + && status.getDescription().startsWith("No error: A GRPC status of OK should have been sent"); } - private boolean isOkHttpALPNException(RuntimeException cause) { - // See https://github.com/grpc/grpc-java/blob/v1.28.1/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java#L96 - // It's the only mechanism we have to identify the problem. - return cause.getMessage() != null && cause.getMessage().startsWith("TLS ALPN negotiation failed with protocols"); + @Override + public void onCompleted() { + logger.log(Level.FINE, "Completing gRPC response observer."); + aggregator.incrementCounter("Supportability/InfiniteTracing/Response/Completed"); + channelManager.cancelSpanObserver(); } -} + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanConverter.java b/infinite-tracing/src/main/java/com/newrelic/SpanConverter.java deleted file mode 100644 index c1ec13a7cd..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/SpanConverter.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.model.SpanEvent; - -public interface SpanConverter { - T convert(SpanEvent event); -} diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanDelivery.java b/infinite-tracing/src/main/java/com/newrelic/SpanDelivery.java deleted file mode 100644 index ec9699497f..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/SpanDelivery.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.api.agent.Logger; -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.V1; -import io.grpc.stub.ClientCallStreamObserver; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; - -class SpanDelivery implements Runnable { - - private final SpanConverter spanConverter; - private final MetricAggregator metricAggregator; - private final Logger logger; - private final BlockingQueue queue; - private final Supplier> streamObserverSupplier; - - public SpanDelivery(SpanConverter spanConverter, MetricAggregator metricAggregator, Logger logger, BlockingQueue queue, - Supplier> streamObserverSupplier) { - this.spanConverter = spanConverter; - this.metricAggregator = metricAggregator; - this.logger = logger; - this.queue = queue; - this.streamObserverSupplier = streamObserverSupplier; - } - - @Override - public void run() { - ClientCallStreamObserver spanClientCallStreamObserver = streamObserverSupplier.get(); - - if (spanClientCallStreamObserver == null) { - return; - } - - if (!spanClientCallStreamObserver.isReady()) { - try { - metricAggregator.incrementCounter("Supportability/InfiniteTracing/NotReady"); - Thread.sleep(250); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - } - return; - } - - SpanEvent spanEvent = pollSafely(); - if (spanEvent == null) { - return; - } - - V1.Span outputSpan = spanConverter.convert(spanEvent); - - try { - spanClientCallStreamObserver.onNext(outputSpan); - } catch (Throwable t) { - logger.log(Level.SEVERE, t, "Unable to send span!"); - throw t; - } - - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Span/Sent"); - } - - private SpanEvent pollSafely() { - try { - return queue.poll(250, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - logger.log(Level.WARNING, "Thread was interrupted while polling for spans."); - Thread.currentThread().interrupt(); - return null; - } - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanEventConsumer.java b/infinite-tracing/src/main/java/com/newrelic/SpanEventConsumer.java deleted file mode 100644 index e7e90d7c65..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/SpanEventConsumer.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.newrelic; - -import com.google.common.annotations.VisibleForTesting; -import com.newrelic.agent.interfaces.backport.Consumer; -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.api.agent.Logger; -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.V1; -import io.grpc.ClientInterceptor; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; - -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Accepts a {@link SpanEvent} for publishing to the Trace Observer. - * - * Not thread-safe. - */ -public class SpanEventConsumer implements Consumer { - private final BlockingQueue queue; - private final MetricAggregator aggregator; - private final ConnectionHeaders connectionHeaders; - private final Runnable spanSender; - private final ExecutorService executorService; - private final AtomicBoolean wasStarted = new AtomicBoolean(false); - private volatile Future senderFuture; - - private SpanEventConsumer(BlockingQueue queue, MetricAggregator aggregator, ConnectionHeaders connectionHeaders, - Runnable spanSender, ExecutorService executorService) { - this.queue = queue; - this.aggregator = aggregator; - this.connectionHeaders = connectionHeaders; - this.spanSender = spanSender; - this.executorService = executorService; - } - - @Override - public void accept(SpanEvent spanEvent) { - aggregator.incrementCounter("Supportability/InfiniteTracing/Span/Seen"); - queue.offer(spanEvent); - } - - public static SpanEventConsumer.Builder builder(InfiniteTracingConfig config, MetricAggregator metricAggregator) { - return new Builder(config, metricAggregator); - } - - public void start() { - if (wasStarted.compareAndSet(false, true)) { - senderFuture = executorService.submit(spanSender); - } - } - - public void stop() { - if (wasStarted.compareAndSet(true,false)) { - senderFuture.cancel(true); - senderFuture = null; - } - } - - public void setConnectionMetadata(String newRunToken, Map headers) { - connectionHeaders.set(newRunToken, headers); - } - - public static class Builder { - private final InfiniteTracingConfig config; - private final SpanConverter spanConverter = new GrpcSpanConverter(); - private final MetricAggregator metricAggregator; - private final Logger logger; - private final BlockingQueue queue; - - private ChannelFactory channelFactory; - private StreamObserverFactory streamObserverFactory; - - public Builder(InfiniteTracingConfig config, MetricAggregator metricAggregator) { - this.logger = config.getLogger(); - this.queue = new LinkedBlockingQueue<>(config.getMaxQueueSize()); - this.metricAggregator = metricAggregator; - this.config = config; - } - - @VisibleForTesting - public Builder setChannelFactory(ChannelFactory channelFactory) { - this.channelFactory = channelFactory; - return this; - } - - @VisibleForTesting - public Builder setStreamObserverFactory(StreamObserverFactory streamObserverFactory) { - this.streamObserverFactory = streamObserverFactory; - return this; - } - - public SpanEventConsumer build() { - BackoffPolicy backoffPolicy = new DefaultBackoffPolicy(); - ConnectionStatus connectionStatus = new ConnectionStatus(logger); - - ConnectionHeaders connectionHeaders = new ConnectionHeaders(connectionStatus, logger, config.getLicenseKey()); - ClientInterceptor clientInterceptor = new HeadersInterceptor(connectionHeaders); - ClientInterceptor maybeInjectFlakyHeader = new FlakyHeaderInterceptor(config); - DisconnectionHandler disconnectionHandler = new DisconnectionHandler(connectionStatus, backoffPolicy, logger); - AtomicBoolean shouldRecreateCall = new AtomicBoolean(false); - - ResponseObserver responseObserver = new ResponseObserver(metricAggregator, logger, disconnectionHandler, shouldRecreateCall); - - ChannelFactory channelFactory = this.channelFactory != null - ? this.channelFactory - : new ChannelFactory(config, clientInterceptor, maybeInjectFlakyHeader); - - StreamObserverFactory streamObserverFactory = this.streamObserverFactory != null - ? this.streamObserverFactory - : new StreamObserverFactory(metricAggregator, responseObserver); - - Function> channelToStreamObserverConverter = - new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - - Supplier channelSupplier = new ChannelSupplier(channelFactory, connectionStatus, logger); - - Supplier> streamObserverSupplier = new StreamObserverSupplier(channelSupplier, channelToStreamObserverConverter); - - Runnable spanDeliveryConsumer = new SpanDelivery(spanConverter, metricAggregator, logger, queue, streamObserverSupplier); - - Runnable loopForever = new LoopForever(logger, spanDeliveryConsumer); - - ExecutorService executorService = Executors.newSingleThreadExecutor(new DaemonThreadFactory("Span Event Consumer")); - - return new SpanEventConsumer(queue, metricAggregator, connectionHeaders, loopForever, executorService); - } - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java b/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java new file mode 100644 index 0000000000..76f7a30a63 --- /dev/null +++ b/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java @@ -0,0 +1,138 @@ +package com.newrelic; + +import com.google.common.annotations.VisibleForTesting; +import com.newrelic.agent.model.SpanEvent; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import com.newrelic.trace.v1.V1; +import io.grpc.stub.ClientCallStreamObserver; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +class SpanEventSender implements Runnable { + + private final Logger logger; + private final BlockingQueue queue; + private final MetricAggregator aggregator; + private final ChannelManager channelManager; + + SpanEventSender(InfiniteTracingConfig config, BlockingQueue queue, MetricAggregator aggregator, ChannelManager channelManager) { + this.logger = config.getLogger(); + this.queue = queue; + this.aggregator = aggregator; + this.channelManager = channelManager; + } + + @Override + public void run() { + logger.log(Level.FINE, "Initializing {0}", this.getClass().getSimpleName()); + while (true) { + try { + pollAndWrite(); + } catch (Throwable t) { + logger.log(Level.SEVERE, t, "A problem occurred and no further spans will be sent."); + return; + } + } + } + + @VisibleForTesting + void pollAndWrite() { + // Get stream observer + ClientCallStreamObserver observer = channelManager.getSpanObserver(); + + // Confirm the observer is ready + if (!awaitReadyObserver(observer)) { + return; + } + + // Poll queue for span + SpanEvent span = pollSafely(); + if (span == null) { + return; + } + + // Convert span and write to observer + V1.Span convertedSpan = convert(span); + writeToObserver(observer, convertedSpan); + } + + @VisibleForTesting + boolean awaitReadyObserver(ClientCallStreamObserver observer) { + if (observer.isReady()) { + return true; + } + try { + logger.log(Level.FINE, "Waiting for gRPC span observer to be ready."); + aggregator.incrementCounter("Supportability/InfiniteTracing/NotReady"); + Thread.sleep(250); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted while awaiting ready gRPC span observer."); + } + return false; + } + + @VisibleForTesting + SpanEvent pollSafely() { + try { + return queue.poll(250, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted while polling for spans."); + } + } + + @VisibleForTesting + static V1.Span convert(SpanEvent spanEvent) { + Map intrinsicAttributes = copyAttributes(spanEvent.getIntrinsics()); + Map userAttributes = copyAttributes(spanEvent.getUserAttributesCopy()); + Map agentAttributes = copyAttributes(spanEvent.getAgentAttributes()); + + intrinsicAttributes.put("appName", V1.AttributeValue.newBuilder().setStringValue(spanEvent.getAppName()).build()); + + return V1.Span.newBuilder() + .setTraceId(spanEvent.getTraceId()) + .putAllIntrinsics(intrinsicAttributes) + .putAllAgentAttributes(agentAttributes) + .putAllUserAttributes(userAttributes) + .build(); + } + + private static Map copyAttributes(Map original) { + Map copy = new HashMap<>(); + if (original == null) { + return copy; + } + + for (Map.Entry entry : original.entrySet()) { + Object value = entry.getValue(); + if (value instanceof String) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setStringValue((String) value).build()); + } else if (value instanceof Long || value instanceof Integer) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setIntValue(((Number) value).longValue()).build()); + } else if (value instanceof Float || value instanceof Double) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setDoubleValue(((Number) value).doubleValue()).build()); + } else if (value instanceof Boolean) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setBoolValue((Boolean) value).build()); + } + } + return copy; + } + + @VisibleForTesting + void writeToObserver(ClientCallStreamObserver observer, V1.Span span) { + try { + observer.onNext(span); + } catch (Throwable t) { + logger.log(Level.SEVERE, t, "Unable to send span."); + throw t; + } + aggregator.incrementCounter("Supportability/InfiniteTracing/Span/Sent"); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/StreamObserverFactory.java b/infinite-tracing/src/main/java/com/newrelic/StreamObserverFactory.java deleted file mode 100644 index 02347abeef..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/StreamObserverFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.IngestServiceGrpc; -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; -import io.grpc.stub.StreamObserver; - -public class StreamObserverFactory { - private final MetricAggregator aggregator; - private final StreamObserver responseObserver; - - public StreamObserverFactory( - MetricAggregator aggregator, - StreamObserver responseObserver) { - this.aggregator = aggregator; - this.responseObserver = responseObserver; - } - - public ClientCallStreamObserver buildStreamObserver(ManagedChannel channel) { - IngestServiceGrpc.IngestServiceStub ingestServiceFutureStub = IngestServiceGrpc.newStub(channel); - ClientCallStreamObserver streamObserver = (ClientCallStreamObserver) ingestServiceFutureStub.recordSpan(responseObserver); - - aggregator.incrementCounter("Supportability/InfiniteTracing/Connect"); - return streamObserver; - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/StreamObserverSupplier.java b/infinite-tracing/src/main/java/com/newrelic/StreamObserverSupplier.java deleted file mode 100644 index 2aa1171785..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/StreamObserverSupplier.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; - -public class StreamObserverSupplier implements Supplier> { - - private final Supplier channelSupplier; - private final Function> channelToStreamObserverConverter; - - public StreamObserverSupplier(Supplier channelSupplier, - Function> channelToStreamObserverConverter) { - this.channelSupplier = channelSupplier; - this.channelToStreamObserverConverter = channelToStreamObserverConverter; - } - - @Override - public ClientCallStreamObserver get() { - return channelToStreamObserverConverter.apply(channelSupplier.get()); - } -} diff --git a/infinite-tracing/src/test/java/com/newrelic/ChannelFactoryTest.java b/infinite-tracing/src/test/java/com/newrelic/ChannelFactoryTest.java deleted file mode 100644 index 36800664b3..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ChannelFactoryTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.newrelic; - -import io.grpc.ClientInterceptor; -import io.grpc.ManagedChannel; -import io.grpc.okhttp.OkHttpChannelBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.Mockito.*; - -class ChannelFactoryTest { - - private InfiniteTracingConfig mockConfig; - - @BeforeEach - void setup() { - mockConfig = mock(InfiniteTracingConfig.class); - when(mockConfig.getHost()).thenReturn("localhost"); - when(mockConfig.getPort()).thenReturn(8989); - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - @Test - void shouldInjectAllAppropriateValues() { - ChannelFactory target = new ChannelFactory(mockConfig, new HeadersInterceptor[0]); - - ManagedChannel channel = target.createChannel(); - assertNotNull(channel); - - verify(mockConfig).getHost(); - verify(mockConfig).getPort(); - } - - @Test - void testDefaultConfigUsesSSL() { - - InfiniteTracingConfig config = InfiniteTracingConfig.builder() - .host("a") - .port(3) - .build(); - final OkHttpChannelBuilder builder = mock(OkHttpChannelBuilder.class); - ManagedChannel channel = mock(ManagedChannel.class); - - when(builder.defaultLoadBalancingPolicy("pick_first")).thenReturn(builder); - when(builder.intercept(any(ClientInterceptor[].class))).thenReturn(builder); - when(builder.enableRetry()).thenReturn(builder); - when(builder.defaultServiceConfig(isA(Map.class))).thenReturn(builder); - when(builder.build()).thenReturn(channel); - - ChannelFactory target = new ChannelFactory(config, new HeadersInterceptor[0]) { - @Override - OkHttpChannelBuilder newOkHttpChannelBuilder() { - return builder; - } - }; - - ManagedChannel resultChannel = target.createChannel(); - assertSame(channel, resultChannel); - verify(builder).useTransportSecurity(); - verify(builder, never()).usePlaintext(); - } -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ChannelManagerTest.java b/infinite-tracing/src/test/java/com/newrelic/ChannelManagerTest.java new file mode 100644 index 0000000000..f043e3ad5b --- /dev/null +++ b/infinite-tracing/src/test/java/com/newrelic/ChannelManagerTest.java @@ -0,0 +1,160 @@ +package com.newrelic; + +import com.google.common.collect.ImmutableMap; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import com.newrelic.trace.v1.IngestServiceGrpc.IngestServiceStub; +import com.newrelic.trace.v1.V1; +import io.grpc.ManagedChannel; +import io.grpc.stub.ClientCallStreamObserver; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ChannelManagerTest { + + @Mock + private Logger logger; + @Mock + private InfiniteTracingConfig config; + @Mock + private MetricAggregator aggregator; + @Mock + private ManagedChannel managedChannel; + @Mock + private ClientCallStreamObserver spanObserver; + @Mock + private ResponseObserver responseObserver; + @Mock + private IngestServiceStub stub; + + private ChannelManager target; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + when(config.getLogger()).thenReturn(logger); + target = spy(new ChannelManager(config, aggregator, "agentToken", ImmutableMap.of("key1", "value1"))); + doReturn(managedChannel).when(target).buildChannel(); + doReturn(stub).when(target).buildStub(managedChannel); + doReturn(responseObserver).when(target).buildResponseObserver(); + doReturn(spanObserver).when(stub).recordSpan(responseObserver); + } + + @Test + void getSpanObserver_ShutdownThrowsException() { + target.shutdownChannelForever(); + + assertThrows(RuntimeException.class, new Executable() { + @Override + public void execute() { + target.getSpanObserver(); + } + }); + } + + @Test + void getSpanObserver_BuildsChannelAndSpanObserverWhenMissing() { + assertEquals(spanObserver, target.getSpanObserver()); + assertEquals(spanObserver, target.getSpanObserver()); + + verify(target, times(1)).buildChannel(); + verify(target, times(1)).buildStub(managedChannel); + verify(target, times(1)).buildResponseObserver(); + verify(stub, times(1)).recordSpan(responseObserver); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Connect"); + } + + @Test + @Timeout(15) + void getSpanObserver_AwaitsBackoff() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(2); + + // Submit a task to initiate a backoff + final AtomicLong backoffCompletedAt = new AtomicLong(); + Future backoffFuture = executorService.submit(new Runnable() { + @Override + public void run() { + target.shutdownChannelAndBackoff(5); + backoffCompletedAt.set(System.currentTimeMillis()); + } + }); + + // Obtain a span observer in another thread, confirming it waits for the backoff to complete + final AtomicLong getSpanObserverCompletedAt = new AtomicLong(); + Future> futureSpanObserver = executorService.submit(new Callable>() { + @Override + public ClientCallStreamObserver call() { + try { + // Wait for the backoff task to have initiated backoff + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException("Thread interrupted while sleeping."); + } + + ClientCallStreamObserver response = target.getSpanObserver(); + getSpanObserverCompletedAt.set(System.currentTimeMillis()); + return response; + } + }); + + backoffFuture.get(); + assertEquals(spanObserver, futureSpanObserver.get()); + assertTrue(backoffCompletedAt.get() > 0); + assertTrue(getSpanObserverCompletedAt.get() > 0); + assertTrue(getSpanObserverCompletedAt.get() >= backoffCompletedAt.get()); + } + + @Test + void cancelSpanObserver_GetSpanObserverRebuildsWhenNextCalled() { + assertEquals(spanObserver, target.getSpanObserver()); + target.cancelSpanObserver(); + assertEquals(spanObserver, target.getSpanObserver()); + + verify(spanObserver).cancel(eq("CLOSING_CONNECTION"), any(ChannelClosingException.class)); + // Channel is only built once + verify(target, times(1)).buildChannel(); + // Span observer is built twice + verify(target, times(2)).buildStub(managedChannel); + verify(target, times(2)).buildResponseObserver(); + verify(stub, times(2)).recordSpan(responseObserver); + verify(aggregator, times(2)).incrementCounter("Supportability/InfiniteTracing/Connect"); + } + + @Test + void shutdownChannelAndBackoff_ShutsDownChannelCancelsSpanObserver() { + assertEquals(spanObserver, target.getSpanObserver()); + target.shutdownChannelAndBackoff(0); + assertEquals(spanObserver, target.getSpanObserver()); + + verify(target).cancelSpanObserver(); + // Channel and span observer is built twice + verify(target, times(2)).buildChannel(); + verify(target, times(2)).buildStub(managedChannel); + verify(target, times(2)).buildResponseObserver(); + verify(stub, times(2)).recordSpan(responseObserver); + verify(aggregator, times(2)).incrementCounter("Supportability/InfiniteTracing/Connect"); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ChannelSupplierTest.java b/infinite-tracing/src/test/java/com/newrelic/ChannelSupplierTest.java deleted file mode 100644 index 6abf6ed89c..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ChannelSupplierTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.ManagedChannel; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class ChannelSupplierTest { - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - @Mock - public ChannelFactory channelFactory; - @Mock - public ManagedChannel mockChannel; - @Mock - public ConnectionStatus connectionStatus; - - @Test - public void shouldCallFactoryIfNotAlreadyExisting() throws InterruptedException { - ChannelSupplier target = prepTargetForFirstCall(); - - assertSame(mockChannel, target.get()); - verify(channelFactory, times(1)).createChannel(); - } - - @Test - public void shouldNotCallFactoryIfAlreadyExisting() throws InterruptedException { - ChannelSupplier target = prepTargetForFirstCall(); - assertSame(mockChannel, target.get()); - verify(channelFactory, times(1)).createChannel(); - - when(connectionStatus.blockOnConnection()).thenReturn(ConnectionStatus.BlockResult.ALREADY_CONNECTED); - assertSame(mockChannel, target.get()); - verify(channelFactory, times(1)).createChannel(); - } - - @Test - public void shouldThrowIfGoingAway() throws InterruptedException { - final ChannelSupplier target = prepTargetForFirstCall(); - reset(connectionStatus); - when(connectionStatus.blockOnConnection()).thenReturn(ConnectionStatus.BlockResult.GO_AWAY_FOREVER); - - assertThrows(RuntimeException.class, new Executable() { - @Override - public void execute() { - target.get(); - } - }); - } - - @Test - public void shutsDownOldChannel() throws InterruptedException { - ChannelSupplier target = prepTargetForFirstCall(); - assertSame(mockChannel, target.get()); - - when(channelFactory.createChannel()).thenReturn(mock(ManagedChannel.class)); - assertNotSame(mockChannel, target.get()); - verify(mockChannel, times(1)).shutdown(); - } - - public ChannelSupplier prepTargetForFirstCall() throws InterruptedException { - when(channelFactory.createChannel()).thenReturn(mockChannel); - when(connectionStatus.blockOnConnection()).thenReturn(ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION); - return new ChannelSupplier(channelFactory, connectionStatus, mock(Logger.class)); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ChannelToStreamObserverTest.java b/infinite-tracing/src/test/java/com/newrelic/ChannelToStreamObserverTest.java deleted file mode 100644 index 125e06135c..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ChannelToStreamObserverTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.newrelic; - -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -class ChannelToStreamObserverTest { - - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - - public AtomicBoolean shouldRecreateCall = new AtomicBoolean(); - - @Mock - public StreamObserverFactory streamObserverFactory; - @Mock - public ManagedChannel mockChannel; - @Mock - public ClientCallStreamObserver streamObserver; - - @SuppressWarnings("unchecked") - public ClientCallStreamObserver mockStreamObserver() { - return (ClientCallStreamObserver) mock(ClientCallStreamObserver.class); - } - - @Test - public void returnsNullImmediatelyWithNullChannel() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - shouldRecreateCall.set(false); - when(streamObserverFactory.buildStreamObserver(null)) - .thenThrow(new AssertionError("~~ should not have been called ~~")); - - ClientCallStreamObserver result = target.apply(null); - assertNull(result); - verifyNoInteractions(streamObserverFactory); - } - - @Test - public void createsAndReturnsNewStreamObserver() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - shouldRecreateCall.set(false); - when(streamObserverFactory.buildStreamObserver(mockChannel)).thenReturn(streamObserver); - - ClientCallStreamObserver result = target.apply(mockChannel); - assertSame(streamObserver, result); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - } - - @Test - public void doesNotRecreateStreamObserverIfCalledWithSameChannel() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - shouldRecreateCall.set(false); - when(streamObserverFactory.buildStreamObserver(mockChannel)).thenReturn(streamObserver); - - ClientCallStreamObserver result = target.apply(mockChannel); - assertSame(streamObserver, result); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - - result = target.apply(mockChannel); - assertSame(streamObserver, result); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - } - - @Test - public void recreatesStreamObserverIfRequired() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - ClientCallStreamObserver mockObserver1 = mockStreamObserver(); - ClientCallStreamObserver mockObserver2 = mockStreamObserver(); - - shouldRecreateCall.set(false); - when(streamObserverFactory.buildStreamObserver(any(ManagedChannel.class))).thenReturn(mockObserver1).thenReturn(mockObserver2); - - ClientCallStreamObserver firstResult = target.apply(mockChannel); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - - shouldRecreateCall.set(true); - ClientCallStreamObserver secondResult = target.apply(mockChannel); - assertNotSame(secondResult, firstResult); - verify(streamObserverFactory, times(2)).buildStreamObserver(mockChannel); - } - - @Test - public void recreatesStreamObserverIfCalledWithDifferentChannel() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - ClientCallStreamObserver mockObserver1 = mockStreamObserver(); - ClientCallStreamObserver mockObserver2 = mockStreamObserver(); - - shouldRecreateCall.set(false); - when(streamObserver.isReady()).thenReturn(true); - when(streamObserverFactory.buildStreamObserver(any(ManagedChannel.class))).thenReturn(mockObserver1).thenReturn(mockObserver2); - - ClientCallStreamObserver result = target.apply(mockChannel); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - - ManagedChannel newChannel = mock(ManagedChannel.class); - ClientCallStreamObserver newResult = target.apply(newChannel); - assertNotSame(result, newResult); - verify(streamObserverFactory, times(1)).buildStreamObserver(newChannel); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ConnectionHeadersTest.java b/infinite-tracing/src/test/java/com/newrelic/ConnectionHeadersTest.java deleted file mode 100644 index 57eb8a6328..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ConnectionHeadersTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import org.junit.jupiter.api.Test; - -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -class ConnectionHeadersTest { - @Test - public void shouldUpdateConnectionStatus() { - ConnectionStatus connectionStatus = mock(ConnectionStatus.class); - ConnectionHeaders target = new ConnectionHeaders(connectionStatus, mock(Logger.class), "license key abc"); - - assertNull(target.get()); - - target.set("bar", Collections.singletonMap("OTHER_KEY", "other value")); - verify(connectionStatus).reattemptConnection(); - assertEquals("other value", target.get().get("OTHER_KEY")); - assertEquals("bar", target.get().get("agent_run_token")); - assertEquals("license key abc", target.get().get("license_key")); - } -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ConnectionStatusTest.java b/infinite-tracing/src/test/java/com/newrelic/ConnectionStatusTest.java deleted file mode 100644 index eb231fcfc9..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ConnectionStatusTest.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.Status; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; - -class ConnectionStatusTest { - @Test - public void onlyOneThreadStartsConnecting() throws InterruptedException { - final ConnectionStatus status = new ConnectionStatus(mock(Logger.class)); - final AtomicInteger numConnectingThreads = new AtomicInteger(0); - final AtomicInteger numFailingThreads = new AtomicInteger(0); - - int numThreads = 10; - List threads = new ArrayList<>(); - final CyclicBarrier barrier = new CyclicBarrier(numThreads); - - while (threads.size() < numThreads) { - final Thread thread = new Thread(new Runnable() { - @Override - public void run() { - try { - barrier.await(); - if (status.blockOnConnection() == ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION) { - numConnectingThreads.incrementAndGet(); - Thread.sleep(100); // emulate time it takes to establish connection - status.didConnect(); - } - } catch (Throwable t) { - numFailingThreads.incrementAndGet(); - } - } - }); - thread.setName("Thread " + threads.size()); - thread.start(); - threads.add(thread); - } - - for (Thread thread : threads) { - thread.join(2000); - if (thread.isAlive()) { - fail(thread.getName() + " is still alive"); - } - } - - assertEquals(0, numFailingThreads.get()); - assertEquals(1, numConnectingThreads.get()); - } - - @Test - public void onlyOneThreadBacksOff() throws InterruptedException { - final ConnectionStatus status = new ConnectionStatus(mock(Logger.class)); - status.didConnect(); - - final AtomicInteger numBackingOffThreads = new AtomicInteger(0); - final AtomicInteger numFailingThreads = new AtomicInteger(0); - - int numThreads = 10; - List threads = new ArrayList<>(); - final CyclicBarrier barrier = new CyclicBarrier(numThreads); - - while (threads.size() < numThreads) { - final Thread thread = new Thread(new Runnable() { - @Override - public void run() { - try { - barrier.await(); - if (status.shouldReconnect()) { - numBackingOffThreads.incrementAndGet(); - Thread.sleep(100); // emulate backoff time - status.reattemptConnection(); - } - } catch (Throwable t) { - numFailingThreads.incrementAndGet(); - } - } - }); - thread.setName("Thread " + threads.size()); - thread.start(); - threads.add(thread); - } - - for (Thread thread : threads) { - thread.join(2000); - if (thread.isAlive()) { - fail(thread.getName() + " is still alive"); - } - } - - assertEquals(0, numFailingThreads.get()); - assertEquals(1, numBackingOffThreads.get()); - } - - @Test - @Timeout(2) - public void shouldReconnectIfDisconnected() throws InterruptedException { - ConnectionStatus target = new ConnectionStatus(mock(Logger.class)); - assertEquals(ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION, target.blockOnConnection()); - target.didConnect(); - assertEquals(ConnectionStatus.BlockResult.ALREADY_CONNECTED, target.blockOnConnection()); - target.reattemptConnection(); - assertEquals(ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION, target.blockOnConnection()); - } - - @Test - public void shouldGoAwayIfToldTo() throws InterruptedException { - ConnectionStatus target = new ConnectionStatus(mock(Logger.class)); - assertEquals(ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION, target.blockOnConnection()); - target.didConnect(); - assertEquals(ConnectionStatus.BlockResult.ALREADY_CONNECTED, target.blockOnConnection()); - target.shutDownForever(); - assertEquals(ConnectionStatus.BlockResult.GO_AWAY_FOREVER, target.blockOnConnection()); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/DaemonThreadFactoryTest.java b/infinite-tracing/src/test/java/com/newrelic/DaemonThreadFactoryTest.java deleted file mode 100644 index e46f535250..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/DaemonThreadFactoryTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.newrelic; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class DaemonThreadFactoryTest { - @Test - public void createsDaemonThreads() { - DaemonThreadFactory target = new DaemonThreadFactory("service name"); - Thread result1 = target.newThread(new NoOpRunnable()); - assertTrue(result1.isDaemon()); - assertEquals("New Relic service name #1", result1.getName()); - - Thread result2 = target.newThread(new NoOpRunnable()); - assertTrue(result2.isDaemon()); - assertEquals("New Relic service name #2", result2.getName()); - } - - private static class NoOpRunnable implements Runnable { - @Override - public void run() { - } - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/DefaultBackoffPolicyTest.java b/infinite-tracing/src/test/java/com/newrelic/DefaultBackoffPolicyTest.java deleted file mode 100644 index 5f00ec22fb..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/DefaultBackoffPolicyTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.newrelic; - -import io.grpc.Status; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -import static org.junit.jupiter.api.Assertions.*; - -class DefaultBackoffPolicyTest { - @Test - public void shouldReconnectOnMostErrors() { - DefaultBackoffPolicy target = new DefaultBackoffPolicy(); - - assertTrue(target.shouldReconnect(null)); - assertTrue(target.shouldReconnect(Status.OK)); - assertTrue(target.shouldReconnect(Status.UNAVAILABLE)); - assertTrue(target.shouldReconnect(Status.CANCELLED)); - assertTrue(target.shouldReconnect(Status.DEADLINE_EXCEEDED)); - assertTrue(target.shouldReconnect(Status.UNAUTHENTICATED)); - } - - @Test - public void shouldNotReconnectOnUnimplemented() { - DefaultBackoffPolicy target = new DefaultBackoffPolicy(); - - assertFalse(target.shouldReconnect(Status.UNIMPLEMENTED)); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/DisconnectionHandlerTest.java b/infinite-tracing/src/test/java/com/newrelic/DisconnectionHandlerTest.java deleted file mode 100644 index 1adbaa7e68..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/DisconnectionHandlerTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.Status; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class DisconnectionHandlerTest { - @Test - public void shouldNotBackOffIfCannotSetStatus() { - DisconnectionHandler target = new DisconnectionHandler(connectionStatus, backoffPolicy, mock(Logger.class)); - when(connectionStatus.shouldReconnect()).thenReturn(false); - target.handle(null); - } - - @Test - public void shutdownIfShouldShutdown() { - DisconnectionHandler target = new DisconnectionHandler(connectionStatus, backoffPolicy, mock(Logger.class)); - when(connectionStatus.shouldReconnect()).thenReturn(true); - when(backoffPolicy.shouldReconnect(Status.ABORTED)).thenReturn(false); - target.handle(Status.ABORTED); - verify(connectionStatus).shutDownForever(); - } - - @Test - public void shouldBackOff() { - DisconnectionHandler target = new DisconnectionHandler(connectionStatus, backoffPolicy, mock(Logger.class)); - when(connectionStatus.shouldReconnect()).thenReturn(true); - when(backoffPolicy.shouldReconnect(Status.ABORTED)).thenReturn(true); - target.handle(Status.ABORTED); - verify(backoffPolicy).shouldReconnect(Status.ABORTED); - verify(connectionStatus).reattemptConnection(); - } - - @Test - public void shouldShutdownForeverOnUnimplemented() { - DisconnectionHandler target = new DisconnectionHandler(connectionStatus, backoffPolicy, mock(Logger.class)); - when(connectionStatus.shouldReconnect()).thenReturn(true); - when(backoffPolicy.shouldReconnect(Status.UNIMPLEMENTED)).thenReturn(false); - target.handle(Status.UNIMPLEMENTED); - verify(connectionStatus).shutDownForever(); - } - - @Mock - public ConnectionStatus connectionStatus; - - @Mock - public BackoffPolicy backoffPolicy; - - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/FlakyHeaderInterceptorTest.java b/infinite-tracing/src/test/java/com/newrelic/FlakyHeaderInterceptorTest.java deleted file mode 100644 index cbf910588e..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/FlakyHeaderInterceptorTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.InternalMetadata; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class FlakyHeaderInterceptorTest { - - private static final Metadata.Key K_KEY = Metadata.Key.of("k1", ASCII_STRING_MARSHALLER); - public static final Metadata.Key FLAKY_KEY = Metadata.Key.of("flaky", ASCII_STRING_MARSHALLER); - Metadata originalHeaders = InternalMetadata.newMetadata(); - - @BeforeEach - void setup() { - originalHeaders = InternalMetadata.newMetadata(); - originalHeaders.put(K_KEY, "v1"); - } - - @Test - void testInjectHeader() { - final Double flakyValue = 15.6; - InfiniteTracingConfig config = InfiniteTracingConfig.builder() - .logger(mock(Logger.class)) - .flakyPercentage(flakyValue) - .build(); - - MethodDescriptor method = mock(MethodDescriptor.class); - CallOptions callOptions = mock(CallOptions.class); - Channel next = mock(Channel.class); - MockForwardingClientCall newCallNext = new MockForwardingClientCall(); - ClientCall.Listener responseListener = new ClientCall.Listener() { - }; - - when(next.newCall(method, callOptions)).thenReturn(newCallNext); - - FlakyHeaderInterceptor testClass = new FlakyHeaderInterceptor(config); - - ClientCall result = testClass.interceptCall(method, callOptions, next); - - result.start(responseListener, originalHeaders); - assertEquals(1, newCallNext.seenHeadersSize()); - assertEquals("v1", newCallNext.getHeader(K_KEY)); - assertEquals("15.6", newCallNext.getHeader(FLAKY_KEY)); - } - - @Test - void testNotConfigured() { - InfiniteTracingConfig config = InfiniteTracingConfig.builder().build(); - - MethodDescriptor method = mock(MethodDescriptor.class); - CallOptions callOptions = mock(CallOptions.class); - Channel next = mock(Channel.class); - MockForwardingClientCall newCallNext = new MockForwardingClientCall(); - ClientCall.Listener responseListener = new ClientCall.Listener() { - }; - - when(next.newCall(method, callOptions)).thenReturn(newCallNext); - - FlakyHeaderInterceptor testClass = new FlakyHeaderInterceptor(config); - - ClientCall result = testClass.interceptCall(method, callOptions, next); - - result.start(responseListener, originalHeaders); - assertEquals(1, newCallNext.seenHeadersSize()); - assertEquals("v1", newCallNext.getHeader(K_KEY)); - assertFalse(newCallNext.containsKey(FLAKY_KEY)); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/GrpcSpanConverterTest.java b/infinite-tracing/src/test/java/com/newrelic/GrpcSpanConverterTest.java deleted file mode 100644 index 8e9ecd8069..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/GrpcSpanConverterTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.trace.v1.V1; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class GrpcSpanConverterTest { - private enum TestEnum { ONE } - - @Test - public void shouldSerializeIntrinsicAttributesOK() throws IOException { - SpanEvent spanEvent = makeSpanWithIntrinsics(); - - V1.Span deserialized = from(spanEvent); - assertEquals("my app", deserialized.getIntrinsicsOrThrow("appName").getStringValue()); - assertEquals("value", deserialized.getIntrinsicsOrThrow("intrStr").getStringValue()); - assertEquals(12345, deserialized.getIntrinsicsOrThrow("intrInt").getIntValue()); - assertEquals(3.14, deserialized.getIntrinsicsOrThrow("intrFloat").getDoubleValue(), 0.00001); - assertTrue(deserialized.getIntrinsicsOrThrow("intrBool").getBoolValue()); - - assertFalse(deserialized.containsIntrinsics("intrOther")); - } - - @Test - public void shouldStoreTraceIdBothPlaces() throws IOException { - SpanEvent spanEvent = makeSpanWithIntrinsics(); - - V1.Span deserialized = from(spanEvent); - assertEquals("abc123", deserialized.getTraceId()); - assertEquals("abc123", deserialized.getIntrinsicsOrThrow("traceId").getStringValue()); - } - - private V1.Span from(SpanEvent spanEvent) throws IOException { - GrpcSpanConverter target = new GrpcSpanConverter(); - V1.Span result = target.convert(spanEvent); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - result.writeTo(baos); - return V1.Span.parseFrom(baos.toByteArray()); - } - - private SpanEvent makeSpanWithIntrinsics() { - return SpanEvent.builder() - .appName("my app") - .putIntrinsic("traceId", "abc123") - .putIntrinsic("intrStr", "value") - .putIntrinsic("intrInt", 12345) - .putIntrinsic("intrFloat", 3.14) - .putIntrinsic("intrBool", true) - .putIntrinsic("intrOther", TestEnum.ONE) - .build(); - } -} diff --git a/infinite-tracing/src/test/java/com/newrelic/HeadersInterceptorTest.java b/infinite-tracing/src/test/java/com/newrelic/HeadersInterceptorTest.java index 5e9c3b55b2..7ba99ad77f 100644 --- a/infinite-tracing/src/test/java/com/newrelic/HeadersInterceptorTest.java +++ b/infinite-tracing/src/test/java/com/newrelic/HeadersInterceptorTest.java @@ -1,15 +1,17 @@ package com.newrelic; import com.google.common.collect.ImmutableMap; -import com.newrelic.agent.interfaces.backport.Supplier; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; +import io.grpc.ForwardingClientCall; import io.grpc.InternalMetadata; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; @@ -20,7 +22,7 @@ class HeadersInterceptorTest { @Test - void testInjectHeader() { + void interceptCall_Valid() { MethodDescriptor method = mock(MethodDescriptor.class); CallOptions callOptions = mock(CallOptions.class); Channel next = mock(Channel.class); @@ -30,17 +32,10 @@ void testInjectHeader() { when(next.newCall(method, callOptions)).thenReturn(newCallNext); - Supplier> headerSupplier = new Supplier>() { - @Override - public Map get() { - return ImmutableMap.of( - "header1", "value1", - "WILL_BE_LOWER_CASED", "value2" - ); - } - }; - - HeadersInterceptor target = new HeadersInterceptor(headerSupplier); + Map headers = ImmutableMap.of( + "header1", "value1", + "WILL_BE_LOWER_CASED", "value2"); + HeadersInterceptor target = new HeadersInterceptor(headers); ClientCall result = target.interceptCall(method, callOptions, next); @@ -53,8 +48,28 @@ public Map get() { assertEquals("value2", newCallNext.getHeader(keyFor("will_be_lower_cased"))); } - private Metadata.Key keyFor(String key) { + private static Metadata.Key keyFor(String key) { return Metadata.Key.of(key, ASCII_STRING_MARSHALLER); } + static class MockForwardingClientCall extends ForwardingClientCall { + private final List seenHeaders = new ArrayList<>(); + + @Override + public void start(Listener responseListener, Metadata headers) { + seenHeaders.add(headers); + super.start(responseListener, headers); + } + + @Override + protected ClientCall delegate() { + return mock(ClientCall.class); + } + + public String getHeader(Metadata.Key key) { + return seenHeaders.get(0).get(key); + } + + } + } \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/InfiniteTracingTest.java b/infinite-tracing/src/test/java/com/newrelic/InfiniteTracingTest.java new file mode 100644 index 0000000000..c25c403f21 --- /dev/null +++ b/infinite-tracing/src/test/java/com/newrelic/InfiniteTracingTest.java @@ -0,0 +1,86 @@ +package com.newrelic; + +import com.google.common.collect.ImmutableMap; +import com.newrelic.agent.model.SpanEvent; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class InfiniteTracingTest { + + @Mock + private Logger logger; + @Mock + private InfiniteTracingConfig config; + @Mock + private MetricAggregator aggregator; + @Mock + private ExecutorService executorService; + @Mock + private ChannelManager channelManager; + @Mock + private SpanEventSender spanEventSender; + + private LinkedBlockingDeque queue; + private InfiniteTracing target; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + when(config.getLogger()).thenReturn(logger); + queue = new LinkedBlockingDeque<>(1); + target = spy(new InfiniteTracing(config, aggregator, executorService, queue)); + doReturn(channelManager).when(target).buildChannelManager(anyString(), ArgumentMatchers.anyMap()); + doReturn(spanEventSender).when(target).buildSpanEventSender(); + } + + @Test + void startAndStop() { + Future future = mock(Future.class); + when(executorService.submit(ArgumentMatchers.any())).thenReturn(future); + + target.start("token1", ImmutableMap.of("key1", "value1")); + target.start("token2", ImmutableMap.of("key2", "value2")); + + verify(target).buildChannelManager("token1", ImmutableMap.of("key1", "value1")); + verify(target).buildSpanEventSender(); + verify(executorService).submit(spanEventSender); + verify(channelManager).updateMetadata("token2", ImmutableMap.of("key2", "value2")); + verify(channelManager).shutdownChannelAndBackoff(0); + + target.stop(); + target.stop(); + + verify(future).cancel(true); + verify(channelManager).shutdownChannelForever(); + } + + @Test + @Timeout(1) + void accept_IncrementsCounterAndOffersToQueue() { + SpanEvent spanEvent = SpanEvent.builder().build(); + + target.accept(spanEvent); + + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Span/Seen"); + assertEquals(spanEvent, queue.poll()); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/MockForwardingClientCall.java b/infinite-tracing/src/test/java/com/newrelic/MockForwardingClientCall.java deleted file mode 100644 index f64a33947a..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/MockForwardingClientCall.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.newrelic; - -import io.grpc.ClientCall; -import io.grpc.ForwardingClientCall; -import io.grpc.Metadata; - -import java.util.ArrayList; -import java.util.List; - -import static org.mockito.Mockito.mock; - -class MockForwardingClientCall extends ForwardingClientCall { - private final List seenHeaders = new ArrayList<>(); - - @Override - public void start(Listener responseListener, Metadata headers) { - seenHeaders.add(headers); - super.start(responseListener, headers); - } - - @Override - protected ClientCall delegate() { - return mock(ClientCall.class); - } - - public int seenHeadersSize() { - return seenHeaders.size(); - } - - public String getHeader(Metadata.Key key) { - return seenHeaders.get(0).get(key); - } - - public boolean containsKey(Metadata.Key key) { - return seenHeaders.get(0).containsKey(key); - } -} diff --git a/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java b/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java index a107c2a9b8..1d3423e4fa 100644 --- a/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java +++ b/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java @@ -5,171 +5,126 @@ import com.newrelic.trace.v1.V1; import io.grpc.Status; import io.grpc.StatusRuntimeException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; +import static com.newrelic.ResponseObserver.BACKOFF_SECONDS_SEQUENCE; +import static com.newrelic.ResponseObserver.DEFAULT_BACKOFF_SECONDS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; class ResponseObserverTest { - AtomicBoolean shouldRecreateCall = new AtomicBoolean(); + @Mock + private Logger logger; + @Mock + private ChannelManager channelManager; + @Mock + private MetricAggregator aggregator; - @Test - public void shouldIncrementCounterOnNext() { - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - mock(DisconnectionHandler.class), shouldRecreateCall); + private ResponseObserver target; - target.onNext(V1.RecordStatus.newBuilder().setMessagesSeen(3000).build()); - - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Response"); + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + target = spy(new ResponseObserver(logger, channelManager, aggregator)); } @Test - public void shouldDisconnectOnNormalException() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - disconnectionHandler, shouldRecreateCall); + void onNext_IncrementsCounter() { + target.onNext(V1.RecordStatus.newBuilder().build()); - target.onError(new Throwable()); - - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Response/Error"); - verify(disconnectionHandler).handle(null); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Response"); } @Test - public void shouldReportStatusOnError() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - disconnectionHandler, shouldRecreateCall); - - StatusRuntimeException exception = new StatusRuntimeException(Status.CANCELLED); + void onError_ChannelClosingExceptionReturns() { + StatusRuntimeException exception = Status.CANCELLED.withCause(new ChannelClosingException()).asRuntimeException(); target.onError(exception); - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Span/gRPC/CANCELLED"); - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Response/Error"); - verify(disconnectionHandler).handle(Status.CANCELLED); + verifyNoInteractions(channelManager, aggregator); } @Test - public void shouldNotDisconnectWhenChannelClosing() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - disconnectionHandler, shouldRecreateCall); + void onError_AlpnErrorShutdownChannelForever() { + RuntimeException cause = new RuntimeException("TLS ALPN negotiation failed with protocols: [h2]"); + StatusRuntimeException exception = Status.UNAVAILABLE.withCause(cause).asRuntimeException(); - StatusRuntimeException exception = Status.CANCELLED.withCause(new ChannelClosingException()).asRuntimeException(); target.onError(exception); - verifyNoInteractions(disconnectionHandler, metricAggregator); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/NoALPNSupport"); + verify(channelManager).shutdownChannelForever(); } @Test - public void shouldDisconnectOnCompleted() { - DisconnectionHandler mockHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - mockHandler, shouldRecreateCall); - - target.onCompleted(); + void onError_UnimplementedShutdownChannelForever() { + target.onError(Status.UNIMPLEMENTED.asException()); - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Response/Completed"); - assertTrue(shouldRecreateCall.get()); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Span/gRPC/" + Status.UNIMPLEMENTED.getCode()); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Response/Error"); + verify(channelManager).shutdownChannelForever(); } @Test - public void shouldTerminateOnALPNError() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); + void onError_OtherStatusShutdownChannelAndBackoff() { + doNothing().when(target).shutdownChannelAndBackoff(ArgumentMatchers.any()); - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - disconnectionHandler, shouldRecreateCall); + target.onError(Status.INTERNAL.asException()); + target.onError(Status.FAILED_PRECONDITION.asException()); + target.onError(Status.UNKNOWN.asException()); - RuntimeException cause = new RuntimeException("TLS ALPN negotiation failed with protocols: [h2]"); - StatusRuntimeException exception = Status.UNAVAILABLE.withCause(cause).asRuntimeException(); - - target.onError(exception); - - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/NoALPNSupport"); - verify(disconnectionHandler).terminate(); + verify(aggregator, times(6)).incrementCounter(anyString()); + verify(target, times(3)).shutdownChannelAndBackoff(ArgumentMatchers.any()); } @Test - public void testIsConnectionTimeoutException() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - Logger logger = mock(Logger.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - logger, - disconnectionHandler, shouldRecreateCall); - - Throwable exception = new StatusRuntimeException( - Status.fromCode(Status.Code.INTERNAL).withDescription("No error: A GRPC status of OK should have been sent\nRst Stream")); - target.onError(exception); + void shutdownChannelAndBackoff_ConnectTimeoutBackoffZeroSeconds() { + Status status = Status.fromCode(Status.Code.INTERNAL).withDescription("No error: A GRPC status of OK should have been sent\nRst Stream"); - verify(logger, never()).log(Level.WARNING, exception, "Encountered gRPC exception"); + target.shutdownChannelAndBackoff(status); + + verify(logger).log(eq(Level.FINE), any(Throwable.class), anyString(), any()); + verify(channelManager).shutdownChannelAndBackoff(0); } @Test - public void testConnectionTimeoutExceptionWrongType() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - Logger logger = mock(Logger.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - logger, - disconnectionHandler, shouldRecreateCall); - - Throwable exception = new RuntimeException("No error: A GRPC status of OK should have been sent\nRst Stream"); - target.onError(exception); - - verify(logger).log(Level.WARNING, exception, "Encountered gRPC exception"); + void shutdownChannelAndBackoff_FailedPreconditionBackoffSequence() { + for (int i = 0; i < 10; i++) { + target.shutdownChannelAndBackoff(Status.FAILED_PRECONDITION); + } + + verify(logger, times(10)).log(eq(Level.WARNING), any(Throwable.class), anyString(), any()); + verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(BACKOFF_SECONDS_SEQUENCE[0]); + verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(BACKOFF_SECONDS_SEQUENCE[BACKOFF_SECONDS_SEQUENCE.length - 1]); } @Test - public void testConnectionTimeoutExceptionWrongMessage() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - Logger logger = mock(Logger.class); + void shutdownChannelAndBackoff_OtherStatusDefaultBackoff() { + target.shutdownChannelAndBackoff(Status.UNKNOWN); - ResponseObserver target = new ResponseObserver( - metricAggregator, - logger, - disconnectionHandler, shouldRecreateCall); + verify(logger).log(eq(Level.WARNING), any(Throwable.class), anyString(), any()); + verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(DEFAULT_BACKOFF_SECONDS); + } - Throwable exception = new StatusRuntimeException(Status.fromCode(Status.Code.INTERNAL).withDescription("A REALLY BAD ERROR: PRINT ME")); - target.onError(exception); + @Test + void onCompleted_IncrementsCounterCancelsSpanObserver() { + target.onCompleted(); - verify(logger).log(Level.WARNING, exception, "Encountered gRPC exception"); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Response/Completed"); + verify(channelManager).cancelSpanObserver(); } } \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/SpanDeliveryTest.java b/infinite-tracing/src/test/java/com/newrelic/SpanDeliveryTest.java deleted file mode 100644 index 0674b5d97d..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/SpanDeliveryTest.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.api.agent.Logger; -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.V1; -import io.grpc.stub.ClientCallStreamObserver; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -class SpanDeliveryTest { - - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - - public BlockingQueue incomingQueue = new LinkedBlockingQueue<>(); - - @Mock - public SpanConverter spanConverter; - @Mock - public MetricAggregator metricAggregator; - @Mock - public Logger logger; - @Mock - public Supplier> streamObserverSupplier; - - @SuppressWarnings("unchecked") - public ClientCallStreamObserver mockStreamObserver() { - return (ClientCallStreamObserver) mock(ClientCallStreamObserver.class); - } - - @Test - public void noCallsIfStreamObserverNull() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - when(streamObserverSupplier.get()).thenReturn(null); - - incomingQueue.add(SpanEvent.builder().build()); - when(spanConverter.convert(any(SpanEvent.class))).thenAnswer(new AlwaysNewSpan()); - - target.run(); - verifyNoInteractions(spanConverter, metricAggregator, logger); - assertEquals(1, incomingQueue.size()); - } - - @Test - public void returnsIfStreamObserverNotReady() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - incomingQueue.add(SpanEvent.builder().build()); - when(spanConverter.convert(any(SpanEvent.class))).thenAnswer(new AlwaysNewSpan()); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(false); - target.run(); - - verify(mockObserver, times(1)).isReady(); - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/NotReady"); - verifyNoInteractions(spanConverter, logger); - } - - @Test - public void doesNotCallOnNextIfQueueEmpty() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - when(spanConverter.convert(any(SpanEvent.class))).thenThrow(new AssertionError("should not convert")); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(true); - target.run(); - - verify(mockObserver, times(1)).isReady(); - verify(mockObserver, never()).onNext(any(V1.Span.class)); - } - - @Test - public void checksStreamObserverReadyAndCallsOnNext() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - incomingQueue.add(SpanEvent.builder().build()); - V1.Span mockSpan = mock(V1.Span.class); - when(spanConverter.convert(any(SpanEvent.class))).thenReturn(mockSpan); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(true); - target.run(); - - verify(mockObserver, times(1)).isReady(); - verify(mockObserver, times(1)).onNext(mockSpan); - } - - @Test - public void doesNotIncrementSentIfOnNextThrows() { - final SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - incomingQueue.add(SpanEvent.builder().build()); - V1.Span mockSpan = mock(V1.Span.class); - when(spanConverter.convert(any(SpanEvent.class))).thenReturn(mockSpan); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(true); - doThrow(new RuntimeException("~~ oops ~~")).when(mockObserver).onNext(any(V1.Span.class)); - - assertThrows(RuntimeException.class, new Executable() { - @Override - public void execute() { - target.run(); - } - }); - verifyNoInteractions(metricAggregator); - } - - @Test - public void incrementsMetricOnSuccess() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - incomingQueue.add(SpanEvent.builder().build()); - V1.Span mockSpan = mock(V1.Span.class); - when(spanConverter.convert(any(SpanEvent.class))).thenReturn(mockSpan); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(true); - target.run(); - - verify(metricAggregator, times(1)).incrementCounter(anyString()); - } - - - private static class AlwaysNewSpan implements Answer { - @Override - public Object answer(InvocationOnMock invocation) { - return V1.Span.newBuilder().build(); - } - } -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/SpanEventConsumerTest.java b/infinite-tracing/src/test/java/com/newrelic/SpanEventConsumerTest.java deleted file mode 100644 index 396c1a4346..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/SpanEventConsumerTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.api.agent.Logger; -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.exceptions.verification.WantedButNotInvoked; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class SpanEventConsumerTest { - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - - @Mock - public ChannelFactory mockChannelFactory; - @Mock - public ManagedChannel mockChannel; - @Mock - public StreamObserverFactory mockStreamObserverFactory; - @Mock - public ClientCallStreamObserver mockStreamObserver; - @Mock - public Logger mockLogger; - @Mock - public MetricAggregator metricAggregator; - - @Test - @Timeout(30) - public void integrationTest() throws InterruptedException { - when(mockChannelFactory.createChannel()).thenReturn(mockChannel); - when(mockStreamObserverFactory.buildStreamObserver(mockChannel)).thenReturn(mockStreamObserver); - when(mockStreamObserver.isReady()).thenReturn(true); - - InfiniteTracingConfig config = InfiniteTracingConfig.builder() - .logger(mockLogger) - .maxQueueSize(10) - .build(); - - SpanEventConsumer target = SpanEventConsumer.builder(config, metricAggregator) - .setChannelFactory(mockChannelFactory) - .setStreamObserverFactory(mockStreamObserverFactory) - .build(); - - target.start(); - - SpanEvent incomingEvent = SpanEvent.builder() - .putIntrinsic("traceId", "123abc") - .putIntrinsic("guid", "some-intrinsic") - .appName("app-name") - .build(); - - target.accept(incomingEvent); - - while (true) { - ArgumentCaptor outgoingSpanCaptor = ArgumentCaptor.forClass(V1.Span.class); - try { - verify(mockStreamObserver, times(1)).onNext(outgoingSpanCaptor.capture()); - } catch (WantedButNotInvoked ignored) { - Thread.sleep(10); - continue; - } - - V1.Span capturedSpan = outgoingSpanCaptor.getValue(); - - assertEquals("123abc", capturedSpan.getTraceId()); - assertEquals("some-intrinsic", capturedSpan.getIntrinsicsOrThrow("guid").getStringValue()); - assertEquals("app-name", capturedSpan.getIntrinsicsOrThrow("appName").getStringValue()); - break; - } - - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java b/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java new file mode 100644 index 0000000000..bca065bfa6 --- /dev/null +++ b/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java @@ -0,0 +1,196 @@ +package com.newrelic; + +import com.newrelic.agent.model.SpanEvent; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import com.newrelic.trace.v1.V1; +import io.grpc.stub.ClientCallStreamObserver; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.function.Executable; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import static com.newrelic.SpanEventSender.convert; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class SpanEventSenderTest { + + @Mock + private Logger logger; + @Mock + private InfiniteTracingConfig config; + @Mock + private BlockingQueue queue; + @Mock + private MetricAggregator aggregator; + @Mock + private ChannelManager channelManager; + @Mock + private ClientCallStreamObserver observer; + + private SpanEventSender target; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + when(config.getLogger()).thenReturn(logger); + when(channelManager.getSpanObserver()).thenReturn(observer); + target = spy(new SpanEventSender(config, queue, aggregator, channelManager)); + } + + @Test + @Timeout(5) + void run_CallsPollAndWriteUntilException() { + doNothing() + .doNothing() + .doThrow(new RuntimeException("Error!")) + .when(target).pollAndWrite(); + + target.run(); + verify(target, times(3)).pollAndWrite(); + } + + @Test + void pollAndWrite_ObserverNotReadyDoesNotPoll() { + doReturn(false).when(target).awaitReadyObserver(observer); + + target.pollAndWrite(); + + verify(target, never()).pollSafely(); + verify(target, never()).writeToObserver(ArgumentMatchers.>any(), ArgumentMatchers.any()); + } + + @Test + void pollAndWrite_NullSpanDoesNotWrite() { + doReturn(true).when(target).awaitReadyObserver(observer); + doReturn(null).when(target).pollSafely(); + + target.pollAndWrite(); + + verify(target, never()).writeToObserver(ArgumentMatchers.>any(), ArgumentMatchers.any()); + } + + @Test + void pollAndWrite_ObserverReadySpanAvailableWrites() { + SpanEvent spanEvent = buildSpanEvent(); + doReturn(true).when(target).awaitReadyObserver(observer); + doReturn(spanEvent).when(target).pollSafely(); + + target.pollAndWrite(); + + verify(target).writeToObserver(observer, convert(spanEvent)); + } + + @Test + void awaitReadyObserver_NotReadySleepsIncrementsCounter() { + long startTime = System.currentTimeMillis(); + when(observer.isReady()).thenReturn(false); + + assertFalse(target.awaitReadyObserver(observer)); + assertTrue(System.currentTimeMillis() - startTime >= 250); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/NotReady"); + } + + @Test + void awaitReadyObserver_IsReadyReturnsTrue() { + when(observer.isReady()).thenReturn(true); + + assertTrue(target.awaitReadyObserver(observer)); + } + + @Test + void pollSafely_InterruptedThrowsException() throws InterruptedException { + doThrow(new InterruptedException()).when(queue).poll(anyLong(), ArgumentMatchers.any()); + + assertThrows(RuntimeException.class, new Executable() { + @Override + public void execute() { + target.pollSafely(); + } + }); + } + + @Test + void pollSafely_Valid() throws InterruptedException { + SpanEvent spanEvent = buildSpanEvent(); + + when(queue.poll(anyLong(), ArgumentMatchers.any())).thenReturn(spanEvent); + + assertEquals(spanEvent, target.pollSafely()); + } + + @Test + void convert_Valid() throws IOException { + SpanEvent spanEvent = buildSpanEvent(); + + V1.Span result = convert(spanEvent); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + result.writeTo(baos); + V1.Span deserialized = V1.Span.parseFrom(baos.toByteArray()); + + assertEquals("abc123", deserialized.getTraceId()); + assertEquals("abc123", deserialized.getIntrinsicsOrThrow("traceId").getStringValue()); + assertEquals("my app", deserialized.getIntrinsicsOrThrow("appName").getStringValue()); + assertEquals("value", deserialized.getIntrinsicsOrThrow("intrStr").getStringValue()); + assertEquals(12345, deserialized.getIntrinsicsOrThrow("intrInt").getIntValue()); + assertEquals(3.14, deserialized.getIntrinsicsOrThrow("intrFloat").getDoubleValue(), 0.00001); + assertTrue(deserialized.getIntrinsicsOrThrow("intrBool").getBoolValue()); + assertFalse(deserialized.containsIntrinsics("intrOther")); + } + + private SpanEvent buildSpanEvent() { + return SpanEvent.builder() + .appName("my app") + .putIntrinsic("traceId", "abc123") + .putIntrinsic("intrStr", "value") + .putIntrinsic("intrInt", 12345) + .putIntrinsic("intrFloat", 3.14) + .putIntrinsic("intrBool", true) + .putIntrinsic("intrOther", TestEnum.ONE) + .build(); + } + + private enum TestEnum { ONE } + + @Test + void writeToObserver_RethrowsException() { + doThrow(new RuntimeException("Error!")).when(observer).onNext(ArgumentMatchers.any()); + + assertThrows(RuntimeException.class, new Executable() { + @Override + public void execute() { + target.writeToObserver(observer, V1.Span.newBuilder().build()); + } + }); + verify(aggregator, never()).incrementCounter(anyString()); + } + + @Test + void writeToObserver_NoExceptionIncrementsCounter() { + target.writeToObserver(observer, V1.Span.newBuilder().build()); + + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Span/Sent"); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/StreamObserverSupplierTest.java b/infinite-tracing/src/test/java/com/newrelic/StreamObserverSupplierTest.java deleted file mode 100644 index a15874c1f3..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/StreamObserverSupplierTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class StreamObserverSupplierTest { - @Mock - public Supplier channelSupplier; - @Mock - public Function> channelToStreamObserverConverter; - @Mock - public ManagedChannel managedChannel; - @Mock - public ClientCallStreamObserver streamObserver; - - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void relaysCallsThrough() { - when(channelSupplier.get()).thenReturn(managedChannel); - when(channelToStreamObserverConverter.apply(managedChannel)).thenReturn(streamObserver); - - StreamObserverSupplier target = new StreamObserverSupplier(channelSupplier, channelToStreamObserverConverter); - assertSame(streamObserver, target.get()); - verify(channelSupplier).get(); - verify(channelToStreamObserverConverter).apply(managedChannel); - } -} \ No newline at end of file diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfig.java index c724c2128b..7b87c213dd 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfig.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfig.java @@ -16,6 +16,8 @@ public interface InfiniteTracingConfig { Double getFlakyPercentage(); + Long getFlakyCode(); + boolean getUsePlaintext(); boolean isEnabled(); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfigImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfigImpl.java index 7020ab2803..62143d57f9 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfigImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfigImpl.java @@ -19,6 +19,7 @@ public class InfiniteTracingConfigImpl extends BaseConfig implements InfiniteTra public static final String TRACE_OBSERVER = "trace_observer"; public static final String SPAN_EVENTS = "span_events"; public static final String FLAKY_PERCENTAGE = "_flakyPercentage"; + public static final String FLAKY_CODE = "_flakyCode"; public static final String USE_PLAINTEXT = "plaintext"; public static final boolean DEFAULT_USE_PLAINTEXT = false; @@ -72,6 +73,11 @@ public Double getFlakyPercentage() { return getProperty(FLAKY_PERCENTAGE); } + @Override + public Long getFlakyCode() { + return getProperty(FLAKY_CODE); + } + @Override public boolean getUsePlaintext() { return getProperty(USE_PLAINTEXT, DEFAULT_USE_PLAINTEXT); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java index db4fb3a6eb..7971d29f89 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java @@ -309,8 +309,6 @@ protected synchronized void doStart() throws Exception { private InfiniteTracing buildInfiniteTracing(ConfigService configService) { com.newrelic.agent.config.InfiniteTracingConfig config = configService.getDefaultAgentConfig().getInfiniteTracingConfig(); - Double flakyPercentage = configService.getDefaultAgentConfig().getInfiniteTracingConfig().getFlakyPercentage(); - boolean usePlaintext = configService.getDefaultAgentConfig().getInfiniteTracingConfig().getUsePlaintext(); InfiniteTracingConfig infiniteTracingConfig = InfiniteTracingConfig.builder() .maxQueueSize(config.getSpanEventsQueueSize()) @@ -318,8 +316,9 @@ private InfiniteTracing buildInfiniteTracing(ConfigService configService) { .host(config.getTraceObserverHost()) .port(config.getTraceObserverPort()) .licenseKey(configService.getDefaultAgentConfig().getLicenseKey()) - .flakyPercentage(flakyPercentage) - .usePlaintext(usePlaintext) + .flakyPercentage(config.getFlakyPercentage()) + .flakyCode(config.getFlakyCode()) + .usePlaintext(config.getUsePlaintext()) .build(); return InfiniteTracing.initialize(infiniteTracingConfig, NewRelic.getAgent().getMetricAggregator()); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/UpdateInfiniteTracingAfterConnect.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/UpdateInfiniteTracingAfterConnect.java index 42c9e4e143..abf9f87acb 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/UpdateInfiniteTracingAfterConnect.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/UpdateInfiniteTracingAfterConnect.java @@ -29,9 +29,7 @@ public void onEstablished(String appName, String agentRunToken, Map headersData = new HashMap<>(); testClass.onEstablished(appName, runToken, headersData); - verify(infiniteTracing).setConnectionMetadata(runToken, headersData); - verify(infiniteTracing).start(); + verify(infiniteTracing).start(runToken, headersData); } @Test From 901b69ada93fc1086c3db5bc4d85048a07912da9 Mon Sep 17 00:00:00 2001 From: jack-berg <34418638+jack-berg@users.noreply.github.com> Date: Mon, 4 Jan 2021 15:50:20 -0600 Subject: [PATCH 2/3] refactor infinite tracing for improved clarity and simplicity, add support for flaky code test configuration --- .../main/java/com/newrelic/BackoffPolicy.java | 8 - .../com/newrelic/ChannelClosingException.java | 2 +- .../java/com/newrelic/ChannelFactory.java | 36 --- .../java/com/newrelic/ChannelManager.java | 205 ++++++++++++++++++ .../java/com/newrelic/ChannelSupplier.java | 59 ----- .../com/newrelic/ChannelToStreamObserver.java | 54 ----- .../java/com/newrelic/ConnectionHeaders.java | 36 --- .../java/com/newrelic/ConnectionStatus.java | 69 ------ .../com/newrelic/DaemonThreadFactory.java | 21 -- .../com/newrelic/DefaultBackoffPolicy.java | 24 -- .../com/newrelic/DisconnectionHandler.java | 40 ---- .../com/newrelic/FlakyHeaderInterceptor.java | 46 ---- .../java/com/newrelic/GrpcSpanConverter.java | 45 ---- .../java/com/newrelic/HeadersInterceptor.java | 45 ++-- .../java/com/newrelic/InfiniteTracing.java | 137 +++++++++--- .../com/newrelic/InfiniteTracingConfig.java | 19 ++ .../main/java/com/newrelic/LoopForever.java | 30 --- .../java/com/newrelic/ResponseObserver.java | 130 ++++++----- .../main/java/com/newrelic/SpanConverter.java | 7 - .../main/java/com/newrelic/SpanDelivery.java | 75 ------- .../java/com/newrelic/SpanEventConsumer.java | 138 ------------ .../java/com/newrelic/SpanEventSender.java | 142 ++++++++++++ .../com/newrelic/StreamObserverFactory.java | 29 --- .../com/newrelic/StreamObserverSupplier.java | 23 -- .../java/com/newrelic/ChannelFactoryTest.java | 66 ------ .../java/com/newrelic/ChannelManagerTest.java | 160 ++++++++++++++ .../com/newrelic/ChannelSupplierTest.java | 79 ------- .../newrelic/ChannelToStreamObserverTest.java | 119 ---------- .../com/newrelic/ConnectionHeadersTest.java | 26 --- .../com/newrelic/ConnectionStatusTest.java | 124 ----------- .../com/newrelic/DaemonThreadFactoryTest.java | 26 --- .../newrelic/DefaultBackoffPolicyTest.java | 29 --- .../newrelic/DisconnectionHandlerTest.java | 60 ----- .../newrelic/FlakyHeaderInterceptorTest.java | 81 ------- .../com/newrelic/GrpcSpanConverterTest.java | 60 ----- .../com/newrelic/HeadersInterceptorTest.java | 43 ++-- .../com/newrelic/InfiniteTracingTest.java | 86 ++++++++ .../newrelic/MockForwardingClientCall.java | 37 ---- .../com/newrelic/ResponseObserverTest.java | 187 ++++++---------- .../java/com/newrelic/SpanDeliveryTest.java | 193 ----------------- .../com/newrelic/SpanEventConsumerTest.java | 87 -------- .../com/newrelic/SpanEventSenderTest.java | 196 +++++++++++++++++ .../newrelic/StreamObserverSupplierTest.java | 41 ---- .../agent/config/InfiniteTracingConfig.java | 2 + .../config/InfiniteTracingConfigImpl.java | 6 + .../agent/service/ServiceManagerImpl.java | 7 +- .../UpdateInfiniteTracingAfterConnect.java | 4 +- ...UpdateInfiniteTracingAfterConnectTest.java | 3 +- 48 files changed, 1131 insertions(+), 2011 deletions(-) delete mode 100644 infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ChannelFactory.java create mode 100644 infinite-tracing/src/main/java/com/newrelic/ChannelManager.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ChannelSupplier.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ChannelToStreamObserver.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ConnectionHeaders.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/ConnectionStatus.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/DaemonThreadFactory.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/DefaultBackoffPolicy.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/DisconnectionHandler.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/FlakyHeaderInterceptor.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/GrpcSpanConverter.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/LoopForever.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/SpanConverter.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/SpanDelivery.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/SpanEventConsumer.java create mode 100644 infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/StreamObserverFactory.java delete mode 100644 infinite-tracing/src/main/java/com/newrelic/StreamObserverSupplier.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ChannelFactoryTest.java create mode 100644 infinite-tracing/src/test/java/com/newrelic/ChannelManagerTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ChannelSupplierTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ChannelToStreamObserverTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ConnectionHeadersTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/ConnectionStatusTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/DaemonThreadFactoryTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/DefaultBackoffPolicyTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/DisconnectionHandlerTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/FlakyHeaderInterceptorTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/GrpcSpanConverterTest.java create mode 100644 infinite-tracing/src/test/java/com/newrelic/InfiniteTracingTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/MockForwardingClientCall.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/SpanDeliveryTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/SpanEventConsumerTest.java create mode 100644 infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java delete mode 100644 infinite-tracing/src/test/java/com/newrelic/StreamObserverSupplierTest.java diff --git a/infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java b/infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java deleted file mode 100644 index 6e8f00bc94..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.newrelic; - -import io.grpc.Status; - -public interface BackoffPolicy { - boolean shouldReconnect(Status status); - void backoff(); -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelClosingException.java b/infinite-tracing/src/main/java/com/newrelic/ChannelClosingException.java index bd18b6190e..55bccc3c9e 100644 --- a/infinite-tracing/src/main/java/com/newrelic/ChannelClosingException.java +++ b/infinite-tracing/src/main/java/com/newrelic/ChannelClosingException.java @@ -4,5 +4,5 @@ * This is a signaling exception to the call that the channel is closing. By using this exception, we can pass the required * information to the {@link io.grpc.stub.StreamObserver#onError} call so it knows not to consider this an actual error. */ -public class ChannelClosingException extends Exception { +class ChannelClosingException extends Exception { } diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelFactory.java b/infinite-tracing/src/main/java/com/newrelic/ChannelFactory.java deleted file mode 100644 index 3d1c59427a..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ChannelFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.newrelic; - -import com.google.common.annotations.VisibleForTesting; -import io.grpc.ClientInterceptor; -import io.grpc.ManagedChannel; -import io.grpc.okhttp.OkHttpChannelBuilder; - -public class ChannelFactory { - private final InfiniteTracingConfig config; - private final ClientInterceptor[] interceptors; - - public ChannelFactory(InfiniteTracingConfig config, ClientInterceptor... interceptors) { - this.config = config; - this.interceptors = interceptors; - } - - public ManagedChannel createChannel() { - OkHttpChannelBuilder okHttpChannelBuilder = newOkHttpChannelBuilder() - .defaultLoadBalancingPolicy("pick_first") - .intercept(interceptors); - - if (config.getUsePlaintext()) { - okHttpChannelBuilder.usePlaintext(); - } else { - okHttpChannelBuilder.useTransportSecurity(); - } - return okHttpChannelBuilder.build(); - } - - @VisibleForTesting - OkHttpChannelBuilder newOkHttpChannelBuilder() { - return OkHttpChannelBuilder - .forAddress(config.getHost(), config.getPort()); - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java b/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java new file mode 100644 index 0000000000..868de0e36f --- /dev/null +++ b/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java @@ -0,0 +1,205 @@ +package com.newrelic; + +import com.google.common.annotations.VisibleForTesting; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import com.newrelic.trace.v1.IngestServiceGrpc; +import com.newrelic.trace.v1.IngestServiceGrpc.IngestServiceStub; +import com.newrelic.trace.v1.V1; +import io.grpc.ManagedChannel; +import io.grpc.okhttp.OkHttpChannelBuilder; +import io.grpc.stub.ClientCallStreamObserver; + +import javax.annotation.concurrent.GuardedBy; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +class ChannelManager { + + private final Logger logger; + private final InfiniteTracingConfig config; + private final MetricAggregator aggregator; + + private final Object lock = new Object(); + @GuardedBy("lock") private boolean isShutdownForever; + @GuardedBy("lock") private CountDownLatch backoffLatch; + @GuardedBy("lock") private ManagedChannel managedChannel; + @GuardedBy("lock") private ClientCallStreamObserver spanObserver; + @GuardedBy("lock") private ResponseObserver responseObserver; + @GuardedBy("lock") private String agentRunToken; + @GuardedBy("lock") private Map requestMetadata; + + ChannelManager(InfiniteTracingConfig config, MetricAggregator aggregator, String agentRunToken, Map requestMetadata) { + this.logger = config.getLogger(); + this.config = config; + this.aggregator = aggregator; + this.agentRunToken = agentRunToken; + this.requestMetadata = requestMetadata; + } + + /** + * Update metadata included on gRPC requests. + * + * @param agentRunToken the agent run token + * @param requestMetadata any extra metadata headers that must be included + */ + void updateMetadata(String agentRunToken, Map requestMetadata) { + synchronized (lock) { + this.agentRunToken = agentRunToken; + this.requestMetadata = requestMetadata; + } + } + + /** + * Obtain a span observer. Creates a channel if one is not open. Creates a span observer if one + * does not exist. If the channel has been shutdown and is backing off via + * {@link #shutdownChannelAndBackoff(int)}, awaits the backoff period before recreating the channel. + * + * @return a span observer + */ + ClientCallStreamObserver getSpanObserver() { + // Obtain the lock, and await the backoff if in progress + CountDownLatch latch; + synchronized (lock) { + latch = backoffLatch; + } + if (latch != null) { + try { + logger.log(Level.FINE, "Awaiting backoff."); + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted while awaiting backoff."); + } + } + + // Obtain the lock, and possibly recreate the channel or the span observer + synchronized (lock) { + if (isShutdownForever) { + throw new RuntimeException("No longer accepting connections to gRPC."); + } + if (managedChannel == null) { + logger.log(Level.FINE, "Creating gRPC channel."); + managedChannel = buildChannel(); + } + if (spanObserver == null) { + logger.log(Level.FINE, "Creating gRPC span observer."); + IngestServiceStub ingestServiceStub = buildStub(managedChannel); + responseObserver = buildResponseObserver(); + spanObserver = (ClientCallStreamObserver) ingestServiceStub.recordSpan(responseObserver); + aggregator.incrementCounter("Supportability/InfiniteTracing/Connect"); + } + return spanObserver; + } + } + + @VisibleForTesting + IngestServiceStub buildStub(ManagedChannel managedChannel) { + return IngestServiceGrpc.newStub(managedChannel); + } + + @VisibleForTesting + ResponseObserver buildResponseObserver() { + return new ResponseObserver(logger, this, aggregator); + } + + /** + * Cancel the span observer. The next time {@link #getSpanObserver()} is called the span observer + * will be recreated. This cancels the span observer with a {@link ChannelClosingException}, which + * {@link ResponseObserver#onError(Throwable)} detects and ignores. + */ + void cancelSpanObserver() { + synchronized (lock) { + if (spanObserver == null) { + return; + } + logger.log(Level.FINE, "Canceling gRPC span observer."); + spanObserver.cancel("CLOSING_CONNECTION", new ChannelClosingException()); + spanObserver = null; + responseObserver = null; + } + } + + /** + * Shutdown the channel, cancel the span observer, and backoff. The next time {@link #getSpanObserver()} + * is called, it will await the backoff and the channel will be recreated. + * + * @param backoffSeconds the number of seconds to await before the channel can be recreated + */ + void shutdownChannelAndBackoff(int backoffSeconds) { + logger.log(Level.FINE, "Shutting down gRPC channel and backing off for {0} seconds.", backoffSeconds); + CountDownLatch latch; + synchronized (lock) { + if (backoffLatch != null) { + logger.log(Level.FINE, "Backoff already in progress."); + return; + } + backoffLatch = new CountDownLatch(1); + latch = backoffLatch; + + if (managedChannel != null) { + managedChannel.shutdown(); + managedChannel = null; + } + cancelSpanObserver(); + } + + try { + TimeUnit.SECONDS.sleep(backoffSeconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted while backing off."); + } + + synchronized (lock) { + latch.countDown(); + backoffLatch = null; + } + logger.log(Level.FINE, "Backoff complete."); + } + + /** + * Shutdown the channel and do not recreate it. The next time {@link #getSpanObserver()} is called + * an exception will be thrown. + */ + void shutdownChannelForever() { + synchronized (lock) { + logger.log(Level.FINE, "Shutting down gRPC channel forever."); + shutdownChannelAndBackoff(0); + this.isShutdownForever = true; + } + } + + @VisibleForTesting + ManagedChannel buildChannel() { + Map headers; + synchronized (lock) { + headers = requestMetadata != null ? new HashMap<>(requestMetadata) : new HashMap(); + headers.put("agent_run_token", agentRunToken); + } + + headers.put("license_key", config.getLicenseKey()); + if (config.getFlakyPercentage() != null) { + logger.log(Level.WARNING, "Infinite tracing is configured with a flaky percentage! There will be errors!"); + headers.put("flaky", config.getFlakyPercentage().toString()); + if (config.getFlakyCode() != null) { + headers.put("flaky_code", config.getFlakyCode().toString()); + } + } + + OkHttpChannelBuilder channelBuilder = OkHttpChannelBuilder + .forAddress(config.getHost(), config.getPort()) + .defaultLoadBalancingPolicy("pick_first") + .intercept(new HeadersInterceptor(headers)); + if (config.getUsePlaintext()) { + channelBuilder.usePlaintext(); + } else { + channelBuilder.useTransportSecurity(); + } + return channelBuilder.build(); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelSupplier.java b/infinite-tracing/src/main/java/com/newrelic/ChannelSupplier.java deleted file mode 100644 index 533c144340..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ChannelSupplier.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.api.agent.Logger; -import io.grpc.ManagedChannel; - -import java.util.logging.Level; - -/** - * Provides a {@link ManagedChannel}, recreating it only if necessary. - * - * Not thread-safe. - */ -public class ChannelSupplier implements Supplier { - private final ConnectionStatus connectionStatus; - private final Logger logger; - private final ChannelFactory channelFactory; - private volatile ManagedChannel channel; - - public ChannelSupplier(ChannelFactory channelFactory, ConnectionStatus connectionStatus, Logger logger) { - this.connectionStatus = connectionStatus; - this.logger = logger; - this.channelFactory = channelFactory; - channel = null; - } - - @Override - public ManagedChannel get() { - ConnectionStatus.BlockResult blockResult = getBlockResult(); - - if (blockResult == ConnectionStatus.BlockResult.GO_AWAY_FOREVER) { - throw new RuntimeException("No longer attempting to connect."); - } - - if (blockResult == ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION || channel == null) { - logger.log(Level.FINE, "Attempting to connect to the Trace Observer."); - - if (channel != null) { - ManagedChannel oldChannel = channel; - channel = null; - oldChannel.shutdown(); - } - channel = channelFactory.createChannel(); - connectionStatus.didConnect(); - } - - return channel; - } - - public ConnectionStatus.BlockResult getBlockResult() { - try { - return connectionStatus.blockOnConnection(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Thread interrupted while attempting to connect."); - } - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelToStreamObserver.java b/infinite-tracing/src/main/java/com/newrelic/ChannelToStreamObserver.java deleted file mode 100644 index 7ca0811b34..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ChannelToStreamObserver.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.newrelic; - -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Accepts a {@link ManagedChannel} and provides a valid {@link ClientCallStreamObserver}, creating it only when required. - * - * Not thread-safe. - */ -public class ChannelToStreamObserver implements Function> { - private final StreamObserverFactory streamObserverFactory; - private final AtomicBoolean shouldRecreateCall; - private volatile ManagedChannel lastChannel; - private volatile ClientCallStreamObserver streamObserver; - - public ChannelToStreamObserver( - StreamObserverFactory streamObserverFactory, - AtomicBoolean shouldRecreateCall) { - this.streamObserverFactory = streamObserverFactory; - this.shouldRecreateCall = shouldRecreateCall; - } - - @Override - public ClientCallStreamObserver apply(ManagedChannel channel) { - if (channel == null) { - return null; - } - - if (lastChannel != channel || streamObserver == null || shouldRecreateCall.get()) { - recreateStreamObserver(channel); - } - - return streamObserver; - } - - private void recreateStreamObserver(ManagedChannel channel) { - lastChannel = channel; - clearStreamObserver(); - streamObserver = streamObserverFactory.buildStreamObserver(channel); - shouldRecreateCall.set(false); - } - - private void clearStreamObserver() { - if (this.streamObserver != null) { - ClientCallStreamObserver oldStreamObserver = this.streamObserver; - this.streamObserver = null; - oldStreamObserver.cancel("CLOSING_CONNECTION", new ChannelClosingException()); - } - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ConnectionHeaders.java b/infinite-tracing/src/main/java/com/newrelic/ConnectionHeaders.java deleted file mode 100644 index 9b06827537..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ConnectionHeaders.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.api.agent.Logger; - -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; - -public class ConnectionHeaders implements Supplier> { - private final ConnectionStatus connectionStatus; - private final Logger logger; - private final String licenseKey; - private volatile Map headers; - - public ConnectionHeaders(ConnectionStatus connectionStatus, Logger logger, String licenseKey) { - this.connectionStatus = connectionStatus; - this.logger = logger; - this.licenseKey = licenseKey; - } - - public void set(String newRunToken, Map headers) { - Map newHeaders = new HashMap<>(headers); - newHeaders.put("agent_run_token", newRunToken); - newHeaders.put("license_key", licenseKey); - this.headers = newHeaders; - - logger.log(Level.INFO, "New Relic connection successful. Attempting connection to the Trace Observer."); - connectionStatus.reattemptConnection(); - } - - @Override - public Map get() { - return headers; - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ConnectionStatus.java b/infinite-tracing/src/main/java/com/newrelic/ConnectionStatus.java deleted file mode 100644 index 8c39a53baf..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/ConnectionStatus.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.Status; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; - -public class ConnectionStatus { - public ConnectionStatus(Logger logger) { - this.logger = logger; - } - - public enum BlockResult {ALREADY_CONNECTED, MUST_ATTEMPT_CONNECTION, GO_AWAY_FOREVER} - - private enum ConnectionState {CONNECT_NEEDED, CONNECTING, CONNECTED, BACKOFF_PAUSE, STOPPED_FOREVER} - - private final Logger logger; - private final AtomicReference currentState = new AtomicReference<>(ConnectionState.CONNECT_NEEDED); - - /** - * Blocks until a connection is either made, or this thread is responsible for making the connection. - * - * @return {@link BlockResult#MUST_ATTEMPT_CONNECTION} if this thread is responsible for connecting; {@link BlockResult#ALREADY_CONNECTED} if already connected. - */ - public BlockResult blockOnConnection() throws InterruptedException { - while (true) { - ConnectionState current = currentState.get(); - if (current == ConnectionState.CONNECTED) { - return BlockResult.ALREADY_CONNECTED; - } else if (current == ConnectionState.STOPPED_FOREVER) { - logger.log(Level.FINE, "No longer attempting to reconnect to gRPC"); - return BlockResult.GO_AWAY_FOREVER; - } else if (currentState.compareAndSet(ConnectionState.CONNECT_NEEDED, ConnectionState.CONNECTING)) { - return BlockResult.MUST_ATTEMPT_CONNECTION; - } - - Thread.sleep(1000); - } - } - - /** - * Threads that successfully connect should call this when complete. It will release other threads busy-waiting for a connection. - */ - public void didConnect() { - currentState.set(ConnectionState.CONNECTED); - } - - /** - * Tells all threads that all connections should be stopped and not resumed. - */ - public void shutDownForever() { - currentState.set(ConnectionState.STOPPED_FOREVER); - } - - /** - * Indicates whether or not this thread should follow the disconnect/backoff routine - */ - public boolean shouldReconnect() { - return currentState.compareAndSet(ConnectionState.CONNECTED, ConnectionState.BACKOFF_PAUSE); - } - - /** - * Tells all threads that the next thread should attempt to reconnect. - */ - public void reattemptConnection() { - currentState.set(ConnectionState.CONNECT_NEEDED); - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/DaemonThreadFactory.java b/infinite-tracing/src/main/java/com/newrelic/DaemonThreadFactory.java deleted file mode 100644 index a0419f6391..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/DaemonThreadFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.newrelic; - -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; - -public class DaemonThreadFactory implements ThreadFactory { - private final String serviceName; - private final AtomicInteger counter = new AtomicInteger(0); - - public DaemonThreadFactory(String serviceName) { - this.serviceName = serviceName; - } - - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable); - thread.setName("New Relic " + serviceName + " #" + counter.incrementAndGet()); - thread.setDaemon(true); - return thread; - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/DefaultBackoffPolicy.java b/infinite-tracing/src/main/java/com/newrelic/DefaultBackoffPolicy.java deleted file mode 100644 index e5896e03de..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/DefaultBackoffPolicy.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.newrelic; - -import io.grpc.Status; - -import java.util.concurrent.TimeUnit; - -public class DefaultBackoffPolicy implements BackoffPolicy { - - @Override - public boolean shouldReconnect(Status status) { - // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#unimplemented - return status != Status.UNIMPLEMENTED; - } - - @Override - public void backoff() { - try { - // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#other-errors-1 - TimeUnit.SECONDS.sleep(15); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/DisconnectionHandler.java b/infinite-tracing/src/main/java/com/newrelic/DisconnectionHandler.java deleted file mode 100644 index 2a2d90e771..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/DisconnectionHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.Status; - -import java.util.logging.Level; - -public class DisconnectionHandler { - private final ConnectionStatus connectionStatus; - private final BackoffPolicy backoffPolicy; - private final Logger logger; - - public DisconnectionHandler(ConnectionStatus connectionStatus, BackoffPolicy backoffPolicy, Logger logger) { - this.connectionStatus = connectionStatus; - this.backoffPolicy = backoffPolicy; - this.logger = logger; - } - - public void terminate() { - connectionStatus.shutDownForever(); - } - - public void handle(Status responseStatus) { - if (!connectionStatus.shouldReconnect()) { - return; - } - - if (!backoffPolicy.shouldReconnect(responseStatus)) { - if (responseStatus != null) { - logger.log(Level.WARNING, "Got gRPC status " + responseStatus.getCode().toString() + ", no longer permitting connections."); - } - terminate(); - } - - logger.log(Level.FINE, "Backing off due to gRPC errors."); - backoffPolicy.backoff(); - logger.log(Level.FINE, "Backoff complete, attempting connection."); - connectionStatus.reattemptConnection(); - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/FlakyHeaderInterceptor.java b/infinite-tracing/src/main/java/com/newrelic/FlakyHeaderInterceptor.java deleted file mode 100644 index de57ff55c3..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/FlakyHeaderInterceptor.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.newrelic; - -import io.grpc.*; -import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; -import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; -import io.grpc.Metadata.Key; - -import java.util.logging.Level; - -/** - * Injects the "flaky" header on each sent span, only if configured to - * do so. - */ -public class FlakyHeaderInterceptor implements ClientInterceptor { - - private static final Key FLAKY_HEADER = Key.of("flaky", Metadata.ASCII_STRING_MARSHALLER); - private final InfiniteTracingConfig config; - - public FlakyHeaderInterceptor(InfiniteTracingConfig config) { - this.config = config; - if (config.getFlakyPercentage() != null) { - config.getLogger().log(Level.WARNING, "Infinite tracing is configured with a flaky percentage! There will be errors!"); - } - } - - @Override - public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { - final Double flakyPercentage = config.getFlakyPercentage(); - if (flakyPercentage == null) { - return next.newCall(method, callOptions); - } - return new SimpleForwardingClientCall(next.newCall(method, callOptions)) { - @Override - public void start(Listener responseListener, Metadata headers) { - headers.put(FLAKY_HEADER, flakyPercentage.toString()); - super.start(new SimpleForwardingClientCallListener(responseListener) { - @Override - public void onHeaders(Metadata headers) { - super.onHeaders(headers); - } - }, headers); - } - }; - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/GrpcSpanConverter.java b/infinite-tracing/src/main/java/com/newrelic/GrpcSpanConverter.java deleted file mode 100644 index bbd16f4819..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/GrpcSpanConverter.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.trace.v1.V1; - -import java.util.HashMap; -import java.util.Map; - -public class GrpcSpanConverter implements SpanConverter { - public V1.Span convert(SpanEvent spanEvent) { - Map intrinsicAttributes = copyAttributes(spanEvent.getIntrinsics()); - Map userAttributes = copyAttributes(spanEvent.getUserAttributesCopy()); - Map agentAttributes = copyAttributes(spanEvent.getAgentAttributes()); - - intrinsicAttributes.put("appName", V1.AttributeValue.newBuilder().setStringValue(spanEvent.getAppName()).build()); - - return V1.Span.newBuilder() - .setTraceId(spanEvent.getTraceId()) - .putAllIntrinsics(intrinsicAttributes) - .putAllAgentAttributes(agentAttributes) - .putAllUserAttributes(userAttributes) - .build(); - } - - private Map copyAttributes(Map original) { - Map copy = new HashMap<>(); - if (original == null) { - return copy; - } - - for (Map.Entry entry : original.entrySet()) { - Object value = entry.getValue(); - if (value instanceof String) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setStringValue((String) value).build()); - } else if (value instanceof Long || value instanceof Integer) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setIntValue(((Number) value).longValue()).build()); - } else if (value instanceof Float || value instanceof Double) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setDoubleValue(((Number) value).doubleValue()).build()); - } else if (value instanceof Boolean) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setBoolValue((Boolean) value).build()); - } - } - return copy; - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/HeadersInterceptor.java b/infinite-tracing/src/main/java/com/newrelic/HeadersInterceptor.java index a002653dfc..080812cd0e 100644 --- a/infinite-tracing/src/main/java/com/newrelic/HeadersInterceptor.java +++ b/infinite-tracing/src/main/java/com/newrelic/HeadersInterceptor.java @@ -1,6 +1,5 @@ package com.newrelic; -import com.newrelic.agent.interfaces.backport.Supplier; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -12,30 +11,38 @@ import java.util.Map; -// this class is for adding headers to the outbound span event stream -public class HeadersInterceptor implements ClientInterceptor { +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; - private final Supplier> headersSupplier; +class HeadersInterceptor implements ClientInterceptor { - public HeadersInterceptor(Supplier> headersSupplier) { - this.headersSupplier = headersSupplier; + private final Map headers; + + HeadersInterceptor(Map headers) { + this.headers = headers; } + @Override public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { - return new ForwardingClientCall.SimpleForwardingClientCall(next.newCall(method, callOptions)) { + return new HeadersClientCall<>(method, callOptions, next); + } - @Override - public void start(Listener responseListener, Metadata headers) { - for (Map.Entry header: headersSupplier.get().entrySet()) { - headers.put(Metadata.Key.of(header.getKey().toLowerCase(), Metadata.ASCII_STRING_MARSHALLER), header.getValue()); - } - super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { - @Override - public void onHeaders(Metadata headers) { - super.onHeaders(headers); - } - }, headers); + private class HeadersClientCall extends ForwardingClientCall.SimpleForwardingClientCall { + + private HeadersClientCall(MethodDescriptor method, CallOptions callOptions, Channel next) { + super(next.newCall(method, callOptions)); + } + + @Override + public void start(Listener responseListener, Metadata metadata) { + for (Map.Entry header : headers.entrySet()) { + metadata.put(Metadata.Key.of(header.getKey().toLowerCase(), ASCII_STRING_MARSHALLER), header.getValue()); } - }; + super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { + @Override + public void onHeaders(Metadata headers) { + super.onHeaders(headers); + } + }, metadata); + } } } \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/InfiniteTracing.java b/infinite-tracing/src/main/java/com/newrelic/InfiniteTracing.java index 0367b5da0e..9cff8261cf 100644 --- a/infinite-tracing/src/main/java/com/newrelic/InfiniteTracing.java +++ b/infinite-tracing/src/main/java/com/newrelic/InfiniteTracing.java @@ -1,60 +1,135 @@ package com.newrelic; +import com.google.common.annotations.VisibleForTesting; import com.newrelic.agent.interfaces.backport.Consumer; import com.newrelic.agent.model.SpanEvent; +import com.newrelic.api.agent.Logger; import com.newrelic.api.agent.MetricAggregator; -import java.util.Collections; +import javax.annotation.concurrent.GuardedBy; import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; public class InfiniteTracing implements Consumer { - private final SpanEventConsumer spanEventConsumer; + + private final Logger logger; + private final InfiniteTracingConfig config; + private final MetricAggregator aggregator; + private final ExecutorService executorService; + private final BlockingQueue queue; + + private final Object lock = new Object(); + @GuardedBy("lock") private Future spanEventSenderFuture; + @GuardedBy("lock") private SpanEventSender spanEventSender; + @GuardedBy("lock") private ChannelManager channelManager; + + @VisibleForTesting + InfiniteTracing(InfiniteTracingConfig config, MetricAggregator aggregator, ExecutorService executorService, BlockingQueue queue) { + this.logger = config.getLogger(); + this.config = config; + this.aggregator = aggregator; + this.executorService = executorService; + this.queue = queue; + } /** - * Set up the Infinite Tracing library. - * @param config Required data to start a connection to Infinite Tracing. - * @param metricAggregator Aggregator to which information about the library will be recorded. - * @return An object that requires two final pieces: {@link #setConnectionMetadata} and {@link #start} + * Start sending spans to the Infinite Tracing Observer. If already running, update with the + * {@code agentRunToken} and {@code requestMetadata}. + * + * @param agentRunToken the agent run token + * @param requestMetadata any extra metadata headers that must be included */ - @SuppressWarnings("unused") // this is the public API of the class - public static InfiniteTracing initialize(InfiniteTracingConfig config, MetricAggregator metricAggregator) { - SpanEventConsumer spanEventConsumer = SpanEventConsumer.builder(config, metricAggregator).build(); - return new InfiniteTracing(spanEventConsumer); + public void start(String agentRunToken, Map requestMetadata) { + synchronized (lock) { + if (spanEventSenderFuture != null) { + channelManager.updateMetadata(agentRunToken, requestMetadata); + channelManager.shutdownChannelAndBackoff(0); + return; + } + logger.log(Level.INFO, "Starting Infinite Tracing."); + channelManager = buildChannelManager(agentRunToken, requestMetadata); + spanEventSender = buildSpanEventSender(); + spanEventSenderFuture = executorService.submit(spanEventSender); + } } - private InfiniteTracing(SpanEventConsumer spanEventConsumer) { - this.spanEventConsumer = spanEventConsumer; + @VisibleForTesting + ChannelManager buildChannelManager(String agentRunToken, Map requestMetadata) { + return new ChannelManager(config, aggregator, agentRunToken, requestMetadata); + } + + @VisibleForTesting + SpanEventSender buildSpanEventSender() { + return new SpanEventSender(config, queue, aggregator, channelManager); } /** - * Call this method when the connection metadata changes, which is driven by the collector. - * - * @param newRunToken The new agent run token that should be supplied to the Trace Observer. - * @param headers The metadata that should be supplied to the Trace Observer as headers. + * Stop sending spans to the Infinite Tracing Observer and cleanup resources. If not already running, + * return immediately. */ - public void setConnectionMetadata(String newRunToken, Map headers) { - spanEventConsumer.setConnectionMetadata(newRunToken, headers); + public void stop() { + synchronized (lock) { + if (spanEventSenderFuture == null) { + return; + } + logger.log(Level.INFO, "Stopping Infinite Tracing."); + spanEventSenderFuture.cancel(true); + channelManager.shutdownChannelForever(); + spanEventSenderFuture = null; + spanEventSender = null; + channelManager = null; + } } /** - * Initiates the connection and acceptance of {@link SpanEvent} instances. + * Offer the span event to the queue to be written to the Infinite Trace Observer. If the queue + * is at capacity, the span event is ignored. + * + * @param spanEvent the span event */ - @SuppressWarnings("unused") // this is the public API of the class - public void start() { - spanEventConsumer.start(); + @Override + public void accept(SpanEvent spanEvent) { + aggregator.incrementCounter("Supportability/InfiniteTracing/Span/Seen"); + if (!queue.offer(spanEvent)) { + logger.log(Level.FINEST, "Span event not accepted. The queue was full."); + } } /** - * Call this method whenever the run token changes. - * @deprecated use {@link #setConnectionMetadata} instead + * Initialize Infinite Tracing. Note, for spans to start being sent {@link #start(String, Map)} must + * be called. + * + * @param config the config + * @param aggregator the metric aggregator + * @return the instance */ - @Deprecated - public void setRunToken(String newRunToken) { - setConnectionMetadata(newRunToken, Collections.emptyMap()); + public static InfiniteTracing initialize(InfiniteTracingConfig config, MetricAggregator aggregator) { + ExecutorService executorService = Executors.newSingleThreadExecutor(new DaemonThreadFactory("Infinite Tracing")); + return new InfiniteTracing(config, aggregator, executorService, new LinkedBlockingDeque(config.getMaxQueueSize())); } - @Override - public void accept(SpanEvent spanEvent) { - spanEventConsumer.accept(spanEvent); + private static class DaemonThreadFactory implements ThreadFactory { + private final String serviceName; + private final AtomicInteger counter = new AtomicInteger(0); + + private DaemonThreadFactory(String serviceName) { + this.serviceName = serviceName; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setName("New Relic " + serviceName + " #" + counter.incrementAndGet()); + thread.setDaemon(true); + return thread; + } } -} + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/InfiniteTracingConfig.java b/infinite-tracing/src/main/java/com/newrelic/InfiniteTracingConfig.java index 0551955aa1..4610257b2a 100644 --- a/infinite-tracing/src/main/java/com/newrelic/InfiniteTracingConfig.java +++ b/infinite-tracing/src/main/java/com/newrelic/InfiniteTracingConfig.java @@ -11,6 +11,7 @@ public class InfiniteTracingConfig { private final int port; private final Logger logger; private final Double flakyPercentage; + private final Long flakyCode; private final boolean usePlaintext; public InfiniteTracingConfig(Builder builder) { @@ -20,6 +21,7 @@ public InfiniteTracingConfig(Builder builder) { this.port = builder.port; this.logger = builder.logger; this.flakyPercentage = builder.flakyPercentage; + this.flakyCode = builder.flakyCode; this.usePlaintext = builder.usePlaintext; } @@ -51,6 +53,10 @@ public Double getFlakyPercentage() { return flakyPercentage; } + public Long getFlakyCode() { + return flakyCode; + } + public boolean getUsePlaintext() { return usePlaintext; } @@ -62,6 +68,7 @@ public static class Builder { private String host; private int port; private Double flakyPercentage; + private Long flakyCode; private boolean usePlaintext; /** @@ -115,6 +122,18 @@ public Builder flakyPercentage(Double flakyPercentage) { return this; } + /** + * The optional gRPC error status to trigger when {@link #flakyPercentage(Double)} is + * specified. + * + * @param flakyCode The gRPC error status code + * @see gRPC status codes + */ + public Builder flakyCode(Long flakyCode) { + this.flakyCode = flakyCode; + return this; + } + /** * The optional boolean connect using plaintext * diff --git a/infinite-tracing/src/main/java/com/newrelic/LoopForever.java b/infinite-tracing/src/main/java/com/newrelic/LoopForever.java deleted file mode 100644 index 511283f90a..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/LoopForever.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; - -import java.util.logging.Level; - -public class LoopForever implements Runnable { - private final Logger logger; - private final Runnable spanDeliveryConsumer; - - public LoopForever( - Logger logger, - Runnable spanDeliveryConsumer) { - this.logger = logger; - this.spanDeliveryConsumer = spanDeliveryConsumer; - } - - @Override - public void run() { - logger.log(Level.FINE, "Initializing {0}", this); - while (true) { - try { - spanDeliveryConsumer.run(); - } catch (Throwable t) { - logger.log(Level.SEVERE, t, "There was a problem with the Infinite Tracing span sender, and no further spans will be sent"); - return; - } - } - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java b/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java index ca72f74971..668c16d085 100644 --- a/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java +++ b/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java @@ -1,98 +1,122 @@ package com.newrelic; +import com.google.common.annotations.VisibleForTesting; import com.newrelic.api.agent.Logger; import com.newrelic.api.agent.MetricAggregator; import com.newrelic.trace.v1.V1; import io.grpc.Status; -import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; -public class ResponseObserver implements StreamObserver { - private final MetricAggregator metricAggregator; +class ResponseObserver implements StreamObserver { + + static final int DEFAULT_BACKOFF_SECONDS = 15; + static final int[] BACKOFF_SECONDS_SEQUENCE = new int[] { 15, 15, 30, 60, 120, 300 }; + private final Logger logger; - private final DisconnectionHandler disconnectionHandler; - private final AtomicBoolean shouldRecreateCall; + private final ChannelManager channelManager; + private final MetricAggregator aggregator; + private final AtomicInteger backoffSequenceIndex = new AtomicInteger(-1); - public ResponseObserver(MetricAggregator metricAggregator, Logger logger, DisconnectionHandler disconnectionHandler, - AtomicBoolean shouldRecreateCall) { - this.metricAggregator = metricAggregator; + ResponseObserver(Logger logger, ChannelManager channelManager, MetricAggregator aggregator) { this.logger = logger; - this.disconnectionHandler = disconnectionHandler; - this.shouldRecreateCall = shouldRecreateCall; + this.channelManager = channelManager; + this.aggregator = aggregator; } @Override public void onNext(V1.RecordStatus value) { - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Response"); + aggregator.incrementCounter("Supportability/InfiniteTracing/Response"); } @Override public void onError(Throwable t) { - if (isChannelClosing(t)) { - logger.log(Level.FINE, "Stopping current gRPC call because the channel is closing."); - return; - } + Status status = Status.fromThrowable(t); - if (isAlpnError(t)) { - logger.log(Level.SEVERE, t, "ALPN does not appear to be supported on this JVM. Please install a supported JCE provider or update Java to use Infinite Tracing"); - metricAggregator.incrementCounter("Supportability/InfiniteTracing/NoALPNSupport"); - disconnectionHandler.terminate(); + // If the span observer is knowingly cancelled, it is canceled with a ChannelClosingException, + // which is detected here and ignored. + if (status.getCause() instanceof ChannelClosingException) { + logger.log(Level.FINE, "Stopping gRPC response observer because span observer was closed by another thread."); return; } - if (!isConnectionTimeoutException(t)) { - logger.log(Level.WARNING, t, "Encountered gRPC exception"); + if (isOkHttpALPNError(status)) { + logger.log(Level.SEVERE, t, + "ALPN does not appear to be supported on this JVM. Please install a supported JCE provider or update Java to use Infinite Tracing."); + aggregator.incrementCounter("Supportability/InfiniteTracing/NoALPNSupport"); + channelManager.shutdownChannelForever(); + return; } - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Response/Error"); + aggregator.incrementCounter("Supportability/InfiniteTracing/Span/gRPC/" + status.getCode()); + aggregator.incrementCounter("Supportability/InfiniteTracing/Response/Error"); - Status status = null; - if (t instanceof StatusRuntimeException) { - StatusRuntimeException statusRuntimeException = (StatusRuntimeException) t; - status = statusRuntimeException.getStatus(); - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Span/gRPC/" + status.getCode()); + if (status.getCode() == Status.Code.UNIMPLEMENTED) { + // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#unimplemented + logger.log(Level.WARNING, "Received gRPC status {0}, no longer permitting connections.", status.getCode().toString()); + channelManager.shutdownChannelForever(); + return; } - disconnectionHandler.handle(status); - } - - @Override - public void onCompleted() { - logger.log(Level.FINE, "Completing gRPC call."); - shouldRecreateCall.set(true); - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Response/Completed"); + shutdownChannelAndBackoff(status); } /** - * Detects if the error received was another thread knowingly cancelling the gRPC call. + * Determine if the error status indicates that ALPN support is not provided by this JVM. */ - private boolean isChannelClosing(Throwable t) { - return t instanceof StatusRuntimeException && t.getCause() instanceof ChannelClosingException; + private static boolean isOkHttpALPNError(Status status) { + // See https://github.com/grpc/grpc-java/blob/v1.28.1/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java#L96 + // It's the only mechanism we have to identify the problem. + return status.getCause() instanceof RuntimeException + && status.getCause().getMessage().startsWith("TLS ALPN negotiation failed with protocols"); } /** - * Detects if the error received was a connection timeout exception. This can happen if the agent hasn't sent any spans for more than 15 seconds. + * Shutdown and backoff the gRPC channel. The amount of time to backoff before allowing reconnection + * is dictated by the {@code status}. + * + * @param status the error status */ - private boolean isConnectionTimeoutException(Throwable t) { - return t instanceof StatusRuntimeException - && t.getMessage().startsWith("INTERNAL: No error: A GRPC status of OK should have been sent"); + @VisibleForTesting + void shutdownChannelAndBackoff(Status status) { + int backoffSeconds; + Level logLevel = Level.WARNING; + + if (isConnectTimeoutError(status)) { + backoffSeconds = 0; + logLevel = Level.FINE; + } else if (status.getCode() == Status.Code.FAILED_PRECONDITION) { + // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#failed_precondition + int nextIndex = backoffSequenceIndex.incrementAndGet(); + backoffSeconds = nextIndex < BACKOFF_SECONDS_SEQUENCE.length + ? BACKOFF_SECONDS_SEQUENCE[nextIndex] + : BACKOFF_SECONDS_SEQUENCE[BACKOFF_SECONDS_SEQUENCE.length - 1]; + } else { + // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#other-errors-1 + backoffSeconds = DEFAULT_BACKOFF_SECONDS; + } + + logger.log(logLevel, status.asException(), "Received gRPC status {0}.", status.getCode().toString()); + channelManager.shutdownChannelAndBackoff(backoffSeconds); } /** - * Attempts to detect if the error received indicates that ALPN support is not provided by this JVM. + * Determine if the error status is a connection timeout exception. This can happen if the agent hasn't sent + * any spans for more than 15 seconds. */ - private boolean isAlpnError(Throwable t) { - return t instanceof StatusRuntimeException - && t.getCause() instanceof RuntimeException - && isOkHttpALPNException((RuntimeException) t.getCause()); + private static boolean isConnectTimeoutError(Status status) { + return status.getCode() == Status.Code.INTERNAL + && status.getDescription() != null + && status.getDescription().startsWith("No error: A GRPC status of OK should have been sent"); } - private boolean isOkHttpALPNException(RuntimeException cause) { - // See https://github.com/grpc/grpc-java/blob/v1.28.1/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java#L96 - // It's the only mechanism we have to identify the problem. - return cause.getMessage() != null && cause.getMessage().startsWith("TLS ALPN negotiation failed with protocols"); + @Override + public void onCompleted() { + logger.log(Level.FINE, "Completing gRPC response observer."); + aggregator.incrementCounter("Supportability/InfiniteTracing/Response/Completed"); + channelManager.cancelSpanObserver(); } -} + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanConverter.java b/infinite-tracing/src/main/java/com/newrelic/SpanConverter.java deleted file mode 100644 index c1ec13a7cd..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/SpanConverter.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.model.SpanEvent; - -public interface SpanConverter { - T convert(SpanEvent event); -} diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanDelivery.java b/infinite-tracing/src/main/java/com/newrelic/SpanDelivery.java deleted file mode 100644 index ec9699497f..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/SpanDelivery.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.api.agent.Logger; -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.V1; -import io.grpc.stub.ClientCallStreamObserver; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; - -class SpanDelivery implements Runnable { - - private final SpanConverter spanConverter; - private final MetricAggregator metricAggregator; - private final Logger logger; - private final BlockingQueue queue; - private final Supplier> streamObserverSupplier; - - public SpanDelivery(SpanConverter spanConverter, MetricAggregator metricAggregator, Logger logger, BlockingQueue queue, - Supplier> streamObserverSupplier) { - this.spanConverter = spanConverter; - this.metricAggregator = metricAggregator; - this.logger = logger; - this.queue = queue; - this.streamObserverSupplier = streamObserverSupplier; - } - - @Override - public void run() { - ClientCallStreamObserver spanClientCallStreamObserver = streamObserverSupplier.get(); - - if (spanClientCallStreamObserver == null) { - return; - } - - if (!spanClientCallStreamObserver.isReady()) { - try { - metricAggregator.incrementCounter("Supportability/InfiniteTracing/NotReady"); - Thread.sleep(250); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - } - return; - } - - SpanEvent spanEvent = pollSafely(); - if (spanEvent == null) { - return; - } - - V1.Span outputSpan = spanConverter.convert(spanEvent); - - try { - spanClientCallStreamObserver.onNext(outputSpan); - } catch (Throwable t) { - logger.log(Level.SEVERE, t, "Unable to send span!"); - throw t; - } - - metricAggregator.incrementCounter("Supportability/InfiniteTracing/Span/Sent"); - } - - private SpanEvent pollSafely() { - try { - return queue.poll(250, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - logger.log(Level.WARNING, "Thread was interrupted while polling for spans."); - Thread.currentThread().interrupt(); - return null; - } - } -} diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanEventConsumer.java b/infinite-tracing/src/main/java/com/newrelic/SpanEventConsumer.java deleted file mode 100644 index e7e90d7c65..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/SpanEventConsumer.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.newrelic; - -import com.google.common.annotations.VisibleForTesting; -import com.newrelic.agent.interfaces.backport.Consumer; -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.api.agent.Logger; -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.V1; -import io.grpc.ClientInterceptor; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; - -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Accepts a {@link SpanEvent} for publishing to the Trace Observer. - * - * Not thread-safe. - */ -public class SpanEventConsumer implements Consumer { - private final BlockingQueue queue; - private final MetricAggregator aggregator; - private final ConnectionHeaders connectionHeaders; - private final Runnable spanSender; - private final ExecutorService executorService; - private final AtomicBoolean wasStarted = new AtomicBoolean(false); - private volatile Future senderFuture; - - private SpanEventConsumer(BlockingQueue queue, MetricAggregator aggregator, ConnectionHeaders connectionHeaders, - Runnable spanSender, ExecutorService executorService) { - this.queue = queue; - this.aggregator = aggregator; - this.connectionHeaders = connectionHeaders; - this.spanSender = spanSender; - this.executorService = executorService; - } - - @Override - public void accept(SpanEvent spanEvent) { - aggregator.incrementCounter("Supportability/InfiniteTracing/Span/Seen"); - queue.offer(spanEvent); - } - - public static SpanEventConsumer.Builder builder(InfiniteTracingConfig config, MetricAggregator metricAggregator) { - return new Builder(config, metricAggregator); - } - - public void start() { - if (wasStarted.compareAndSet(false, true)) { - senderFuture = executorService.submit(spanSender); - } - } - - public void stop() { - if (wasStarted.compareAndSet(true,false)) { - senderFuture.cancel(true); - senderFuture = null; - } - } - - public void setConnectionMetadata(String newRunToken, Map headers) { - connectionHeaders.set(newRunToken, headers); - } - - public static class Builder { - private final InfiniteTracingConfig config; - private final SpanConverter spanConverter = new GrpcSpanConverter(); - private final MetricAggregator metricAggregator; - private final Logger logger; - private final BlockingQueue queue; - - private ChannelFactory channelFactory; - private StreamObserverFactory streamObserverFactory; - - public Builder(InfiniteTracingConfig config, MetricAggregator metricAggregator) { - this.logger = config.getLogger(); - this.queue = new LinkedBlockingQueue<>(config.getMaxQueueSize()); - this.metricAggregator = metricAggregator; - this.config = config; - } - - @VisibleForTesting - public Builder setChannelFactory(ChannelFactory channelFactory) { - this.channelFactory = channelFactory; - return this; - } - - @VisibleForTesting - public Builder setStreamObserverFactory(StreamObserverFactory streamObserverFactory) { - this.streamObserverFactory = streamObserverFactory; - return this; - } - - public SpanEventConsumer build() { - BackoffPolicy backoffPolicy = new DefaultBackoffPolicy(); - ConnectionStatus connectionStatus = new ConnectionStatus(logger); - - ConnectionHeaders connectionHeaders = new ConnectionHeaders(connectionStatus, logger, config.getLicenseKey()); - ClientInterceptor clientInterceptor = new HeadersInterceptor(connectionHeaders); - ClientInterceptor maybeInjectFlakyHeader = new FlakyHeaderInterceptor(config); - DisconnectionHandler disconnectionHandler = new DisconnectionHandler(connectionStatus, backoffPolicy, logger); - AtomicBoolean shouldRecreateCall = new AtomicBoolean(false); - - ResponseObserver responseObserver = new ResponseObserver(metricAggregator, logger, disconnectionHandler, shouldRecreateCall); - - ChannelFactory channelFactory = this.channelFactory != null - ? this.channelFactory - : new ChannelFactory(config, clientInterceptor, maybeInjectFlakyHeader); - - StreamObserverFactory streamObserverFactory = this.streamObserverFactory != null - ? this.streamObserverFactory - : new StreamObserverFactory(metricAggregator, responseObserver); - - Function> channelToStreamObserverConverter = - new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - - Supplier channelSupplier = new ChannelSupplier(channelFactory, connectionStatus, logger); - - Supplier> streamObserverSupplier = new StreamObserverSupplier(channelSupplier, channelToStreamObserverConverter); - - Runnable spanDeliveryConsumer = new SpanDelivery(spanConverter, metricAggregator, logger, queue, streamObserverSupplier); - - Runnable loopForever = new LoopForever(logger, spanDeliveryConsumer); - - ExecutorService executorService = Executors.newSingleThreadExecutor(new DaemonThreadFactory("Span Event Consumer")); - - return new SpanEventConsumer(queue, metricAggregator, connectionHeaders, loopForever, executorService); - } - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java b/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java new file mode 100644 index 0000000000..9731819cc4 --- /dev/null +++ b/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java @@ -0,0 +1,142 @@ +package com.newrelic; + +import com.google.common.annotations.VisibleForTesting; +import com.newrelic.agent.model.SpanEvent; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import com.newrelic.trace.v1.V1; +import io.grpc.stub.ClientCallStreamObserver; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +class SpanEventSender implements Runnable { + + private final Logger logger; + private final BlockingQueue queue; + private final MetricAggregator aggregator; + private final ChannelManager channelManager; + + SpanEventSender(InfiniteTracingConfig config, BlockingQueue queue, MetricAggregator aggregator, ChannelManager channelManager) { + this.logger = config.getLogger(); + this.queue = queue; + this.aggregator = aggregator; + this.channelManager = channelManager; + } + + /** + * Run a continuous loop polling the {@link #queue} for the next span event and writing it + * to the Infinite Trace Observer via gRPC. + */ + @Override + public void run() { + logger.log(Level.FINE, "Initializing {0}", this.getClass().getSimpleName()); + while (true) { + try { + pollAndWrite(); + } catch (Throwable t) { + logger.log(Level.SEVERE, t, "A problem occurred and no further spans will be sent."); + return; + } + } + } + + @VisibleForTesting + void pollAndWrite() { + // Get stream observer + ClientCallStreamObserver observer = channelManager.getSpanObserver(); + + // Confirm the observer is ready + if (!awaitReadyObserver(observer)) { + return; + } + + // Poll queue for span + SpanEvent span = pollSafely(); + if (span == null) { + return; + } + + // Convert span and write to observer + V1.Span convertedSpan = convert(span); + writeToObserver(observer, convertedSpan); + } + + @VisibleForTesting + boolean awaitReadyObserver(ClientCallStreamObserver observer) { + if (observer.isReady()) { + return true; + } + try { + logger.log(Level.FINE, "Waiting for gRPC span observer to be ready."); + aggregator.incrementCounter("Supportability/InfiniteTracing/NotReady"); + Thread.sleep(250); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted while awaiting ready gRPC span observer."); + } + return false; + } + + @VisibleForTesting + SpanEvent pollSafely() { + try { + return queue.poll(250, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted while polling for spans."); + } + } + + @VisibleForTesting + static V1.Span convert(SpanEvent spanEvent) { + Map intrinsicAttributes = copyAttributes(spanEvent.getIntrinsics()); + Map userAttributes = copyAttributes(spanEvent.getUserAttributesCopy()); + Map agentAttributes = copyAttributes(spanEvent.getAgentAttributes()); + + intrinsicAttributes.put("appName", V1.AttributeValue.newBuilder().setStringValue(spanEvent.getAppName()).build()); + + return V1.Span.newBuilder() + .setTraceId(spanEvent.getTraceId()) + .putAllIntrinsics(intrinsicAttributes) + .putAllAgentAttributes(agentAttributes) + .putAllUserAttributes(userAttributes) + .build(); + } + + private static Map copyAttributes(Map original) { + Map copy = new HashMap<>(); + if (original == null) { + return copy; + } + + for (Map.Entry entry : original.entrySet()) { + Object value = entry.getValue(); + if (value instanceof String) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setStringValue((String) value).build()); + } else if (value instanceof Long || value instanceof Integer) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setIntValue(((Number) value).longValue()).build()); + } else if (value instanceof Float || value instanceof Double) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setDoubleValue(((Number) value).doubleValue()).build()); + } else if (value instanceof Boolean) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setBoolValue((Boolean) value).build()); + } + } + return copy; + } + + @VisibleForTesting + void writeToObserver(ClientCallStreamObserver observer, V1.Span span) { + try { + observer.onNext(span); + } catch (Throwable t) { + logger.log(Level.SEVERE, t, "Unable to send span."); + throw t; + } + aggregator.incrementCounter("Supportability/InfiniteTracing/Span/Sent"); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/StreamObserverFactory.java b/infinite-tracing/src/main/java/com/newrelic/StreamObserverFactory.java deleted file mode 100644 index 02347abeef..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/StreamObserverFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.IngestServiceGrpc; -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; -import io.grpc.stub.StreamObserver; - -public class StreamObserverFactory { - private final MetricAggregator aggregator; - private final StreamObserver responseObserver; - - public StreamObserverFactory( - MetricAggregator aggregator, - StreamObserver responseObserver) { - this.aggregator = aggregator; - this.responseObserver = responseObserver; - } - - public ClientCallStreamObserver buildStreamObserver(ManagedChannel channel) { - IngestServiceGrpc.IngestServiceStub ingestServiceFutureStub = IngestServiceGrpc.newStub(channel); - ClientCallStreamObserver streamObserver = (ClientCallStreamObserver) ingestServiceFutureStub.recordSpan(responseObserver); - - aggregator.incrementCounter("Supportability/InfiniteTracing/Connect"); - return streamObserver; - } - -} diff --git a/infinite-tracing/src/main/java/com/newrelic/StreamObserverSupplier.java b/infinite-tracing/src/main/java/com/newrelic/StreamObserverSupplier.java deleted file mode 100644 index 2aa1171785..0000000000 --- a/infinite-tracing/src/main/java/com/newrelic/StreamObserverSupplier.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; - -public class StreamObserverSupplier implements Supplier> { - - private final Supplier channelSupplier; - private final Function> channelToStreamObserverConverter; - - public StreamObserverSupplier(Supplier channelSupplier, - Function> channelToStreamObserverConverter) { - this.channelSupplier = channelSupplier; - this.channelToStreamObserverConverter = channelToStreamObserverConverter; - } - - @Override - public ClientCallStreamObserver get() { - return channelToStreamObserverConverter.apply(channelSupplier.get()); - } -} diff --git a/infinite-tracing/src/test/java/com/newrelic/ChannelFactoryTest.java b/infinite-tracing/src/test/java/com/newrelic/ChannelFactoryTest.java deleted file mode 100644 index 36800664b3..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ChannelFactoryTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.newrelic; - -import io.grpc.ClientInterceptor; -import io.grpc.ManagedChannel; -import io.grpc.okhttp.OkHttpChannelBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.Mockito.*; - -class ChannelFactoryTest { - - private InfiniteTracingConfig mockConfig; - - @BeforeEach - void setup() { - mockConfig = mock(InfiniteTracingConfig.class); - when(mockConfig.getHost()).thenReturn("localhost"); - when(mockConfig.getPort()).thenReturn(8989); - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - @Test - void shouldInjectAllAppropriateValues() { - ChannelFactory target = new ChannelFactory(mockConfig, new HeadersInterceptor[0]); - - ManagedChannel channel = target.createChannel(); - assertNotNull(channel); - - verify(mockConfig).getHost(); - verify(mockConfig).getPort(); - } - - @Test - void testDefaultConfigUsesSSL() { - - InfiniteTracingConfig config = InfiniteTracingConfig.builder() - .host("a") - .port(3) - .build(); - final OkHttpChannelBuilder builder = mock(OkHttpChannelBuilder.class); - ManagedChannel channel = mock(ManagedChannel.class); - - when(builder.defaultLoadBalancingPolicy("pick_first")).thenReturn(builder); - when(builder.intercept(any(ClientInterceptor[].class))).thenReturn(builder); - when(builder.enableRetry()).thenReturn(builder); - when(builder.defaultServiceConfig(isA(Map.class))).thenReturn(builder); - when(builder.build()).thenReturn(channel); - - ChannelFactory target = new ChannelFactory(config, new HeadersInterceptor[0]) { - @Override - OkHttpChannelBuilder newOkHttpChannelBuilder() { - return builder; - } - }; - - ManagedChannel resultChannel = target.createChannel(); - assertSame(channel, resultChannel); - verify(builder).useTransportSecurity(); - verify(builder, never()).usePlaintext(); - } -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ChannelManagerTest.java b/infinite-tracing/src/test/java/com/newrelic/ChannelManagerTest.java new file mode 100644 index 0000000000..f043e3ad5b --- /dev/null +++ b/infinite-tracing/src/test/java/com/newrelic/ChannelManagerTest.java @@ -0,0 +1,160 @@ +package com.newrelic; + +import com.google.common.collect.ImmutableMap; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import com.newrelic.trace.v1.IngestServiceGrpc.IngestServiceStub; +import com.newrelic.trace.v1.V1; +import io.grpc.ManagedChannel; +import io.grpc.stub.ClientCallStreamObserver; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ChannelManagerTest { + + @Mock + private Logger logger; + @Mock + private InfiniteTracingConfig config; + @Mock + private MetricAggregator aggregator; + @Mock + private ManagedChannel managedChannel; + @Mock + private ClientCallStreamObserver spanObserver; + @Mock + private ResponseObserver responseObserver; + @Mock + private IngestServiceStub stub; + + private ChannelManager target; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + when(config.getLogger()).thenReturn(logger); + target = spy(new ChannelManager(config, aggregator, "agentToken", ImmutableMap.of("key1", "value1"))); + doReturn(managedChannel).when(target).buildChannel(); + doReturn(stub).when(target).buildStub(managedChannel); + doReturn(responseObserver).when(target).buildResponseObserver(); + doReturn(spanObserver).when(stub).recordSpan(responseObserver); + } + + @Test + void getSpanObserver_ShutdownThrowsException() { + target.shutdownChannelForever(); + + assertThrows(RuntimeException.class, new Executable() { + @Override + public void execute() { + target.getSpanObserver(); + } + }); + } + + @Test + void getSpanObserver_BuildsChannelAndSpanObserverWhenMissing() { + assertEquals(spanObserver, target.getSpanObserver()); + assertEquals(spanObserver, target.getSpanObserver()); + + verify(target, times(1)).buildChannel(); + verify(target, times(1)).buildStub(managedChannel); + verify(target, times(1)).buildResponseObserver(); + verify(stub, times(1)).recordSpan(responseObserver); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Connect"); + } + + @Test + @Timeout(15) + void getSpanObserver_AwaitsBackoff() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(2); + + // Submit a task to initiate a backoff + final AtomicLong backoffCompletedAt = new AtomicLong(); + Future backoffFuture = executorService.submit(new Runnable() { + @Override + public void run() { + target.shutdownChannelAndBackoff(5); + backoffCompletedAt.set(System.currentTimeMillis()); + } + }); + + // Obtain a span observer in another thread, confirming it waits for the backoff to complete + final AtomicLong getSpanObserverCompletedAt = new AtomicLong(); + Future> futureSpanObserver = executorService.submit(new Callable>() { + @Override + public ClientCallStreamObserver call() { + try { + // Wait for the backoff task to have initiated backoff + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException("Thread interrupted while sleeping."); + } + + ClientCallStreamObserver response = target.getSpanObserver(); + getSpanObserverCompletedAt.set(System.currentTimeMillis()); + return response; + } + }); + + backoffFuture.get(); + assertEquals(spanObserver, futureSpanObserver.get()); + assertTrue(backoffCompletedAt.get() > 0); + assertTrue(getSpanObserverCompletedAt.get() > 0); + assertTrue(getSpanObserverCompletedAt.get() >= backoffCompletedAt.get()); + } + + @Test + void cancelSpanObserver_GetSpanObserverRebuildsWhenNextCalled() { + assertEquals(spanObserver, target.getSpanObserver()); + target.cancelSpanObserver(); + assertEquals(spanObserver, target.getSpanObserver()); + + verify(spanObserver).cancel(eq("CLOSING_CONNECTION"), any(ChannelClosingException.class)); + // Channel is only built once + verify(target, times(1)).buildChannel(); + // Span observer is built twice + verify(target, times(2)).buildStub(managedChannel); + verify(target, times(2)).buildResponseObserver(); + verify(stub, times(2)).recordSpan(responseObserver); + verify(aggregator, times(2)).incrementCounter("Supportability/InfiniteTracing/Connect"); + } + + @Test + void shutdownChannelAndBackoff_ShutsDownChannelCancelsSpanObserver() { + assertEquals(spanObserver, target.getSpanObserver()); + target.shutdownChannelAndBackoff(0); + assertEquals(spanObserver, target.getSpanObserver()); + + verify(target).cancelSpanObserver(); + // Channel and span observer is built twice + verify(target, times(2)).buildChannel(); + verify(target, times(2)).buildStub(managedChannel); + verify(target, times(2)).buildResponseObserver(); + verify(stub, times(2)).recordSpan(responseObserver); + verify(aggregator, times(2)).incrementCounter("Supportability/InfiniteTracing/Connect"); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ChannelSupplierTest.java b/infinite-tracing/src/test/java/com/newrelic/ChannelSupplierTest.java deleted file mode 100644 index 6abf6ed89c..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ChannelSupplierTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.ManagedChannel; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class ChannelSupplierTest { - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - @Mock - public ChannelFactory channelFactory; - @Mock - public ManagedChannel mockChannel; - @Mock - public ConnectionStatus connectionStatus; - - @Test - public void shouldCallFactoryIfNotAlreadyExisting() throws InterruptedException { - ChannelSupplier target = prepTargetForFirstCall(); - - assertSame(mockChannel, target.get()); - verify(channelFactory, times(1)).createChannel(); - } - - @Test - public void shouldNotCallFactoryIfAlreadyExisting() throws InterruptedException { - ChannelSupplier target = prepTargetForFirstCall(); - assertSame(mockChannel, target.get()); - verify(channelFactory, times(1)).createChannel(); - - when(connectionStatus.blockOnConnection()).thenReturn(ConnectionStatus.BlockResult.ALREADY_CONNECTED); - assertSame(mockChannel, target.get()); - verify(channelFactory, times(1)).createChannel(); - } - - @Test - public void shouldThrowIfGoingAway() throws InterruptedException { - final ChannelSupplier target = prepTargetForFirstCall(); - reset(connectionStatus); - when(connectionStatus.blockOnConnection()).thenReturn(ConnectionStatus.BlockResult.GO_AWAY_FOREVER); - - assertThrows(RuntimeException.class, new Executable() { - @Override - public void execute() { - target.get(); - } - }); - } - - @Test - public void shutsDownOldChannel() throws InterruptedException { - ChannelSupplier target = prepTargetForFirstCall(); - assertSame(mockChannel, target.get()); - - when(channelFactory.createChannel()).thenReturn(mock(ManagedChannel.class)); - assertNotSame(mockChannel, target.get()); - verify(mockChannel, times(1)).shutdown(); - } - - public ChannelSupplier prepTargetForFirstCall() throws InterruptedException { - when(channelFactory.createChannel()).thenReturn(mockChannel); - when(connectionStatus.blockOnConnection()).thenReturn(ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION); - return new ChannelSupplier(channelFactory, connectionStatus, mock(Logger.class)); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ChannelToStreamObserverTest.java b/infinite-tracing/src/test/java/com/newrelic/ChannelToStreamObserverTest.java deleted file mode 100644 index 125e06135c..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ChannelToStreamObserverTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.newrelic; - -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -class ChannelToStreamObserverTest { - - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - - public AtomicBoolean shouldRecreateCall = new AtomicBoolean(); - - @Mock - public StreamObserverFactory streamObserverFactory; - @Mock - public ManagedChannel mockChannel; - @Mock - public ClientCallStreamObserver streamObserver; - - @SuppressWarnings("unchecked") - public ClientCallStreamObserver mockStreamObserver() { - return (ClientCallStreamObserver) mock(ClientCallStreamObserver.class); - } - - @Test - public void returnsNullImmediatelyWithNullChannel() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - shouldRecreateCall.set(false); - when(streamObserverFactory.buildStreamObserver(null)) - .thenThrow(new AssertionError("~~ should not have been called ~~")); - - ClientCallStreamObserver result = target.apply(null); - assertNull(result); - verifyNoInteractions(streamObserverFactory); - } - - @Test - public void createsAndReturnsNewStreamObserver() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - shouldRecreateCall.set(false); - when(streamObserverFactory.buildStreamObserver(mockChannel)).thenReturn(streamObserver); - - ClientCallStreamObserver result = target.apply(mockChannel); - assertSame(streamObserver, result); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - } - - @Test - public void doesNotRecreateStreamObserverIfCalledWithSameChannel() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - shouldRecreateCall.set(false); - when(streamObserverFactory.buildStreamObserver(mockChannel)).thenReturn(streamObserver); - - ClientCallStreamObserver result = target.apply(mockChannel); - assertSame(streamObserver, result); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - - result = target.apply(mockChannel); - assertSame(streamObserver, result); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - } - - @Test - public void recreatesStreamObserverIfRequired() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - ClientCallStreamObserver mockObserver1 = mockStreamObserver(); - ClientCallStreamObserver mockObserver2 = mockStreamObserver(); - - shouldRecreateCall.set(false); - when(streamObserverFactory.buildStreamObserver(any(ManagedChannel.class))).thenReturn(mockObserver1).thenReturn(mockObserver2); - - ClientCallStreamObserver firstResult = target.apply(mockChannel); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - - shouldRecreateCall.set(true); - ClientCallStreamObserver secondResult = target.apply(mockChannel); - assertNotSame(secondResult, firstResult); - verify(streamObserverFactory, times(2)).buildStreamObserver(mockChannel); - } - - @Test - public void recreatesStreamObserverIfCalledWithDifferentChannel() { - ChannelToStreamObserver target = new ChannelToStreamObserver(streamObserverFactory, shouldRecreateCall); - ClientCallStreamObserver mockObserver1 = mockStreamObserver(); - ClientCallStreamObserver mockObserver2 = mockStreamObserver(); - - shouldRecreateCall.set(false); - when(streamObserver.isReady()).thenReturn(true); - when(streamObserverFactory.buildStreamObserver(any(ManagedChannel.class))).thenReturn(mockObserver1).thenReturn(mockObserver2); - - ClientCallStreamObserver result = target.apply(mockChannel); - verify(streamObserverFactory, times(1)).buildStreamObserver(mockChannel); - - ManagedChannel newChannel = mock(ManagedChannel.class); - ClientCallStreamObserver newResult = target.apply(newChannel); - assertNotSame(result, newResult); - verify(streamObserverFactory, times(1)).buildStreamObserver(newChannel); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ConnectionHeadersTest.java b/infinite-tracing/src/test/java/com/newrelic/ConnectionHeadersTest.java deleted file mode 100644 index 57eb8a6328..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ConnectionHeadersTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import org.junit.jupiter.api.Test; - -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -class ConnectionHeadersTest { - @Test - public void shouldUpdateConnectionStatus() { - ConnectionStatus connectionStatus = mock(ConnectionStatus.class); - ConnectionHeaders target = new ConnectionHeaders(connectionStatus, mock(Logger.class), "license key abc"); - - assertNull(target.get()); - - target.set("bar", Collections.singletonMap("OTHER_KEY", "other value")); - verify(connectionStatus).reattemptConnection(); - assertEquals("other value", target.get().get("OTHER_KEY")); - assertEquals("bar", target.get().get("agent_run_token")); - assertEquals("license key abc", target.get().get("license_key")); - } -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ConnectionStatusTest.java b/infinite-tracing/src/test/java/com/newrelic/ConnectionStatusTest.java deleted file mode 100644 index eb231fcfc9..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/ConnectionStatusTest.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.Status; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; - -class ConnectionStatusTest { - @Test - public void onlyOneThreadStartsConnecting() throws InterruptedException { - final ConnectionStatus status = new ConnectionStatus(mock(Logger.class)); - final AtomicInteger numConnectingThreads = new AtomicInteger(0); - final AtomicInteger numFailingThreads = new AtomicInteger(0); - - int numThreads = 10; - List threads = new ArrayList<>(); - final CyclicBarrier barrier = new CyclicBarrier(numThreads); - - while (threads.size() < numThreads) { - final Thread thread = new Thread(new Runnable() { - @Override - public void run() { - try { - barrier.await(); - if (status.blockOnConnection() == ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION) { - numConnectingThreads.incrementAndGet(); - Thread.sleep(100); // emulate time it takes to establish connection - status.didConnect(); - } - } catch (Throwable t) { - numFailingThreads.incrementAndGet(); - } - } - }); - thread.setName("Thread " + threads.size()); - thread.start(); - threads.add(thread); - } - - for (Thread thread : threads) { - thread.join(2000); - if (thread.isAlive()) { - fail(thread.getName() + " is still alive"); - } - } - - assertEquals(0, numFailingThreads.get()); - assertEquals(1, numConnectingThreads.get()); - } - - @Test - public void onlyOneThreadBacksOff() throws InterruptedException { - final ConnectionStatus status = new ConnectionStatus(mock(Logger.class)); - status.didConnect(); - - final AtomicInteger numBackingOffThreads = new AtomicInteger(0); - final AtomicInteger numFailingThreads = new AtomicInteger(0); - - int numThreads = 10; - List threads = new ArrayList<>(); - final CyclicBarrier barrier = new CyclicBarrier(numThreads); - - while (threads.size() < numThreads) { - final Thread thread = new Thread(new Runnable() { - @Override - public void run() { - try { - barrier.await(); - if (status.shouldReconnect()) { - numBackingOffThreads.incrementAndGet(); - Thread.sleep(100); // emulate backoff time - status.reattemptConnection(); - } - } catch (Throwable t) { - numFailingThreads.incrementAndGet(); - } - } - }); - thread.setName("Thread " + threads.size()); - thread.start(); - threads.add(thread); - } - - for (Thread thread : threads) { - thread.join(2000); - if (thread.isAlive()) { - fail(thread.getName() + " is still alive"); - } - } - - assertEquals(0, numFailingThreads.get()); - assertEquals(1, numBackingOffThreads.get()); - } - - @Test - @Timeout(2) - public void shouldReconnectIfDisconnected() throws InterruptedException { - ConnectionStatus target = new ConnectionStatus(mock(Logger.class)); - assertEquals(ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION, target.blockOnConnection()); - target.didConnect(); - assertEquals(ConnectionStatus.BlockResult.ALREADY_CONNECTED, target.blockOnConnection()); - target.reattemptConnection(); - assertEquals(ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION, target.blockOnConnection()); - } - - @Test - public void shouldGoAwayIfToldTo() throws InterruptedException { - ConnectionStatus target = new ConnectionStatus(mock(Logger.class)); - assertEquals(ConnectionStatus.BlockResult.MUST_ATTEMPT_CONNECTION, target.blockOnConnection()); - target.didConnect(); - assertEquals(ConnectionStatus.BlockResult.ALREADY_CONNECTED, target.blockOnConnection()); - target.shutDownForever(); - assertEquals(ConnectionStatus.BlockResult.GO_AWAY_FOREVER, target.blockOnConnection()); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/DaemonThreadFactoryTest.java b/infinite-tracing/src/test/java/com/newrelic/DaemonThreadFactoryTest.java deleted file mode 100644 index e46f535250..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/DaemonThreadFactoryTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.newrelic; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class DaemonThreadFactoryTest { - @Test - public void createsDaemonThreads() { - DaemonThreadFactory target = new DaemonThreadFactory("service name"); - Thread result1 = target.newThread(new NoOpRunnable()); - assertTrue(result1.isDaemon()); - assertEquals("New Relic service name #1", result1.getName()); - - Thread result2 = target.newThread(new NoOpRunnable()); - assertTrue(result2.isDaemon()); - assertEquals("New Relic service name #2", result2.getName()); - } - - private static class NoOpRunnable implements Runnable { - @Override - public void run() { - } - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/DefaultBackoffPolicyTest.java b/infinite-tracing/src/test/java/com/newrelic/DefaultBackoffPolicyTest.java deleted file mode 100644 index 5f00ec22fb..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/DefaultBackoffPolicyTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.newrelic; - -import io.grpc.Status; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -import static org.junit.jupiter.api.Assertions.*; - -class DefaultBackoffPolicyTest { - @Test - public void shouldReconnectOnMostErrors() { - DefaultBackoffPolicy target = new DefaultBackoffPolicy(); - - assertTrue(target.shouldReconnect(null)); - assertTrue(target.shouldReconnect(Status.OK)); - assertTrue(target.shouldReconnect(Status.UNAVAILABLE)); - assertTrue(target.shouldReconnect(Status.CANCELLED)); - assertTrue(target.shouldReconnect(Status.DEADLINE_EXCEEDED)); - assertTrue(target.shouldReconnect(Status.UNAUTHENTICATED)); - } - - @Test - public void shouldNotReconnectOnUnimplemented() { - DefaultBackoffPolicy target = new DefaultBackoffPolicy(); - - assertFalse(target.shouldReconnect(Status.UNIMPLEMENTED)); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/DisconnectionHandlerTest.java b/infinite-tracing/src/test/java/com/newrelic/DisconnectionHandlerTest.java deleted file mode 100644 index 1adbaa7e68..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/DisconnectionHandlerTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.Status; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class DisconnectionHandlerTest { - @Test - public void shouldNotBackOffIfCannotSetStatus() { - DisconnectionHandler target = new DisconnectionHandler(connectionStatus, backoffPolicy, mock(Logger.class)); - when(connectionStatus.shouldReconnect()).thenReturn(false); - target.handle(null); - } - - @Test - public void shutdownIfShouldShutdown() { - DisconnectionHandler target = new DisconnectionHandler(connectionStatus, backoffPolicy, mock(Logger.class)); - when(connectionStatus.shouldReconnect()).thenReturn(true); - when(backoffPolicy.shouldReconnect(Status.ABORTED)).thenReturn(false); - target.handle(Status.ABORTED); - verify(connectionStatus).shutDownForever(); - } - - @Test - public void shouldBackOff() { - DisconnectionHandler target = new DisconnectionHandler(connectionStatus, backoffPolicy, mock(Logger.class)); - when(connectionStatus.shouldReconnect()).thenReturn(true); - when(backoffPolicy.shouldReconnect(Status.ABORTED)).thenReturn(true); - target.handle(Status.ABORTED); - verify(backoffPolicy).shouldReconnect(Status.ABORTED); - verify(connectionStatus).reattemptConnection(); - } - - @Test - public void shouldShutdownForeverOnUnimplemented() { - DisconnectionHandler target = new DisconnectionHandler(connectionStatus, backoffPolicy, mock(Logger.class)); - when(connectionStatus.shouldReconnect()).thenReturn(true); - when(backoffPolicy.shouldReconnect(Status.UNIMPLEMENTED)).thenReturn(false); - target.handle(Status.UNIMPLEMENTED); - verify(connectionStatus).shutDownForever(); - } - - @Mock - public ConnectionStatus connectionStatus; - - @Mock - public BackoffPolicy backoffPolicy; - - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/FlakyHeaderInterceptorTest.java b/infinite-tracing/src/test/java/com/newrelic/FlakyHeaderInterceptorTest.java deleted file mode 100644 index cbf910588e..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/FlakyHeaderInterceptorTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.newrelic; - -import com.newrelic.api.agent.Logger; -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.InternalMetadata; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class FlakyHeaderInterceptorTest { - - private static final Metadata.Key K_KEY = Metadata.Key.of("k1", ASCII_STRING_MARSHALLER); - public static final Metadata.Key FLAKY_KEY = Metadata.Key.of("flaky", ASCII_STRING_MARSHALLER); - Metadata originalHeaders = InternalMetadata.newMetadata(); - - @BeforeEach - void setup() { - originalHeaders = InternalMetadata.newMetadata(); - originalHeaders.put(K_KEY, "v1"); - } - - @Test - void testInjectHeader() { - final Double flakyValue = 15.6; - InfiniteTracingConfig config = InfiniteTracingConfig.builder() - .logger(mock(Logger.class)) - .flakyPercentage(flakyValue) - .build(); - - MethodDescriptor method = mock(MethodDescriptor.class); - CallOptions callOptions = mock(CallOptions.class); - Channel next = mock(Channel.class); - MockForwardingClientCall newCallNext = new MockForwardingClientCall(); - ClientCall.Listener responseListener = new ClientCall.Listener() { - }; - - when(next.newCall(method, callOptions)).thenReturn(newCallNext); - - FlakyHeaderInterceptor testClass = new FlakyHeaderInterceptor(config); - - ClientCall result = testClass.interceptCall(method, callOptions, next); - - result.start(responseListener, originalHeaders); - assertEquals(1, newCallNext.seenHeadersSize()); - assertEquals("v1", newCallNext.getHeader(K_KEY)); - assertEquals("15.6", newCallNext.getHeader(FLAKY_KEY)); - } - - @Test - void testNotConfigured() { - InfiniteTracingConfig config = InfiniteTracingConfig.builder().build(); - - MethodDescriptor method = mock(MethodDescriptor.class); - CallOptions callOptions = mock(CallOptions.class); - Channel next = mock(Channel.class); - MockForwardingClientCall newCallNext = new MockForwardingClientCall(); - ClientCall.Listener responseListener = new ClientCall.Listener() { - }; - - when(next.newCall(method, callOptions)).thenReturn(newCallNext); - - FlakyHeaderInterceptor testClass = new FlakyHeaderInterceptor(config); - - ClientCall result = testClass.interceptCall(method, callOptions, next); - - result.start(responseListener, originalHeaders); - assertEquals(1, newCallNext.seenHeadersSize()); - assertEquals("v1", newCallNext.getHeader(K_KEY)); - assertFalse(newCallNext.containsKey(FLAKY_KEY)); - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/GrpcSpanConverterTest.java b/infinite-tracing/src/test/java/com/newrelic/GrpcSpanConverterTest.java deleted file mode 100644 index 8e9ecd8069..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/GrpcSpanConverterTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.trace.v1.V1; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class GrpcSpanConverterTest { - private enum TestEnum { ONE } - - @Test - public void shouldSerializeIntrinsicAttributesOK() throws IOException { - SpanEvent spanEvent = makeSpanWithIntrinsics(); - - V1.Span deserialized = from(spanEvent); - assertEquals("my app", deserialized.getIntrinsicsOrThrow("appName").getStringValue()); - assertEquals("value", deserialized.getIntrinsicsOrThrow("intrStr").getStringValue()); - assertEquals(12345, deserialized.getIntrinsicsOrThrow("intrInt").getIntValue()); - assertEquals(3.14, deserialized.getIntrinsicsOrThrow("intrFloat").getDoubleValue(), 0.00001); - assertTrue(deserialized.getIntrinsicsOrThrow("intrBool").getBoolValue()); - - assertFalse(deserialized.containsIntrinsics("intrOther")); - } - - @Test - public void shouldStoreTraceIdBothPlaces() throws IOException { - SpanEvent spanEvent = makeSpanWithIntrinsics(); - - V1.Span deserialized = from(spanEvent); - assertEquals("abc123", deserialized.getTraceId()); - assertEquals("abc123", deserialized.getIntrinsicsOrThrow("traceId").getStringValue()); - } - - private V1.Span from(SpanEvent spanEvent) throws IOException { - GrpcSpanConverter target = new GrpcSpanConverter(); - V1.Span result = target.convert(spanEvent); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - result.writeTo(baos); - return V1.Span.parseFrom(baos.toByteArray()); - } - - private SpanEvent makeSpanWithIntrinsics() { - return SpanEvent.builder() - .appName("my app") - .putIntrinsic("traceId", "abc123") - .putIntrinsic("intrStr", "value") - .putIntrinsic("intrInt", 12345) - .putIntrinsic("intrFloat", 3.14) - .putIntrinsic("intrBool", true) - .putIntrinsic("intrOther", TestEnum.ONE) - .build(); - } -} diff --git a/infinite-tracing/src/test/java/com/newrelic/HeadersInterceptorTest.java b/infinite-tracing/src/test/java/com/newrelic/HeadersInterceptorTest.java index 5e9c3b55b2..7ba99ad77f 100644 --- a/infinite-tracing/src/test/java/com/newrelic/HeadersInterceptorTest.java +++ b/infinite-tracing/src/test/java/com/newrelic/HeadersInterceptorTest.java @@ -1,15 +1,17 @@ package com.newrelic; import com.google.common.collect.ImmutableMap; -import com.newrelic.agent.interfaces.backport.Supplier; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; +import io.grpc.ForwardingClientCall; import io.grpc.InternalMetadata; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; @@ -20,7 +22,7 @@ class HeadersInterceptorTest { @Test - void testInjectHeader() { + void interceptCall_Valid() { MethodDescriptor method = mock(MethodDescriptor.class); CallOptions callOptions = mock(CallOptions.class); Channel next = mock(Channel.class); @@ -30,17 +32,10 @@ void testInjectHeader() { when(next.newCall(method, callOptions)).thenReturn(newCallNext); - Supplier> headerSupplier = new Supplier>() { - @Override - public Map get() { - return ImmutableMap.of( - "header1", "value1", - "WILL_BE_LOWER_CASED", "value2" - ); - } - }; - - HeadersInterceptor target = new HeadersInterceptor(headerSupplier); + Map headers = ImmutableMap.of( + "header1", "value1", + "WILL_BE_LOWER_CASED", "value2"); + HeadersInterceptor target = new HeadersInterceptor(headers); ClientCall result = target.interceptCall(method, callOptions, next); @@ -53,8 +48,28 @@ public Map get() { assertEquals("value2", newCallNext.getHeader(keyFor("will_be_lower_cased"))); } - private Metadata.Key keyFor(String key) { + private static Metadata.Key keyFor(String key) { return Metadata.Key.of(key, ASCII_STRING_MARSHALLER); } + static class MockForwardingClientCall extends ForwardingClientCall { + private final List seenHeaders = new ArrayList<>(); + + @Override + public void start(Listener responseListener, Metadata headers) { + seenHeaders.add(headers); + super.start(responseListener, headers); + } + + @Override + protected ClientCall delegate() { + return mock(ClientCall.class); + } + + public String getHeader(Metadata.Key key) { + return seenHeaders.get(0).get(key); + } + + } + } \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/InfiniteTracingTest.java b/infinite-tracing/src/test/java/com/newrelic/InfiniteTracingTest.java new file mode 100644 index 0000000000..c25c403f21 --- /dev/null +++ b/infinite-tracing/src/test/java/com/newrelic/InfiniteTracingTest.java @@ -0,0 +1,86 @@ +package com.newrelic; + +import com.google.common.collect.ImmutableMap; +import com.newrelic.agent.model.SpanEvent; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class InfiniteTracingTest { + + @Mock + private Logger logger; + @Mock + private InfiniteTracingConfig config; + @Mock + private MetricAggregator aggregator; + @Mock + private ExecutorService executorService; + @Mock + private ChannelManager channelManager; + @Mock + private SpanEventSender spanEventSender; + + private LinkedBlockingDeque queue; + private InfiniteTracing target; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + when(config.getLogger()).thenReturn(logger); + queue = new LinkedBlockingDeque<>(1); + target = spy(new InfiniteTracing(config, aggregator, executorService, queue)); + doReturn(channelManager).when(target).buildChannelManager(anyString(), ArgumentMatchers.anyMap()); + doReturn(spanEventSender).when(target).buildSpanEventSender(); + } + + @Test + void startAndStop() { + Future future = mock(Future.class); + when(executorService.submit(ArgumentMatchers.any())).thenReturn(future); + + target.start("token1", ImmutableMap.of("key1", "value1")); + target.start("token2", ImmutableMap.of("key2", "value2")); + + verify(target).buildChannelManager("token1", ImmutableMap.of("key1", "value1")); + verify(target).buildSpanEventSender(); + verify(executorService).submit(spanEventSender); + verify(channelManager).updateMetadata("token2", ImmutableMap.of("key2", "value2")); + verify(channelManager).shutdownChannelAndBackoff(0); + + target.stop(); + target.stop(); + + verify(future).cancel(true); + verify(channelManager).shutdownChannelForever(); + } + + @Test + @Timeout(1) + void accept_IncrementsCounterAndOffersToQueue() { + SpanEvent spanEvent = SpanEvent.builder().build(); + + target.accept(spanEvent); + + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Span/Seen"); + assertEquals(spanEvent, queue.poll()); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/MockForwardingClientCall.java b/infinite-tracing/src/test/java/com/newrelic/MockForwardingClientCall.java deleted file mode 100644 index f64a33947a..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/MockForwardingClientCall.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.newrelic; - -import io.grpc.ClientCall; -import io.grpc.ForwardingClientCall; -import io.grpc.Metadata; - -import java.util.ArrayList; -import java.util.List; - -import static org.mockito.Mockito.mock; - -class MockForwardingClientCall extends ForwardingClientCall { - private final List seenHeaders = new ArrayList<>(); - - @Override - public void start(Listener responseListener, Metadata headers) { - seenHeaders.add(headers); - super.start(responseListener, headers); - } - - @Override - protected ClientCall delegate() { - return mock(ClientCall.class); - } - - public int seenHeadersSize() { - return seenHeaders.size(); - } - - public String getHeader(Metadata.Key key) { - return seenHeaders.get(0).get(key); - } - - public boolean containsKey(Metadata.Key key) { - return seenHeaders.get(0).containsKey(key); - } -} diff --git a/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java b/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java index a107c2a9b8..1d3423e4fa 100644 --- a/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java +++ b/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java @@ -5,171 +5,126 @@ import com.newrelic.trace.v1.V1; import io.grpc.Status; import io.grpc.StatusRuntimeException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; +import static com.newrelic.ResponseObserver.BACKOFF_SECONDS_SEQUENCE; +import static com.newrelic.ResponseObserver.DEFAULT_BACKOFF_SECONDS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; class ResponseObserverTest { - AtomicBoolean shouldRecreateCall = new AtomicBoolean(); + @Mock + private Logger logger; + @Mock + private ChannelManager channelManager; + @Mock + private MetricAggregator aggregator; - @Test - public void shouldIncrementCounterOnNext() { - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - mock(DisconnectionHandler.class), shouldRecreateCall); + private ResponseObserver target; - target.onNext(V1.RecordStatus.newBuilder().setMessagesSeen(3000).build()); - - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Response"); + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + target = spy(new ResponseObserver(logger, channelManager, aggregator)); } @Test - public void shouldDisconnectOnNormalException() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - disconnectionHandler, shouldRecreateCall); + void onNext_IncrementsCounter() { + target.onNext(V1.RecordStatus.newBuilder().build()); - target.onError(new Throwable()); - - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Response/Error"); - verify(disconnectionHandler).handle(null); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Response"); } @Test - public void shouldReportStatusOnError() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - disconnectionHandler, shouldRecreateCall); - - StatusRuntimeException exception = new StatusRuntimeException(Status.CANCELLED); + void onError_ChannelClosingExceptionReturns() { + StatusRuntimeException exception = Status.CANCELLED.withCause(new ChannelClosingException()).asRuntimeException(); target.onError(exception); - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Span/gRPC/CANCELLED"); - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Response/Error"); - verify(disconnectionHandler).handle(Status.CANCELLED); + verifyNoInteractions(channelManager, aggregator); } @Test - public void shouldNotDisconnectWhenChannelClosing() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - disconnectionHandler, shouldRecreateCall); + void onError_AlpnErrorShutdownChannelForever() { + RuntimeException cause = new RuntimeException("TLS ALPN negotiation failed with protocols: [h2]"); + StatusRuntimeException exception = Status.UNAVAILABLE.withCause(cause).asRuntimeException(); - StatusRuntimeException exception = Status.CANCELLED.withCause(new ChannelClosingException()).asRuntimeException(); target.onError(exception); - verifyNoInteractions(disconnectionHandler, metricAggregator); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/NoALPNSupport"); + verify(channelManager).shutdownChannelForever(); } @Test - public void shouldDisconnectOnCompleted() { - DisconnectionHandler mockHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - mockHandler, shouldRecreateCall); - - target.onCompleted(); + void onError_UnimplementedShutdownChannelForever() { + target.onError(Status.UNIMPLEMENTED.asException()); - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/Response/Completed"); - assertTrue(shouldRecreateCall.get()); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Span/gRPC/" + Status.UNIMPLEMENTED.getCode()); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Response/Error"); + verify(channelManager).shutdownChannelForever(); } @Test - public void shouldTerminateOnALPNError() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); + void onError_OtherStatusShutdownChannelAndBackoff() { + doNothing().when(target).shutdownChannelAndBackoff(ArgumentMatchers.any()); - ResponseObserver target = new ResponseObserver( - metricAggregator, - mock(Logger.class), - disconnectionHandler, shouldRecreateCall); + target.onError(Status.INTERNAL.asException()); + target.onError(Status.FAILED_PRECONDITION.asException()); + target.onError(Status.UNKNOWN.asException()); - RuntimeException cause = new RuntimeException("TLS ALPN negotiation failed with protocols: [h2]"); - StatusRuntimeException exception = Status.UNAVAILABLE.withCause(cause).asRuntimeException(); - - target.onError(exception); - - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/NoALPNSupport"); - verify(disconnectionHandler).terminate(); + verify(aggregator, times(6)).incrementCounter(anyString()); + verify(target, times(3)).shutdownChannelAndBackoff(ArgumentMatchers.any()); } @Test - public void testIsConnectionTimeoutException() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - Logger logger = mock(Logger.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - logger, - disconnectionHandler, shouldRecreateCall); - - Throwable exception = new StatusRuntimeException( - Status.fromCode(Status.Code.INTERNAL).withDescription("No error: A GRPC status of OK should have been sent\nRst Stream")); - target.onError(exception); + void shutdownChannelAndBackoff_ConnectTimeoutBackoffZeroSeconds() { + Status status = Status.fromCode(Status.Code.INTERNAL).withDescription("No error: A GRPC status of OK should have been sent\nRst Stream"); - verify(logger, never()).log(Level.WARNING, exception, "Encountered gRPC exception"); + target.shutdownChannelAndBackoff(status); + + verify(logger).log(eq(Level.FINE), any(Throwable.class), anyString(), any()); + verify(channelManager).shutdownChannelAndBackoff(0); } @Test - public void testConnectionTimeoutExceptionWrongType() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - Logger logger = mock(Logger.class); - - ResponseObserver target = new ResponseObserver( - metricAggregator, - logger, - disconnectionHandler, shouldRecreateCall); - - Throwable exception = new RuntimeException("No error: A GRPC status of OK should have been sent\nRst Stream"); - target.onError(exception); - - verify(logger).log(Level.WARNING, exception, "Encountered gRPC exception"); + void shutdownChannelAndBackoff_FailedPreconditionBackoffSequence() { + for (int i = 0; i < 10; i++) { + target.shutdownChannelAndBackoff(Status.FAILED_PRECONDITION); + } + + verify(logger, times(10)).log(eq(Level.WARNING), any(Throwable.class), anyString(), any()); + verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(BACKOFF_SECONDS_SEQUENCE[0]); + verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(BACKOFF_SECONDS_SEQUENCE[BACKOFF_SECONDS_SEQUENCE.length - 1]); } @Test - public void testConnectionTimeoutExceptionWrongMessage() { - DisconnectionHandler disconnectionHandler = mock(DisconnectionHandler.class); - MetricAggregator metricAggregator = mock(MetricAggregator.class); - Logger logger = mock(Logger.class); + void shutdownChannelAndBackoff_OtherStatusDefaultBackoff() { + target.shutdownChannelAndBackoff(Status.UNKNOWN); - ResponseObserver target = new ResponseObserver( - metricAggregator, - logger, - disconnectionHandler, shouldRecreateCall); + verify(logger).log(eq(Level.WARNING), any(Throwable.class), anyString(), any()); + verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(DEFAULT_BACKOFF_SECONDS); + } - Throwable exception = new StatusRuntimeException(Status.fromCode(Status.Code.INTERNAL).withDescription("A REALLY BAD ERROR: PRINT ME")); - target.onError(exception); + @Test + void onCompleted_IncrementsCounterCancelsSpanObserver() { + target.onCompleted(); - verify(logger).log(Level.WARNING, exception, "Encountered gRPC exception"); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Response/Completed"); + verify(channelManager).cancelSpanObserver(); } } \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/SpanDeliveryTest.java b/infinite-tracing/src/test/java/com/newrelic/SpanDeliveryTest.java deleted file mode 100644 index 0674b5d97d..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/SpanDeliveryTest.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.api.agent.Logger; -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.V1; -import io.grpc.stub.ClientCallStreamObserver; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -class SpanDeliveryTest { - - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - - public BlockingQueue incomingQueue = new LinkedBlockingQueue<>(); - - @Mock - public SpanConverter spanConverter; - @Mock - public MetricAggregator metricAggregator; - @Mock - public Logger logger; - @Mock - public Supplier> streamObserverSupplier; - - @SuppressWarnings("unchecked") - public ClientCallStreamObserver mockStreamObserver() { - return (ClientCallStreamObserver) mock(ClientCallStreamObserver.class); - } - - @Test - public void noCallsIfStreamObserverNull() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - when(streamObserverSupplier.get()).thenReturn(null); - - incomingQueue.add(SpanEvent.builder().build()); - when(spanConverter.convert(any(SpanEvent.class))).thenAnswer(new AlwaysNewSpan()); - - target.run(); - verifyNoInteractions(spanConverter, metricAggregator, logger); - assertEquals(1, incomingQueue.size()); - } - - @Test - public void returnsIfStreamObserverNotReady() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - incomingQueue.add(SpanEvent.builder().build()); - when(spanConverter.convert(any(SpanEvent.class))).thenAnswer(new AlwaysNewSpan()); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(false); - target.run(); - - verify(mockObserver, times(1)).isReady(); - verify(metricAggregator).incrementCounter("Supportability/InfiniteTracing/NotReady"); - verifyNoInteractions(spanConverter, logger); - } - - @Test - public void doesNotCallOnNextIfQueueEmpty() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - when(spanConverter.convert(any(SpanEvent.class))).thenThrow(new AssertionError("should not convert")); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(true); - target.run(); - - verify(mockObserver, times(1)).isReady(); - verify(mockObserver, never()).onNext(any(V1.Span.class)); - } - - @Test - public void checksStreamObserverReadyAndCallsOnNext() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - incomingQueue.add(SpanEvent.builder().build()); - V1.Span mockSpan = mock(V1.Span.class); - when(spanConverter.convert(any(SpanEvent.class))).thenReturn(mockSpan); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(true); - target.run(); - - verify(mockObserver, times(1)).isReady(); - verify(mockObserver, times(1)).onNext(mockSpan); - } - - @Test - public void doesNotIncrementSentIfOnNextThrows() { - final SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - incomingQueue.add(SpanEvent.builder().build()); - V1.Span mockSpan = mock(V1.Span.class); - when(spanConverter.convert(any(SpanEvent.class))).thenReturn(mockSpan); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(true); - doThrow(new RuntimeException("~~ oops ~~")).when(mockObserver).onNext(any(V1.Span.class)); - - assertThrows(RuntimeException.class, new Executable() { - @Override - public void execute() { - target.run(); - } - }); - verifyNoInteractions(metricAggregator); - } - - @Test - public void incrementsMetricOnSuccess() { - SpanDelivery target = new SpanDelivery( - spanConverter, - metricAggregator, - logger, - incomingQueue, - streamObserverSupplier); - - incomingQueue.add(SpanEvent.builder().build()); - V1.Span mockSpan = mock(V1.Span.class); - when(spanConverter.convert(any(SpanEvent.class))).thenReturn(mockSpan); - - ClientCallStreamObserver mockObserver = mockStreamObserver(); - when(streamObserverSupplier.get()).thenReturn(mockObserver); - when(mockObserver.isReady()).thenReturn(true); - target.run(); - - verify(metricAggregator, times(1)).incrementCounter(anyString()); - } - - - private static class AlwaysNewSpan implements Answer { - @Override - public Object answer(InvocationOnMock invocation) { - return V1.Span.newBuilder().build(); - } - } -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/SpanEventConsumerTest.java b/infinite-tracing/src/test/java/com/newrelic/SpanEventConsumerTest.java deleted file mode 100644 index 396c1a4346..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/SpanEventConsumerTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.model.SpanEvent; -import com.newrelic.api.agent.Logger; -import com.newrelic.api.agent.MetricAggregator; -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.exceptions.verification.WantedButNotInvoked; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class SpanEventConsumerTest { - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - - @Mock - public ChannelFactory mockChannelFactory; - @Mock - public ManagedChannel mockChannel; - @Mock - public StreamObserverFactory mockStreamObserverFactory; - @Mock - public ClientCallStreamObserver mockStreamObserver; - @Mock - public Logger mockLogger; - @Mock - public MetricAggregator metricAggregator; - - @Test - @Timeout(30) - public void integrationTest() throws InterruptedException { - when(mockChannelFactory.createChannel()).thenReturn(mockChannel); - when(mockStreamObserverFactory.buildStreamObserver(mockChannel)).thenReturn(mockStreamObserver); - when(mockStreamObserver.isReady()).thenReturn(true); - - InfiniteTracingConfig config = InfiniteTracingConfig.builder() - .logger(mockLogger) - .maxQueueSize(10) - .build(); - - SpanEventConsumer target = SpanEventConsumer.builder(config, metricAggregator) - .setChannelFactory(mockChannelFactory) - .setStreamObserverFactory(mockStreamObserverFactory) - .build(); - - target.start(); - - SpanEvent incomingEvent = SpanEvent.builder() - .putIntrinsic("traceId", "123abc") - .putIntrinsic("guid", "some-intrinsic") - .appName("app-name") - .build(); - - target.accept(incomingEvent); - - while (true) { - ArgumentCaptor outgoingSpanCaptor = ArgumentCaptor.forClass(V1.Span.class); - try { - verify(mockStreamObserver, times(1)).onNext(outgoingSpanCaptor.capture()); - } catch (WantedButNotInvoked ignored) { - Thread.sleep(10); - continue; - } - - V1.Span capturedSpan = outgoingSpanCaptor.getValue(); - - assertEquals("123abc", capturedSpan.getTraceId()); - assertEquals("some-intrinsic", capturedSpan.getIntrinsicsOrThrow("guid").getStringValue()); - assertEquals("app-name", capturedSpan.getIntrinsicsOrThrow("appName").getStringValue()); - break; - } - - } - -} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java b/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java new file mode 100644 index 0000000000..bca065bfa6 --- /dev/null +++ b/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java @@ -0,0 +1,196 @@ +package com.newrelic; + +import com.newrelic.agent.model.SpanEvent; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.MetricAggregator; +import com.newrelic.trace.v1.V1; +import io.grpc.stub.ClientCallStreamObserver; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.function.Executable; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import static com.newrelic.SpanEventSender.convert; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class SpanEventSenderTest { + + @Mock + private Logger logger; + @Mock + private InfiniteTracingConfig config; + @Mock + private BlockingQueue queue; + @Mock + private MetricAggregator aggregator; + @Mock + private ChannelManager channelManager; + @Mock + private ClientCallStreamObserver observer; + + private SpanEventSender target; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + when(config.getLogger()).thenReturn(logger); + when(channelManager.getSpanObserver()).thenReturn(observer); + target = spy(new SpanEventSender(config, queue, aggregator, channelManager)); + } + + @Test + @Timeout(5) + void run_CallsPollAndWriteUntilException() { + doNothing() + .doNothing() + .doThrow(new RuntimeException("Error!")) + .when(target).pollAndWrite(); + + target.run(); + verify(target, times(3)).pollAndWrite(); + } + + @Test + void pollAndWrite_ObserverNotReadyDoesNotPoll() { + doReturn(false).when(target).awaitReadyObserver(observer); + + target.pollAndWrite(); + + verify(target, never()).pollSafely(); + verify(target, never()).writeToObserver(ArgumentMatchers.>any(), ArgumentMatchers.any()); + } + + @Test + void pollAndWrite_NullSpanDoesNotWrite() { + doReturn(true).when(target).awaitReadyObserver(observer); + doReturn(null).when(target).pollSafely(); + + target.pollAndWrite(); + + verify(target, never()).writeToObserver(ArgumentMatchers.>any(), ArgumentMatchers.any()); + } + + @Test + void pollAndWrite_ObserverReadySpanAvailableWrites() { + SpanEvent spanEvent = buildSpanEvent(); + doReturn(true).when(target).awaitReadyObserver(observer); + doReturn(spanEvent).when(target).pollSafely(); + + target.pollAndWrite(); + + verify(target).writeToObserver(observer, convert(spanEvent)); + } + + @Test + void awaitReadyObserver_NotReadySleepsIncrementsCounter() { + long startTime = System.currentTimeMillis(); + when(observer.isReady()).thenReturn(false); + + assertFalse(target.awaitReadyObserver(observer)); + assertTrue(System.currentTimeMillis() - startTime >= 250); + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/NotReady"); + } + + @Test + void awaitReadyObserver_IsReadyReturnsTrue() { + when(observer.isReady()).thenReturn(true); + + assertTrue(target.awaitReadyObserver(observer)); + } + + @Test + void pollSafely_InterruptedThrowsException() throws InterruptedException { + doThrow(new InterruptedException()).when(queue).poll(anyLong(), ArgumentMatchers.any()); + + assertThrows(RuntimeException.class, new Executable() { + @Override + public void execute() { + target.pollSafely(); + } + }); + } + + @Test + void pollSafely_Valid() throws InterruptedException { + SpanEvent spanEvent = buildSpanEvent(); + + when(queue.poll(anyLong(), ArgumentMatchers.any())).thenReturn(spanEvent); + + assertEquals(spanEvent, target.pollSafely()); + } + + @Test + void convert_Valid() throws IOException { + SpanEvent spanEvent = buildSpanEvent(); + + V1.Span result = convert(spanEvent); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + result.writeTo(baos); + V1.Span deserialized = V1.Span.parseFrom(baos.toByteArray()); + + assertEquals("abc123", deserialized.getTraceId()); + assertEquals("abc123", deserialized.getIntrinsicsOrThrow("traceId").getStringValue()); + assertEquals("my app", deserialized.getIntrinsicsOrThrow("appName").getStringValue()); + assertEquals("value", deserialized.getIntrinsicsOrThrow("intrStr").getStringValue()); + assertEquals(12345, deserialized.getIntrinsicsOrThrow("intrInt").getIntValue()); + assertEquals(3.14, deserialized.getIntrinsicsOrThrow("intrFloat").getDoubleValue(), 0.00001); + assertTrue(deserialized.getIntrinsicsOrThrow("intrBool").getBoolValue()); + assertFalse(deserialized.containsIntrinsics("intrOther")); + } + + private SpanEvent buildSpanEvent() { + return SpanEvent.builder() + .appName("my app") + .putIntrinsic("traceId", "abc123") + .putIntrinsic("intrStr", "value") + .putIntrinsic("intrInt", 12345) + .putIntrinsic("intrFloat", 3.14) + .putIntrinsic("intrBool", true) + .putIntrinsic("intrOther", TestEnum.ONE) + .build(); + } + + private enum TestEnum { ONE } + + @Test + void writeToObserver_RethrowsException() { + doThrow(new RuntimeException("Error!")).when(observer).onNext(ArgumentMatchers.any()); + + assertThrows(RuntimeException.class, new Executable() { + @Override + public void execute() { + target.writeToObserver(observer, V1.Span.newBuilder().build()); + } + }); + verify(aggregator, never()).incrementCounter(anyString()); + } + + @Test + void writeToObserver_NoExceptionIncrementsCounter() { + target.writeToObserver(observer, V1.Span.newBuilder().build()); + + verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Span/Sent"); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/StreamObserverSupplierTest.java b/infinite-tracing/src/test/java/com/newrelic/StreamObserverSupplierTest.java deleted file mode 100644 index a15874c1f3..0000000000 --- a/infinite-tracing/src/test/java/com/newrelic/StreamObserverSupplierTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.newrelic; - -import com.newrelic.agent.interfaces.backport.Supplier; -import com.newrelic.trace.v1.V1; -import io.grpc.ManagedChannel; -import io.grpc.stub.ClientCallStreamObserver; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class StreamObserverSupplierTest { - @Mock - public Supplier channelSupplier; - @Mock - public Function> channelToStreamObserverConverter; - @Mock - public ManagedChannel managedChannel; - @Mock - public ClientCallStreamObserver streamObserver; - - @BeforeEach - public void beforeEach() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void relaysCallsThrough() { - when(channelSupplier.get()).thenReturn(managedChannel); - when(channelToStreamObserverConverter.apply(managedChannel)).thenReturn(streamObserver); - - StreamObserverSupplier target = new StreamObserverSupplier(channelSupplier, channelToStreamObserverConverter); - assertSame(streamObserver, target.get()); - verify(channelSupplier).get(); - verify(channelToStreamObserverConverter).apply(managedChannel); - } -} \ No newline at end of file diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfig.java index c724c2128b..7b87c213dd 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfig.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfig.java @@ -16,6 +16,8 @@ public interface InfiniteTracingConfig { Double getFlakyPercentage(); + Long getFlakyCode(); + boolean getUsePlaintext(); boolean isEnabled(); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfigImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfigImpl.java index 7020ab2803..62143d57f9 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfigImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/InfiniteTracingConfigImpl.java @@ -19,6 +19,7 @@ public class InfiniteTracingConfigImpl extends BaseConfig implements InfiniteTra public static final String TRACE_OBSERVER = "trace_observer"; public static final String SPAN_EVENTS = "span_events"; public static final String FLAKY_PERCENTAGE = "_flakyPercentage"; + public static final String FLAKY_CODE = "_flakyCode"; public static final String USE_PLAINTEXT = "plaintext"; public static final boolean DEFAULT_USE_PLAINTEXT = false; @@ -72,6 +73,11 @@ public Double getFlakyPercentage() { return getProperty(FLAKY_PERCENTAGE); } + @Override + public Long getFlakyCode() { + return getProperty(FLAKY_CODE); + } + @Override public boolean getUsePlaintext() { return getProperty(USE_PLAINTEXT, DEFAULT_USE_PLAINTEXT); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java index db4fb3a6eb..7971d29f89 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java @@ -309,8 +309,6 @@ protected synchronized void doStart() throws Exception { private InfiniteTracing buildInfiniteTracing(ConfigService configService) { com.newrelic.agent.config.InfiniteTracingConfig config = configService.getDefaultAgentConfig().getInfiniteTracingConfig(); - Double flakyPercentage = configService.getDefaultAgentConfig().getInfiniteTracingConfig().getFlakyPercentage(); - boolean usePlaintext = configService.getDefaultAgentConfig().getInfiniteTracingConfig().getUsePlaintext(); InfiniteTracingConfig infiniteTracingConfig = InfiniteTracingConfig.builder() .maxQueueSize(config.getSpanEventsQueueSize()) @@ -318,8 +316,9 @@ private InfiniteTracing buildInfiniteTracing(ConfigService configService) { .host(config.getTraceObserverHost()) .port(config.getTraceObserverPort()) .licenseKey(configService.getDefaultAgentConfig().getLicenseKey()) - .flakyPercentage(flakyPercentage) - .usePlaintext(usePlaintext) + .flakyPercentage(config.getFlakyPercentage()) + .flakyCode(config.getFlakyCode()) + .usePlaintext(config.getUsePlaintext()) .build(); return InfiniteTracing.initialize(infiniteTracingConfig, NewRelic.getAgent().getMetricAggregator()); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/UpdateInfiniteTracingAfterConnect.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/UpdateInfiniteTracingAfterConnect.java index 42c9e4e143..abf9f87acb 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/UpdateInfiniteTracingAfterConnect.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/UpdateInfiniteTracingAfterConnect.java @@ -29,9 +29,7 @@ public void onEstablished(String appName, String agentRunToken, Map headersData = new HashMap<>(); testClass.onEstablished(appName, runToken, headersData); - verify(infiniteTracing).setConnectionMetadata(runToken, headersData); - verify(infiniteTracing).start(); + verify(infiniteTracing).start(runToken, headersData); } @Test From 02caf84bd2e0b9c0918a7e757f0c8c0d587007e6 Mon Sep 17 00:00:00 2001 From: jack-berg <34418638+jack-berg@users.noreply.github.com> Date: Fri, 8 Jan 2021 14:00:35 -0600 Subject: [PATCH 3/3] ensure backoff policy is shared among response observers, respond to pr feedback --- .../main/java/com/newrelic/BackoffPolicy.java | 43 ++++++++++++++ .../java/com/newrelic/ChannelManager.java | 4 +- .../java/com/newrelic/ResponseObserver.java | 17 ++---- .../main/java/com/newrelic/SpanConverter.java | 56 +++++++++++++++++++ .../java/com/newrelic/SpanEventSender.java | 41 +------------- .../java/com/newrelic/BackoffPolicyTest.java | 36 ++++++++++++ .../com/newrelic/ResponseObserverTest.java | 25 +++++---- .../java/com/newrelic/SpanConverterTest.java | 49 ++++++++++++++++ .../com/newrelic/SpanEventSenderTest.java | 39 +------------ 9 files changed, 211 insertions(+), 99 deletions(-) create mode 100644 infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java create mode 100644 infinite-tracing/src/main/java/com/newrelic/SpanConverter.java create mode 100644 infinite-tracing/src/test/java/com/newrelic/BackoffPolicyTest.java create mode 100644 infinite-tracing/src/test/java/com/newrelic/SpanConverterTest.java diff --git a/infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java b/infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java new file mode 100644 index 0000000000..8695eee907 --- /dev/null +++ b/infinite-tracing/src/main/java/com/newrelic/BackoffPolicy.java @@ -0,0 +1,43 @@ +package com.newrelic; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.concurrent.atomic.AtomicInteger; + +class BackoffPolicy { + + @VisibleForTesting + static final int[] BACKOFF_SECONDS_SEQUENCE = new int[] { 15, 15, 30, 60, 120, 300 }; + private static final int DEFAULT_BACKOFF_SECONDS = 15; + + private final AtomicInteger backoffSequenceIndex = new AtomicInteger(-1); + + /** + * Get the default backoff seconds. + * + * @return the default backoff seconds + */ + int getDefaultBackoffSeconds() { + return DEFAULT_BACKOFF_SECONDS; + } + + /** + * Get the next entry in the backoff sequence. + * + * @return the next number of seconds to backoff + */ + int getNextBackoffSeconds() { + int nextIndex = backoffSequenceIndex.incrementAndGet(); + return nextIndex < BACKOFF_SECONDS_SEQUENCE.length + ? BACKOFF_SECONDS_SEQUENCE[nextIndex] + : BACKOFF_SECONDS_SEQUENCE[BACKOFF_SECONDS_SEQUENCE.length - 1]; + } + + /** + * Reset the backoff sequence. + */ + void reset() { + backoffSequenceIndex.set(-1); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java b/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java index 868de0e36f..be2260d4e0 100644 --- a/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java +++ b/infinite-tracing/src/main/java/com/newrelic/ChannelManager.java @@ -22,6 +22,7 @@ class ChannelManager { private final Logger logger; private final InfiniteTracingConfig config; private final MetricAggregator aggregator; + private final BackoffPolicy backoffManager; private final Object lock = new Object(); @GuardedBy("lock") private boolean isShutdownForever; @@ -38,6 +39,7 @@ class ChannelManager { this.aggregator = aggregator; this.agentRunToken = agentRunToken; this.requestMetadata = requestMetadata; + this.backoffManager = new BackoffPolicy(); } /** @@ -103,7 +105,7 @@ IngestServiceStub buildStub(ManagedChannel managedChannel) { @VisibleForTesting ResponseObserver buildResponseObserver() { - return new ResponseObserver(logger, this, aggregator); + return new ResponseObserver(logger, this, aggregator, backoffManager); } /** diff --git a/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java b/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java index 668c16d085..bda63788ee 100644 --- a/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java +++ b/infinite-tracing/src/main/java/com/newrelic/ResponseObserver.java @@ -7,28 +7,26 @@ import io.grpc.Status; import io.grpc.stub.StreamObserver; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; class ResponseObserver implements StreamObserver { - static final int DEFAULT_BACKOFF_SECONDS = 15; - static final int[] BACKOFF_SECONDS_SEQUENCE = new int[] { 15, 15, 30, 60, 120, 300 }; - private final Logger logger; private final ChannelManager channelManager; private final MetricAggregator aggregator; - private final AtomicInteger backoffSequenceIndex = new AtomicInteger(-1); + private final BackoffPolicy backoffManager; - ResponseObserver(Logger logger, ChannelManager channelManager, MetricAggregator aggregator) { + ResponseObserver(Logger logger, ChannelManager channelManager, MetricAggregator aggregator, BackoffPolicy backoffPolicy) { this.logger = logger; this.channelManager = channelManager; this.aggregator = aggregator; + this.backoffManager = backoffPolicy; } @Override public void onNext(V1.RecordStatus value) { aggregator.incrementCounter("Supportability/InfiniteTracing/Response"); + backoffManager.reset(); } @Override @@ -89,13 +87,10 @@ void shutdownChannelAndBackoff(Status status) { logLevel = Level.FINE; } else if (status.getCode() == Status.Code.FAILED_PRECONDITION) { // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#failed_precondition - int nextIndex = backoffSequenceIndex.incrementAndGet(); - backoffSeconds = nextIndex < BACKOFF_SECONDS_SEQUENCE.length - ? BACKOFF_SECONDS_SEQUENCE[nextIndex] - : BACKOFF_SECONDS_SEQUENCE[BACKOFF_SECONDS_SEQUENCE.length - 1]; + backoffSeconds = backoffManager.getNextBackoffSeconds(); } else { // See: https://source.datanerd.us/agents/agent-specs/blob/master/Infinite-Tracing.md#other-errors-1 - backoffSeconds = DEFAULT_BACKOFF_SECONDS; + backoffSeconds = backoffManager.getDefaultBackoffSeconds(); } logger.log(logLevel, status.asException(), "Received gRPC status {0}.", status.getCode().toString()); diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanConverter.java b/infinite-tracing/src/main/java/com/newrelic/SpanConverter.java new file mode 100644 index 0000000000..ed3dfd62fd --- /dev/null +++ b/infinite-tracing/src/main/java/com/newrelic/SpanConverter.java @@ -0,0 +1,56 @@ +package com.newrelic; + +import com.newrelic.agent.model.SpanEvent; +import com.newrelic.trace.v1.V1; + +import java.util.HashMap; +import java.util.Map; + +class SpanConverter { + + private SpanConverter() { + } + + /** + * Convert the span event the equivalent gRPC span. + * + * @param spanEvent the span event + * @return the gRPC span + */ + static V1.Span convert(SpanEvent spanEvent) { + Map intrinsicAttributes = copyAttributes(spanEvent.getIntrinsics()); + Map userAttributes = copyAttributes(spanEvent.getUserAttributesCopy()); + Map agentAttributes = copyAttributes(spanEvent.getAgentAttributes()); + + intrinsicAttributes.put("appName", V1.AttributeValue.newBuilder().setStringValue(spanEvent.getAppName()).build()); + + return V1.Span.newBuilder() + .setTraceId(spanEvent.getTraceId()) + .putAllIntrinsics(intrinsicAttributes) + .putAllAgentAttributes(agentAttributes) + .putAllUserAttributes(userAttributes) + .build(); + } + + private static Map copyAttributes(Map original) { + Map copy = new HashMap<>(); + if (original == null) { + return copy; + } + + for (Map.Entry entry : original.entrySet()) { + Object value = entry.getValue(); + if (value instanceof String) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setStringValue((String) value).build()); + } else if (value instanceof Long || value instanceof Integer) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setIntValue(((Number) value).longValue()).build()); + } else if (value instanceof Float || value instanceof Double) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setDoubleValue(((Number) value).doubleValue()).build()); + } else if (value instanceof Boolean) { + copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setBoolValue((Boolean) value).build()); + } + } + return copy; + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java b/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java index 9731819cc4..9a4128d21c 100644 --- a/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java +++ b/infinite-tracing/src/main/java/com/newrelic/SpanEventSender.java @@ -7,8 +7,6 @@ import com.newrelic.trace.v1.V1; import io.grpc.stub.ClientCallStreamObserver; -import java.util.HashMap; -import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -61,7 +59,7 @@ void pollAndWrite() { } // Convert span and write to observer - V1.Span convertedSpan = convert(span); + V1.Span convertedSpan = SpanConverter.convert(span); writeToObserver(observer, convertedSpan); } @@ -91,43 +89,6 @@ SpanEvent pollSafely() { } } - @VisibleForTesting - static V1.Span convert(SpanEvent spanEvent) { - Map intrinsicAttributes = copyAttributes(spanEvent.getIntrinsics()); - Map userAttributes = copyAttributes(spanEvent.getUserAttributesCopy()); - Map agentAttributes = copyAttributes(spanEvent.getAgentAttributes()); - - intrinsicAttributes.put("appName", V1.AttributeValue.newBuilder().setStringValue(spanEvent.getAppName()).build()); - - return V1.Span.newBuilder() - .setTraceId(spanEvent.getTraceId()) - .putAllIntrinsics(intrinsicAttributes) - .putAllAgentAttributes(agentAttributes) - .putAllUserAttributes(userAttributes) - .build(); - } - - private static Map copyAttributes(Map original) { - Map copy = new HashMap<>(); - if (original == null) { - return copy; - } - - for (Map.Entry entry : original.entrySet()) { - Object value = entry.getValue(); - if (value instanceof String) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setStringValue((String) value).build()); - } else if (value instanceof Long || value instanceof Integer) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setIntValue(((Number) value).longValue()).build()); - } else if (value instanceof Float || value instanceof Double) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setDoubleValue(((Number) value).doubleValue()).build()); - } else if (value instanceof Boolean) { - copy.put(entry.getKey(), V1.AttributeValue.newBuilder().setBoolValue((Boolean) value).build()); - } - } - return copy; - } - @VisibleForTesting void writeToObserver(ClientCallStreamObserver observer, V1.Span span) { try { diff --git a/infinite-tracing/src/test/java/com/newrelic/BackoffPolicyTest.java b/infinite-tracing/src/test/java/com/newrelic/BackoffPolicyTest.java new file mode 100644 index 0000000000..9e357c3cc8 --- /dev/null +++ b/infinite-tracing/src/test/java/com/newrelic/BackoffPolicyTest.java @@ -0,0 +1,36 @@ +package com.newrelic; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static com.newrelic.BackoffPolicy.BACKOFF_SECONDS_SEQUENCE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BackoffPolicyTest { + + private BackoffPolicy target; + + @BeforeEach + void setup() { + target = new BackoffPolicy(); + } + + @Test + void getNextBackoffSeconds_Valid() { + List responses = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + responses.add(target.getNextBackoffSeconds()); + } + + assertEquals(BACKOFF_SECONDS_SEQUENCE[0], responses.get(0)); + assertEquals(BACKOFF_SECONDS_SEQUENCE[BACKOFF_SECONDS_SEQUENCE.length - 1], responses.get(responses.size() - 1)); + + target.reset(); + + assertEquals(BACKOFF_SECONDS_SEQUENCE[0], target.getNextBackoffSeconds()); + } + +} \ No newline at end of file diff --git a/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java b/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java index 1d3423e4fa..a06d694f0e 100644 --- a/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java +++ b/infinite-tracing/src/test/java/com/newrelic/ResponseObserverTest.java @@ -13,8 +13,6 @@ import java.util.logging.Level; -import static com.newrelic.ResponseObserver.BACKOFF_SECONDS_SEQUENCE; -import static com.newrelic.ResponseObserver.DEFAULT_BACKOFF_SECONDS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -24,6 +22,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; class ResponseObserverTest { @@ -33,13 +32,15 @@ class ResponseObserverTest { private ChannelManager channelManager; @Mock private MetricAggregator aggregator; + @Mock + private BackoffPolicy backoffPolicy; private ResponseObserver target; @BeforeEach void setup() { MockitoAnnotations.initMocks(this); - target = spy(new ResponseObserver(logger, channelManager, aggregator)); + target = spy(new ResponseObserver(logger, channelManager, aggregator, backoffPolicy)); } @Test @@ -47,6 +48,7 @@ void onNext_IncrementsCounter() { target.onNext(V1.RecordStatus.newBuilder().build()); verify(aggregator).incrementCounter("Supportability/InfiniteTracing/Response"); + verify(backoffPolicy).reset(); } @Test @@ -102,21 +104,24 @@ void shutdownChannelAndBackoff_ConnectTimeoutBackoffZeroSeconds() { @Test void shutdownChannelAndBackoff_FailedPreconditionBackoffSequence() { - for (int i = 0; i < 10; i++) { - target.shutdownChannelAndBackoff(Status.FAILED_PRECONDITION); - } + int backoffSeconds = 5; + when(backoffPolicy.getNextBackoffSeconds()).thenReturn(backoffSeconds); + + target.shutdownChannelAndBackoff(Status.FAILED_PRECONDITION); - verify(logger, times(10)).log(eq(Level.WARNING), any(Throwable.class), anyString(), any()); - verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(BACKOFF_SECONDS_SEQUENCE[0]); - verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(BACKOFF_SECONDS_SEQUENCE[BACKOFF_SECONDS_SEQUENCE.length - 1]); + verify(logger).log(eq(Level.WARNING), any(Throwable.class), anyString(), any()); + verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(backoffSeconds); } @Test void shutdownChannelAndBackoff_OtherStatusDefaultBackoff() { + int backoffSeconds = 5; + when(backoffPolicy.getDefaultBackoffSeconds()).thenReturn(backoffSeconds); + target.shutdownChannelAndBackoff(Status.UNKNOWN); verify(logger).log(eq(Level.WARNING), any(Throwable.class), anyString(), any()); - verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(DEFAULT_BACKOFF_SECONDS); + verify(channelManager, atLeast(1)).shutdownChannelAndBackoff(backoffSeconds); } @Test diff --git a/infinite-tracing/src/test/java/com/newrelic/SpanConverterTest.java b/infinite-tracing/src/test/java/com/newrelic/SpanConverterTest.java new file mode 100644 index 0000000000..4d2e52aed2 --- /dev/null +++ b/infinite-tracing/src/test/java/com/newrelic/SpanConverterTest.java @@ -0,0 +1,49 @@ +package com.newrelic; + +import com.newrelic.agent.model.SpanEvent; +import com.newrelic.trace.v1.V1; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SpanConverterTest { + + @Test + void convert_Valid() throws IOException { + SpanEvent spanEvent = buildSpanEvent(); + + V1.Span result = SpanConverter.convert(spanEvent); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + result.writeTo(baos); + V1.Span deserialized = V1.Span.parseFrom(baos.toByteArray()); + + assertEquals("abc123", deserialized.getTraceId()); + assertEquals("abc123", deserialized.getIntrinsicsOrThrow("traceId").getStringValue()); + assertEquals("my app", deserialized.getIntrinsicsOrThrow("appName").getStringValue()); + assertEquals("value", deserialized.getIntrinsicsOrThrow("intrStr").getStringValue()); + assertEquals(12345, deserialized.getIntrinsicsOrThrow("intrInt").getIntValue()); + assertEquals(3.14, deserialized.getIntrinsicsOrThrow("intrFloat").getDoubleValue(), 0.00001); + assertTrue(deserialized.getIntrinsicsOrThrow("intrBool").getBoolValue()); + assertFalse(deserialized.containsIntrinsics("intrOther")); + } + + static SpanEvent buildSpanEvent() { + return SpanEvent.builder() + .appName("my app") + .putIntrinsic("traceId", "abc123") + .putIntrinsic("intrStr", "value") + .putIntrinsic("intrInt", 12345) + .putIntrinsic("intrFloat", 3.14) + .putIntrinsic("intrBool", true) + .putIntrinsic("intrOther", TestEnum.ONE) + .build(); + } + + private enum TestEnum { ONE } + +} diff --git a/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java b/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java index bca065bfa6..cfebd6df7e 100644 --- a/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java +++ b/infinite-tracing/src/test/java/com/newrelic/SpanEventSenderTest.java @@ -13,12 +13,10 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; -import static com.newrelic.SpanEventSender.convert; +import static com.newrelic.SpanConverterTest.buildSpanEvent; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -99,7 +97,7 @@ void pollAndWrite_ObserverReadySpanAvailableWrites() { target.pollAndWrite(); - verify(target).writeToObserver(observer, convert(spanEvent)); + verify(target).writeToObserver(observer, SpanConverter.convert(spanEvent)); } @Test @@ -140,39 +138,6 @@ void pollSafely_Valid() throws InterruptedException { assertEquals(spanEvent, target.pollSafely()); } - @Test - void convert_Valid() throws IOException { - SpanEvent spanEvent = buildSpanEvent(); - - V1.Span result = convert(spanEvent); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - result.writeTo(baos); - V1.Span deserialized = V1.Span.parseFrom(baos.toByteArray()); - - assertEquals("abc123", deserialized.getTraceId()); - assertEquals("abc123", deserialized.getIntrinsicsOrThrow("traceId").getStringValue()); - assertEquals("my app", deserialized.getIntrinsicsOrThrow("appName").getStringValue()); - assertEquals("value", deserialized.getIntrinsicsOrThrow("intrStr").getStringValue()); - assertEquals(12345, deserialized.getIntrinsicsOrThrow("intrInt").getIntValue()); - assertEquals(3.14, deserialized.getIntrinsicsOrThrow("intrFloat").getDoubleValue(), 0.00001); - assertTrue(deserialized.getIntrinsicsOrThrow("intrBool").getBoolValue()); - assertFalse(deserialized.containsIntrinsics("intrOther")); - } - - private SpanEvent buildSpanEvent() { - return SpanEvent.builder() - .appName("my app") - .putIntrinsic("traceId", "abc123") - .putIntrinsic("intrStr", "value") - .putIntrinsic("intrInt", 12345) - .putIntrinsic("intrFloat", 3.14) - .putIntrinsic("intrBool", true) - .putIntrinsic("intrOther", TestEnum.ONE) - .build(); - } - - private enum TestEnum { ONE } - @Test void writeToObserver_RethrowsException() { doThrow(new RuntimeException("Error!")).when(observer).onNext(ArgumentMatchers.any());