nl.topicus.jdbc.shaded.io.grpc.internal.ServerImpl Maven / Gradle / Ivy
Show all versions of spanner-jdbc Show documentation
/*
* Copyright 2014, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package nl.topicus.jdbc.shaded.io.grpc.internal;
import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Preconditions.checkState;
import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.util.concurrent.MoreExecutors.directExecutor;
import static nl.topicus.jdbc.shaded.io.grpc.Contexts.statusFromCancelled;
import static nl.topicus.jdbc.shaded.io.grpc.Status.DEADLINE_EXCEEDED;
import static nl.topicus.jdbc.shaded.io.grpc.internal.GrpcUtil.TIMEOUT_KEY;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.annotations.VisibleForTesting;
import nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Preconditions;
import nl.topicus.jdbc.shaded.io.grpc.Attributes;
import nl.topicus.jdbc.shaded.io.grpc.CompressorRegistry;
import nl.topicus.jdbc.shaded.io.grpc.Context;
import nl.topicus.jdbc.shaded.io.grpc.DecompressorRegistry;
import nl.topicus.jdbc.shaded.io.grpc.HandlerRegistry;
import nl.topicus.jdbc.shaded.io.grpc.Metadata;
import nl.topicus.jdbc.shaded.io.grpc.ServerCall;
import nl.topicus.jdbc.shaded.io.grpc.ServerMethodDefinition;
import nl.topicus.jdbc.shaded.io.grpc.ServerServiceDefinition;
import nl.topicus.jdbc.shaded.io.grpc.ServerTransportFilter;
import nl.topicus.jdbc.shaded.io.grpc.Status;
import java.nl.topicus.jdbc.shaded.io.IOException;
import java.nl.topicus.jdbc.shaded.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import nl.topicus.jdbc.shaded.javax.annotation.concurrent.GuardedBy;
/**
* Default implementation of {@link nl.topicus.jdbc.shaded.io.grpc.Server}, for creation by transports.
*
* Expected usage (by a theoretical TCP transport):
*
public class TcpTransportServerFactory {
* public static Server newServer(Executor executor, HandlerRegistry registry,
* String configuration) {
* return new ServerImpl(executor, registry, new TcpTransportServer(configuration));
* }
* }
*
* Starting the server starts the underlying transport for servicing requests. Stopping the
* server stops servicing new requests and waits for all connections to terminate.
*/
public final class ServerImpl extends nl.topicus.jdbc.shaded.io.grpc.Server implements WithLogId {
private static final ServerStreamListener NOOP_LISTENER = new NoopListener();
private final LogId logId = LogId.allocate(getClass().getName());
private final ObjectPool extends Executor> executorPool;
/** Executor for application processing. Safe to read after {@link #start()}. */
private Executor executor;
private final InternalHandlerRegistry registry;
private final HandlerRegistry fallbackRegistry;
private final List transportFilters;
@GuardedBy("lock") private boolean started;
@GuardedBy("lock") private boolean shutdown;
/** non-{@code null} if immediate shutdown has been requested. */
@GuardedBy("lock") private Status shutdownNowStatus;
/** {@code true} if ServerListenerImpl.serverShutdown() was called. */
@GuardedBy("lock") private boolean serverShutdownCallbackInvoked;
@GuardedBy("lock") private boolean terminated;
/** Service encapsulating something similar to an accept() socket. */
private final InternalServer transportServer;
private final Object lock = new Object();
@GuardedBy("lock") private boolean transportServerTerminated;
/** {@code transportServer} and services encapsulating something similar to a TCP connection. */
@GuardedBy("lock") private final Collection transports =
new HashSet();
private final ObjectPool timeoutServicePool;
private ScheduledExecutorService timeoutService;
private final Context rootContext;
private final DecompressorRegistry decompressorRegistry;
private final CompressorRegistry nl.topicus.jdbc.shaded.com.ressorRegistry;
/**
* Construct a server.
*
* @param executorPool provides an executor to call methods on behalf of remote clients
* @param registry the primary method registry
* @param fallbackRegistry the secondary method registry, used only if the primary registry
* doesn't have the method
*/
ServerImpl(ObjectPool extends Executor> executorPool,
ObjectPool timeoutServicePool,
InternalHandlerRegistry registry, HandlerRegistry fallbackRegistry,
InternalServer transportServer, Context rootContext,
DecompressorRegistry decompressorRegistry, CompressorRegistry nl.topicus.jdbc.shaded.com.ressorRegistry,
List transportFilters) {
this.executorPool = Preconditions.checkNotNull(executorPool, "executorPool");
this.timeoutServicePool = Preconditions.checkNotNull(timeoutServicePool, "timeoutServicePool");
this.registry = Preconditions.checkNotNull(registry, "registry");
this.fallbackRegistry = Preconditions.checkNotNull(fallbackRegistry, "fallbackRegistry");
this.transportServer = Preconditions.checkNotNull(transportServer, "transportServer");
// Fork from the passed in context so that it does not propagate cancellation, it only
// inherits values.
this.rootContext = Preconditions.checkNotNull(rootContext, "rootContext").fork();
this.decompressorRegistry = decompressorRegistry;
this.nl.topicus.jdbc.shaded.com.ressorRegistry = nl.topicus.jdbc.shaded.com.ressorRegistry;
this.transportFilters = Collections.unmodifiableList(
new ArrayList(transportFilters));
}
/**
* Bind and start the server.
*
* @return {@code this} object
* @throws IllegalStateException if already started
* @throws IOException if unable to bind
*/
@Override
public ServerImpl start() throws IOException {
synchronized (lock) {
checkState(!started, "Already started");
checkState(!shutdown, "Shutting down");
// Start and wait for any port to actually be bound.
transportServer.start(new ServerListenerImpl());
timeoutService = Preconditions.checkNotNull(timeoutServicePool.getObject(), "timeoutService");
executor = Preconditions.checkNotNull(executorPool.getObject(), "executor");
started = true;
return this;
}
}
@Override
public int getPort() {
synchronized (lock) {
checkState(started, "Not started");
checkState(!terminated, "Already terminated");
return transportServer.getPort();
}
}
@Override
public List getServices() {
List fallbackServices = fallbackRegistry.getServices();
if (fallbackServices.isEmpty()) {
return registry.getServices();
} else {
List registryServices = registry.getServices();
int servicesCount = registryServices.size() + fallbackServices.size();
List services =
new ArrayList(servicesCount);
services.addAll(registryServices);
services.addAll(fallbackServices);
return Collections.unmodifiableList(services);
}
}
@Override
public List getImmutableServices() {
return registry.getServices();
}
@Override
public List getMutableServices() {
return Collections.unmodifiableList(fallbackRegistry.getServices());
}
/**
* Initiates an orderly shutdown in which preexisting calls continue but new calls are rejected.
*/
@Override
public ServerImpl shutdown() {
boolean shutdownTransportServer;
synchronized (lock) {
if (shutdown) {
return this;
}
shutdown = true;
shutdownTransportServer = started;
if (!shutdownTransportServer) {
transportServerTerminated = true;
checkForTermination();
}
}
if (shutdownTransportServer) {
transportServer.shutdown();
}
return this;
}
@Override
public ServerImpl shutdownNow() {
shutdown();
Collection transportsCopy;
Status nowStatus = Status.UNAVAILABLE.withDescription("Server shutdownNow invoked");
boolean savedServerShutdownCallbackInvoked;
synchronized (lock) {
// Short-circuiting not strictly necessary, but prevents transports from needing to handle
// multiple shutdownNow invocations if shutdownNow is called multiple times.
if (shutdownNowStatus != null) {
return this;
}
shutdownNowStatus = nowStatus;
transportsCopy = new ArrayList(transports);
savedServerShutdownCallbackInvoked = serverShutdownCallbackInvoked;
}
// Short-circuiting not strictly necessary, but prevents transports from needing to handle
// multiple shutdownNow invocations, between here and the serverShutdown callback.
if (savedServerShutdownCallbackInvoked) {
// Have to call shutdownNow, because serverShutdown callback only called shutdown, not
// shutdownNow
for (ServerTransport transport : transportsCopy) {
transport.shutdownNow(nowStatus);
}
}
return this;
}
@Override
public boolean isShutdown() {
synchronized (lock) {
return shutdown;
}
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
synchronized (lock) {
long timeoutNanos = unit.toNanos(timeout);
long endTimeNanos = System.nanoTime() + timeoutNanos;
while (!terminated && (timeoutNanos = endTimeNanos - System.nanoTime()) > 0) {
NANOSECONDS.timedWait(lock, timeoutNanos);
}
return terminated;
}
}
@Override
public void awaitTermination() throws InterruptedException {
synchronized (lock) {
while (!terminated) {
lock.wait();
}
}
}
@Override
public boolean isTerminated() {
synchronized (lock) {
return terminated;
}
}
/**
* Remove transport service from accounting collection and notify of nl.topicus.jdbc.shaded.com.lete shutdown if
* necessary.
*
* @param transport service to remove
*/
private void transportClosed(ServerTransport transport) {
synchronized (lock) {
if (!transports.remove(transport)) {
throw new AssertionError("Transport already removed");
}
checkForTermination();
}
}
/** Notify of nl.topicus.jdbc.shaded.com.lete shutdown if necessary. */
private void checkForTermination() {
synchronized (lock) {
if (shutdown && transports.isEmpty() && transportServerTerminated) {
if (terminated) {
throw new AssertionError("Server already terminated");
}
terminated = true;
if (timeoutService != null) {
timeoutService = timeoutServicePool.returnObject(timeoutService);
}
if (executor != null) {
executor = executorPool.returnObject(executor);
}
// TODO(carl-mastrangelo): move this outside the synchronized block.
lock.notifyAll();
}
}
}
private class ServerListenerImpl implements ServerListener {
@Override
public ServerTransportListener transportCreated(ServerTransport transport) {
synchronized (lock) {
transports.add(transport);
}
return new ServerTransportListenerImpl(transport);
}
@Override
public void serverShutdown() {
ArrayList copiedTransports;
Status shutdownNowStatusCopy;
synchronized (lock) {
// transports collection can be modified during shutdown(), even if we hold the lock, due
// to reentrancy.
copiedTransports = new ArrayList(transports);
shutdownNowStatusCopy = shutdownNowStatus;
serverShutdownCallbackInvoked = true;
}
for (ServerTransport transport : copiedTransports) {
if (shutdownNowStatusCopy == null) {
transport.shutdown();
} else {
transport.shutdownNow(shutdownNowStatusCopy);
}
}
synchronized (lock) {
transportServerTerminated = true;
checkForTermination();
}
}
}
private class ServerTransportListenerImpl implements ServerTransportListener {
private final ServerTransport transport;
private Attributes attributes;
public ServerTransportListenerImpl(ServerTransport transport) {
this.transport = transport;
}
@Override
public Attributes transportReady(Attributes attributes) {
for (ServerTransportFilter filter : transportFilters) {
attributes = Preconditions.checkNotNull(filter.transportReady(attributes),
"Filter %s returned null", filter);
}
this.attributes = attributes;
return attributes;
}
@Override
public void transportTerminated() {
for (ServerTransportFilter filter : transportFilters) {
filter.transportTerminated(attributes);
}
transportClosed(transport);
}
@Override
public void streamCreated(
final ServerStream stream, final String methodName, final Metadata headers) {
final StatsTraceContext statsTraceCtx = Preconditions.checkNotNull(
stream.statsTraceContext(), "statsTraceCtx not present from stream");
final Context.CancellableContext context = createContext(stream, headers, statsTraceCtx);
final Executor wrappedExecutor;
// This is a performance optimization that avoids the synchronization and queuing overhead
// that nl.topicus.jdbc.shaded.com.s with SerializingExecutor.
if (executor == directExecutor()) {
wrappedExecutor = new SerializeReentrantCallsDirectExecutor();
} else {
wrappedExecutor = new SerializingExecutor(executor);
}
final JumpToApplicationThreadServerStreamListener jumpListener
= new JumpToApplicationThreadServerStreamListener(wrappedExecutor, stream, context);
stream.setListener(jumpListener);
// Run in wrappedExecutor so jumpListener.setListener() is called before any callbacks
// are delivered, including any errors. Callbacks can still be triggered, but they will be
// queued.
wrappedExecutor.execute(new ContextRunnable(context) {
@Override
public void runInContext() {
ServerStreamListener listener = NOOP_LISTENER;
try {
ServerMethodDefinition, ?> method = registry.lookupMethod(methodName);
if (method == null) {
method = fallbackRegistry.lookupMethod(methodName, stream.getAuthority());
}
if (method == null) {
Status status = Status.UNIMPLEMENTED.withDescription(
"Method not found: " + methodName);
// TODO(zhangkun83): this error may be recorded by the tracer, and if it's kept in
// memory as a map whose key is the method name, this would allow a misbehaving
// client to blow up the server in-memory stats storage by sending large number of
// distinct unimplemented method
// names. (https://github.nl.topicus.jdbc.shaded.com.grpc/grpc-java/issues/2285)
stream.close(status, new Metadata());
context.cancel(null);
return;
}
listener = startCall(stream, methodName, method, headers, context, statsTraceCtx);
} catch (RuntimeException e) {
stream.close(Status.fromThrowable(e), new Metadata());
context.cancel(null);
throw e;
} catch (Error e) {
stream.close(Status.fromThrowable(e), new Metadata());
context.cancel(null);
throw e;
} finally {
jumpListener.setListener(listener);
}
}
});
}
private Context.CancellableContext createContext(
final ServerStream stream, Metadata headers, StatsTraceContext statsTraceCtx) {
Long timeoutNanos = headers.get(TIMEOUT_KEY);
Context baseContext = statsTraceCtx.serverFilterContext(rootContext);
if (timeoutNanos == null) {
return baseContext.withCancellation();
}
Context.CancellableContext context =
baseContext.withDeadlineAfter(timeoutNanos, NANOSECONDS, timeoutService);
context.addListener(new Context.CancellationListener() {
@Override
public void cancelled(Context context) {
Status status = statusFromCancelled(context);
if (DEADLINE_EXCEEDED.getCode().equals(status.getCode())) {
// This should rarely get run, since the client will likely cancel the stream before
// the timeout is reached.
stream.cancel(status);
}
}
}, directExecutor());
return context;
}
/** Never returns {@code null}. */
private ServerStreamListener startCall(ServerStream stream, String fullMethodName,
ServerMethodDefinition methodDef, Metadata headers,
Context.CancellableContext context, StatsTraceContext statsTraceCtx) {
// TODO(ejona86): should we update fullMethodName to have the canonical path of the method?
ServerCallImpl call = new ServerCallImpl(
stream, methodDef.getMethodDescriptor(), headers, context,
decompressorRegistry, nl.topicus.jdbc.shaded.com.ressorRegistry);
statsTraceCtx.serverCallStarted(call);
ServerCall.Listener listener =
methodDef.getServerCallHandler().startCall(call, headers);
if (listener == null) {
throw new NullPointerException(
"startCall() returned a null listener for method " + fullMethodName);
}
return call.newServerStreamListener(listener);
}
}
@Override
public LogId getLogId() {
return logId;
}
private static class NoopListener implements ServerStreamListener {
@Override
public void messageRead(InputStream value) {
try {
value.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void halfClosed() {}
@Override
public void closed(Status status) {}
@Override
public void onReady() {}
}
/**
* Dispatches callbacks onto an application-provided executor and correctly propagates
* exceptions.
*/
@VisibleForTesting
static class JumpToApplicationThreadServerStreamListener implements ServerStreamListener {
private final Executor callExecutor;
private final Context.CancellableContext context;
private final ServerStream stream;
// Only accessed from callExecutor.
private ServerStreamListener listener;
public JumpToApplicationThreadServerStreamListener(Executor executor,
ServerStream stream, Context.CancellableContext context) {
this.callExecutor = executor;
this.stream = stream;
this.context = context;
}
private ServerStreamListener getListener() {
if (listener == null) {
throw new IllegalStateException("listener unset");
}
return listener;
}
@VisibleForTesting
void setListener(ServerStreamListener listener) {
Preconditions.checkNotNull(listener, "listener must not be null");
Preconditions.checkState(this.listener == null, "Listener already set");
this.listener = listener;
}
/**
* Like {@link ServerCall#close(Status, Metadata)}, but thread-safe for internal use.
*/
private void internalClose(Status status, Metadata trailers) {
// TODO(ejona86): this is not thread-safe :)
stream.close(status, trailers);
}
@Override
public void messageRead(final InputStream message) {
callExecutor.execute(new ContextRunnable(context) {
@Override
public void runInContext() {
try {
getListener().messageRead(message);
} catch (RuntimeException e) {
internalClose(Status.fromThrowable(e), new Metadata());
throw e;
} catch (Error e) {
internalClose(Status.fromThrowable(e), new Metadata());
throw e;
}
}
});
}
@Override
public void halfClosed() {
callExecutor.execute(new ContextRunnable(context) {
@Override
public void runInContext() {
try {
getListener().halfClosed();
} catch (RuntimeException e) {
internalClose(Status.fromThrowable(e), new Metadata());
throw e;
} catch (Error e) {
internalClose(Status.fromThrowable(e), new Metadata());
throw e;
}
}
});
}
@Override
public void closed(final Status status) {
callExecutor.execute(new ContextRunnable(context) {
@Override
public void runInContext() {
try {
getListener().closed(status);
} finally {
// Regardless of the status code we cancel the context so that listeners
// are aware that the call is done.
context.cancel(status.getCause());
}
}
});
}
@Override
public void onReady() {
callExecutor.execute(new ContextRunnable(context) {
@Override
public void runInContext() {
try {
getListener().onReady();
} catch (RuntimeException e) {
internalClose(Status.fromThrowable(e), new Metadata());
throw e;
} catch (Error e) {
internalClose(Status.fromThrowable(e), new Metadata());
throw e;
}
}
});
}
}
}