Fix race condition with releaseUploadChannel(). A race condition can occur in `HttpBlobStore.releaseUploadChannel()` when `ChannelHandlerContext` is closed by `HttpUploadHandler.channelRead0`. Because `ChannelHandlerContext.close` runs asynchronously and closes all pipelines in reverse order (before the channel is marked as closed), `HttpBlobStore.releaseUploadChannel()` can be executed while channel is still open and handlers are being removed, causing an exception to be thrown by the `ch.pipeline().remove(X)` calls. This fix saves the status of the keepAlive response from the `HttpUploadHandler` so that the channel can be fully closed before releasing. Resolves #5952 Closes #5953. PiperOrigin-RevId: 211437717
diff --git a/src/main/java/com/google/devtools/build/lib/remote/blobstore/http/HttpBlobStore.java b/src/main/java/com/google/devtools/build/lib/remote/blobstore/http/HttpBlobStore.java index ca9a8eb..a1ec3b2 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/blobstore/http/HttpBlobStore.java +++ b/src/main/java/com/google/devtools/build/lib/remote/blobstore/http/HttpBlobStore.java
@@ -65,6 +65,7 @@ import java.net.SocketAddress; import java.net.URI; import java.util.List; +import java.util.NoSuchElementException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -99,7 +100,6 @@ * <p>The implementation currently does not support transfer encoding chunked. */ public final class HttpBlobStore implements SimpleBlobStore { - private static final Pattern INVALID_TOKEN_ERROR = Pattern.compile("\\s*error\\s*=\\s*\"?invalid_token\"?"); @@ -107,6 +107,7 @@ private final ChannelPool channelPool; private final URI uri; private final int timeoutMillis; + private final boolean useTls; private final Object closeLock = new Object(); @@ -159,7 +160,7 @@ URI uri, int timeoutMillis, int remoteMaxConnections, @Nullable final Credentials creds, @Nullable SocketAddress socketAddress) throws Exception { - boolean useTls = uri.getScheme().equals("https"); + useTls = uri.getScheme().equals("https"); if (uri.getPort() == -1) { int port = useTls ? 443 : 80; uri = @@ -237,6 +238,13 @@ try { Channel ch = channelAcquired.getNow(); ChannelPipeline p = ch.pipeline(); + + if (!isChannelPipelineEmpty(p)) { + channelReady.setFailure( + new IllegalStateException("Channel pipeline is not empty.")); + return; + } + p.addLast(new HttpResponseDecoder()); // The 10KiB limit was chosen at random. We only expect HTTP servers to respond with // an error message in the body and that should always be less than 10KiB. @@ -264,11 +272,18 @@ @SuppressWarnings("FutureReturnValueIgnored") private void releaseUploadChannel(Channel ch) { if (ch.isOpen()) { - ch.pipeline().remove(HttpResponseDecoder.class); - ch.pipeline().remove(HttpObjectAggregator.class); - ch.pipeline().remove(HttpRequestEncoder.class); - ch.pipeline().remove(ChunkedWriteHandler.class); - ch.pipeline().remove(HttpUploadHandler.class); + try { + ch.pipeline().remove(HttpResponseDecoder.class); + ch.pipeline().remove(HttpObjectAggregator.class); + ch.pipeline().remove(HttpRequestEncoder.class); + ch.pipeline().remove(ChunkedWriteHandler.class); + ch.pipeline().remove(HttpUploadHandler.class); + } catch (NoSuchElementException e) { + // If the channel is in the process of closing but not yet closed, some handlers could have + // been removed and would cause NoSuchElement exceptions to be thrown. Because handlers are + // removed in reverse-order, if we get a NoSuchElement exception, the following handlers + // should have been removed. + } } channelPool.release(ch); } @@ -288,6 +303,13 @@ try { Channel ch = channelAcquired.getNow(); ChannelPipeline p = ch.pipeline(); + + if (!isChannelPipelineEmpty(p)) { + channelReady.setFailure( + new IllegalStateException("Channel pipeline is not empty.")); + return; + } + ch.pipeline() .addFirst("read-timeout-handler", new ReadTimeoutHandler(timeoutMillis)); p.addLast(new HttpClientCodec()); @@ -309,13 +331,27 @@ if (ch.isOpen()) { // The channel might have been closed due to an error, in which case its pipeline // has already been cleared. Closed channels can't be reused. - ch.pipeline().remove(ReadTimeoutHandler.class); - ch.pipeline().remove(HttpClientCodec.class); - ch.pipeline().remove(HttpDownloadHandler.class); + try { + ch.pipeline().remove(ReadTimeoutHandler.class); + ch.pipeline().remove(HttpClientCodec.class); + ch.pipeline().remove(HttpDownloadHandler.class); + } catch (NoSuchElementException e) { + // If the channel is in the process of closing but not yet closed, some handlers could have + // been removed and would cause NoSuchElement exceptions to be thrown. Because handlers are + // removed in reverse-order, if we get a NoSuchElement exception, the following handlers + // should have been removed. + } } channelPool.release(ch); } + private boolean isChannelPipelineEmpty(ChannelPipeline pipeline) { + return (pipeline.first() == null) + || (useTls + && "ssl-handler".equals(pipeline.firstContext().name()) + && pipeline.first() == pipeline.last()); + } + @Override public boolean containsKey(String key) { throw new UnsupportedOperationException("HTTP Caching does not use this method.");