org.glowroot.agent.it.harness.impl.GrpcServerWrapper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of glowroot-agent-it-harness Show documentation
Show all versions of glowroot-agent-it-harness Show documentation
Glowroot Agent Integration Test Harness
/*
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.glowroot.agent.it.harness.impl;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import org.glowroot.agent.it.harness.shaded.com.google.common.base.Stopwatch;
import org.glowroot.agent.it.harness.shaded.com.google.common.cache.Cache;
import org.glowroot.agent.it.harness.shaded.com.google.common.cache.CacheBuilder;
import org.glowroot.agent.it.harness.shaded.com.google.common.collect.Lists;
import org.glowroot.agent.it.harness.shaded.com.google.common.collect.Maps;
import org.glowroot.agent.it.harness.shaded.com.google.common.hash.Hashing;
import org.glowroot.agent.it.harness.shaded.com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.glowroot.agent.it.harness.shaded.io.grpc.Server;
import org.glowroot.agent.it.harness.shaded.io.grpc.netty.NettyServerBuilder;
import org.glowroot.agent.it.harness.shaded.io.grpc.stub.StreamObserver;
import org.glowroot.agent.it.harness.shaded.io.netty.channel.EventLoopGroup;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.glowroot.agent.shaded.org.slf4j.Logger;
import org.glowroot.agent.shaded.org.slf4j.LoggerFactory;
import org.glowroot.common.Constants;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig;
import org.glowroot.wire.api.model.AggregateOuterClass.Aggregate;
import org.glowroot.wire.api.model.CollectorServiceGrpc.CollectorServiceImplBase;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.AggregateResponseMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.AggregateStreamMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.EmptyMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.GaugeValueMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.GaugeValueResponseMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.InitMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.InitResponse;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.LogMessage;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.TraceStreamMessage;
import org.glowroot.wire.api.model.DownstreamServiceGrpc.DownstreamServiceImplBase;
import org.glowroot.wire.api.model.DownstreamServiceOuterClass.AgentConfigUpdateRequest;
import org.glowroot.wire.api.model.DownstreamServiceOuterClass.AgentResponse;
import org.glowroot.wire.api.model.DownstreamServiceOuterClass.AgentResponse.MessageCase;
import org.glowroot.wire.api.model.DownstreamServiceOuterClass.CentralRequest;
import org.glowroot.wire.api.model.DownstreamServiceOuterClass.ReweaveRequest;
import org.glowroot.wire.api.model.ProfileOuterClass.Profile;
import org.glowroot.wire.api.model.TraceOuterClass.Trace;
import static org.glowroot.agent.it.harness.shaded.com.google.common.base.Charsets.UTF_8;
import static org.glowroot.agent.it.harness.shaded.com.google.common.base.Preconditions.checkNotNull;
import static org.glowroot.agent.it.harness.shaded.com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
class GrpcServerWrapper {
private static final Logger logger = LoggerFactory.getLogger(GrpcServerWrapper.class);
private final EventLoopGroup bossEventLoopGroup;
private final EventLoopGroup workerEventLoopGroup;
private final ExecutorService executor;
private final Server server;
private final DownstreamServiceImpl downstreamService;
private volatile @MonotonicNonNull AgentConfig agentConfig;
GrpcServerWrapper(TraceCollector collector, int port) throws IOException {
bossEventLoopGroup = EventLoopGroups.create("Glowroot-IT-Harness-GRPC-Boss-ELG");
workerEventLoopGroup = EventLoopGroups.create("Glowroot-IT-Harness-GRPC-Worker-ELG");
executor = Executors.newCachedThreadPool(
new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("Glowroot-IT-Harness-GRPC-Executor-%d")
.build());
downstreamService = new DownstreamServiceImpl();
server = NettyServerBuilder.forPort(port)
.bossEventLoopGroup(bossEventLoopGroup)
.workerEventLoopGroup(workerEventLoopGroup)
.executor(executor)
.addService(new CollectorServiceImpl(collector).bindService())
.addService(downstreamService.bindService())
.maxMessageSize(1024 * 1024 * 100)
.build()
.start();
}
AgentConfig getAgentConfig() throws InterruptedException {
Stopwatch stopwatch = Stopwatch.createStarted();
while (agentConfig == null && stopwatch.elapsed(SECONDS) < 10) {
MILLISECONDS.sleep(10);
}
if (agentConfig == null) {
throw new IllegalStateException("Timed out waiting to receive agent config");
}
return agentConfig;
}
void updateAgentConfig(AgentConfig agentConfig) throws Exception {
downstreamService.updateAgentConfig(agentConfig);
this.agentConfig = agentConfig;
}
int reweave() throws Exception {
return downstreamService.reweave();
}
void close() throws InterruptedException {
Stopwatch stopwatch = Stopwatch.createStarted();
while (stopwatch.elapsed(SECONDS) < 10 && !downstreamService.closedByAgent) {
MILLISECONDS.sleep(10);
}
checkState(downstreamService.closedByAgent);
// TODO shutdownNow() has been needed to interrupt grpc threads since grpc-java 1.7.0
server.shutdownNow();
if (!server.awaitTermination(10, SECONDS)) {
throw new IllegalStateException("Could not terminate channel");
}
// not sure why, but server needs a little extra time to shut down properly
// without this sleep, this warning is logged (but tests still pass):
// org.glowroot.agent.it.harness.shaded.io.grpc.netty.NettyServerHandler - Connection Error: RejectedExecutionException
MILLISECONDS.sleep(100);
executor.shutdown();
if (!executor.awaitTermination(10, SECONDS)) {
throw new IllegalStateException("Could not terminate executor");
}
if (!bossEventLoopGroup.shutdownGracefully(0, 0, SECONDS).await(10, SECONDS)) {
throw new IllegalStateException("Could not terminate event loop group");
}
if (!workerEventLoopGroup.shutdownGracefully(0, 0, SECONDS).await(10, SECONDS)) {
throw new IllegalStateException("Could not terminate event loop group");
}
}
private class CollectorServiceImpl extends CollectorServiceImplBase {
private final TraceCollector collector;
private final Map fullTexts = Maps.newConcurrentMap();
private CollectorServiceImpl(TraceCollector collector) {
this.collector = collector;
}
@Override
public void collectInit(InitMessage request,
StreamObserver responseObserver) {
agentConfig = request.getAgentConfig();
responseObserver.onNext(InitResponse.getDefaultInstance());
responseObserver.onCompleted();
}
@Override
public StreamObserver collectAggregateStream(
final StreamObserver responseObserver) {
return new StreamObserver() {
@Override
public void onNext(AggregateStreamMessage value) {}
@Override
public void onError(Throwable t) {
logger.error(t.getMessage(), t);
}
@Override
public void onCompleted() {
responseObserver.onNext(AggregateResponseMessage.getDefaultInstance());
responseObserver.onCompleted();
}
};
}
@Override
public void collectGaugeValues(GaugeValueMessage request,
StreamObserver responseObserver) {
responseObserver.onNext(GaugeValueResponseMessage.getDefaultInstance());
responseObserver.onCompleted();
}
@Override
public StreamObserver collectTraceStream(
final StreamObserver responseObserver) {
return new StreamObserver() {
private List sharedQueryTexts = Lists.newArrayList();
private List entries = Lists.newArrayList();
private List queries = Lists.newArrayList();
private @MonotonicNonNull Profile mainThreadProfile;
private @MonotonicNonNull Profile auxThreadProfile;
private Trace. /*@MonotonicNonNull*/ Header header;
@Override
public void onNext(TraceStreamMessage value) {
switch (value.getMessageCase()) {
case STREAM_HEADER:
break;
case SHARED_QUERY_TEXT:
sharedQueryTexts.add(Trace.SharedQueryText.newBuilder()
.setFullText(resolveFullText(value.getSharedQueryText()))
.build());
break;
case ENTRY:
entries.add(value.getEntry());
break;
case QUERIES:
queries.addAll(value.getQueries().getQueryList());
break;
case MAIN_THREAD_PROFILE:
mainThreadProfile = value.getMainThreadProfile();
break;
case AUX_THREAD_PROFILE:
auxThreadProfile = value.getAuxThreadProfile();
break;
case HEADER:
header = value.getHeader();
break;
case STREAM_COUNTS:
break;
default:
throw new RuntimeException(
"Unexpected message: " + value.getMessageCase());
}
}
@Override
public void onError(Throwable t) {
logger.error(t.getMessage(), t);
}
@Override
public void onCompleted() {
checkNotNull(header);
Trace.Builder trace = Trace.newBuilder()
.setHeader(header)
.addAllSharedQueryText(sharedQueryTexts)
.addAllEntry(entries)
.addAllQuery(queries);
if (mainThreadProfile != null) {
trace.setMainThreadProfile(mainThreadProfile);
}
if (auxThreadProfile != null) {
trace.setAuxThreadProfile(auxThreadProfile);
}
try {
collector.collectTrace(trace.build());
} catch (Throwable t) {
responseObserver.onError(t);
return;
}
responseObserver.onNext(EmptyMessage.getDefaultInstance());
responseObserver.onCompleted();
}
};
}
@Override
public void log(LogMessage request, StreamObserver responseObserver) {
try {
collector.log(request.getLogEvent());
} catch (Throwable t) {
responseObserver.onError(t);
return;
}
responseObserver.onNext(EmptyMessage.getDefaultInstance());
responseObserver.onCompleted();
}
private String resolveFullText(Trace.SharedQueryText sharedQueryText) {
String fullTextSha1 = sharedQueryText.getFullTextSha1();
if (fullTextSha1.isEmpty()) {
String fullText = sharedQueryText.getFullText();
if (fullText.length() > 2 * Constants.TRACE_QUERY_TEXT_TRUNCATE) {
fullTextSha1 = Hashing.sha1().hashString(fullText, UTF_8).toString();
fullTexts.put(fullTextSha1, fullText);
}
return fullText;
}
String fullText = fullTexts.get(fullTextSha1);
if (fullText == null) {
throw new IllegalStateException(
"Full text not found for sha1: " + fullTextSha1);
}
return fullText;
}
}
private static class DownstreamServiceImpl extends DownstreamServiceImplBase {
private final AtomicLong nextRequestId = new AtomicLong(1);
// expiration in the unlikely case that response is never returned from agent
private final Cache responseHolders = CacheBuilder.newBuilder()
.expireAfterWrite(1, HOURS)
.build();
private final StreamObserver responseObserver =
new StreamObserver() {
@Override
public void onNext(AgentResponse value) {
if (value.getMessageCase() == MessageCase.HELLO) {
return;
}
long requestId = value.getRequestId();
ResponseHolder responseHolder = responseHolders.getIfPresent(requestId);
responseHolders.invalidate(requestId);
if (responseHolder == null) {
logger.error("no response holder for request id: {}", requestId);
return;
}
try {
// this shouldn't timeout since it is the other side of the exchange
// that is waiting
responseHolder.response.exchange(value, 1, MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error(e.getMessage(), e);
} catch (TimeoutException e) {
logger.error(e.getMessage(), e);
}
}
@Override
public void onError(Throwable t) {
logger.error(t.getMessage(), t);
}
@Override
public void onCompleted() {
checkNotNull(requestObserver).onCompleted();
closedByAgent = true;
}
};
private volatile @MonotonicNonNull StreamObserver requestObserver;
private volatile boolean closedByAgent;
@Override
public StreamObserver connect(
StreamObserver requestObserver) {
this.requestObserver = requestObserver;
return responseObserver;
}
private void updateAgentConfig(AgentConfig agentConfig) throws Exception {
sendRequest(CentralRequest.newBuilder()
.setRequestId(nextRequestId.getAndIncrement())
.setAgentConfigUpdateRequest(AgentConfigUpdateRequest.newBuilder()
.setAgentConfig(agentConfig))
.build());
}
private int reweave() throws Exception {
AgentResponse response = sendRequest(CentralRequest.newBuilder()
.setRequestId(nextRequestId.getAndIncrement())
.setReweaveRequest(ReweaveRequest.getDefaultInstance())
.build());
return response.getReweaveResponse().getClassUpdateCount();
}
private AgentResponse sendRequest(CentralRequest request) throws Exception {
ResponseHolder responseHolder = new ResponseHolder();
responseHolders.put(request.getRequestId(), responseHolder);
while (requestObserver == null) {
MILLISECONDS.sleep(10);
}
requestObserver.onNext(request);
// timeout is in case agent never responds
// passing AgentResponse.getDefaultInstance() is just dummy (non-null) value
AgentResponse response = responseHolder.response
.exchange(AgentResponse.getDefaultInstance(), 1, MINUTES);
if (response.getMessageCase() == MessageCase.UNKNOWN_REQUEST_RESPONSE) {
throw new IllegalStateException();
}
if (response.getMessageCase() == MessageCase.EXCEPTION_RESPONSE) {
throw new IllegalStateException();
}
return response;
}
}
private static class ResponseHolder {
private final Exchanger response = new Exchanger();
}
}