/* * Copyright © 2015 Cask Data, Inc. * * 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 co.cask.cdap.internal.app.runtime.service.http; import co.cask.cdap.api.Transactional; import co.cask.cdap.api.TxRunnable; import co.cask.cdap.api.data.DatasetContext; import co.cask.cdap.api.service.http.HttpContentConsumer; import co.cask.cdap.api.service.http.HttpContentProducer; import co.cask.cdap.api.service.http.HttpServiceResponder; import co.cask.cdap.common.lang.ClassLoaders; import co.cask.http.BodyConsumer; import co.cask.http.BodyProducer; import co.cask.http.HttpResponder; import com.google.common.collect.Multimap; import org.apache.twill.common.Cancellable; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import javax.annotation.Nullable; /** * An adapter class to delegate calls from {@link BodyConsumer} to {@link HttpContentConsumer}. */ final class BodyConsumerAdapter extends BodyConsumer { private final DelayedHttpServiceResponder responder; private final HttpContentConsumer delegate; private final Transactional transactional; private final ClassLoader programContextClassLoader; private final Cancellable contextReleaser; private boolean completed; /** * Constructs a new instance. * * @param responder the responder used for sending response back to client * @param delegate the {@link HttpContentConsumer} to delegate calls to * @param transactional a {@link Transactional} for executing transactional task * @param programContextClassLoader the context ClassLoader to use to execute user code * @param contextReleaser A {@link Cancellable} for returning the context back to the http server */ BodyConsumerAdapter(DelayedHttpServiceResponder responder, HttpContentConsumer delegate, Transactional transactional, ClassLoader programContextClassLoader, Cancellable contextReleaser) { this.responder = responder; this.delegate = delegate; this.transactional = transactional; this.programContextClassLoader = programContextClassLoader; this.contextReleaser = contextReleaser; } @Override public void chunk(final ChannelBuffer request, HttpResponder responder) { // Due to async nature of netty, chunk might get called even we try to close the connection in onError. if (completed) { return; } try { final ClassLoader oldClassLoader = ClassLoaders.setContextClassLoader(programContextClassLoader); try { delegate.onReceived(request.toByteBuffer(), transactional); } finally { ClassLoaders.setContextClassLoader(oldClassLoader); } } catch (Throwable t) { onError(t, this.responder); } } @Override public void finished(HttpResponder responder) { try { transactional.execute(new TxRunnable() { @Override public void run(DatasetContext context) throws Exception { delegate.onFinish(BodyConsumerAdapter.this.responder); } }); } catch (Throwable t) { onError(t, this.responder); return; } // To the HttpContentConsumer, the call is completed even if it fails to send response back to client. completed = true; try { BodyConsumerAdapter.this.responder.execute(); } finally { if (!this.responder.hasContentProducer()) { contextReleaser.cancel(); } } } @Override public void handleError(final Throwable cause) { // When this method is called from netty-http, the response has already been sent, hence uses a no-op // DelayedHttpServiceResponder for the onError call. onError(cause, new DelayedHttpServiceResponder(responder, new ErrorBodyProducerFactory()) { @Override protected void doSend(int status, String contentType, @Nullable ChannelBuffer content, @Nullable HttpContentProducer contentProducer, @Nullable Multimap<String, String> headers) { // no-op } @Override public void setTransactionFailureResponse(Throwable t) { // no-op } @Override public void execute(boolean keepAlive) { // no-op } @Override public boolean hasContentProducer() { // Always release the context at the end since it's not possible to send with a content producer return false; } }); } /** * Calls the {@link HttpContentConsumer#onError(HttpServiceResponder, Throwable)} method from a transaction. */ private void onError(final Throwable cause, final DelayedHttpServiceResponder responder) { if (completed) { return; } // To the HttpContentConsumer, once onError is called, no other methods will be triggered completed = true; try { transactional.execute(new TxRunnable() { @Override public void run(DatasetContext context) throws Exception { delegate.onError(responder, cause); } }); } catch (Throwable t) { responder.setTransactionFailureResponse(t); } finally { try { responder.execute(false); } finally { if (!responder.hasContentProducer()) { contextReleaser.cancel(); } } } } /** * A {@link BodyProducerFactory} to be used when {@link #handleError(Throwable)} is called. */ private static final class ErrorBodyProducerFactory implements BodyProducerFactory { @Override public BodyProducer create(HttpContentProducer contentProducer, TransactionalHttpServiceContext serviceContext) { // It doesn't matter what it returns as it'll never get used // Returning a body producer that gives empty content return new BodyProducer() { @Override public ChannelBuffer nextChunk() throws Exception { return ChannelBuffers.EMPTY_BUFFER; } @Override public void finished() throws Exception { // no-op } @Override public void handleError(@Nullable Throwable throwable) { // no-op } }; } } }