All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.http.netty.reactive.HandlerPublisher Maven / Gradle / Ivy

There is a newer version: 4.5.3
Show newest version
/*
 * Copyright 2017-2020 original 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
 *
 * https://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 io.micronaut.http.netty.reactive;

import io.micronaut.core.annotation.Internal;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.EventExecutor;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;

import static io.micronaut.http.netty.reactive.HandlerPublisher.State.BUFFERING;
import static io.micronaut.http.netty.reactive.HandlerPublisher.State.DEMANDING;
import static io.micronaut.http.netty.reactive.HandlerPublisher.State.DONE;
import static io.micronaut.http.netty.reactive.HandlerPublisher.State.DRAINING;
import static io.micronaut.http.netty.reactive.HandlerPublisher.State.IDLE;
import static io.micronaut.http.netty.reactive.HandlerPublisher.State.NO_CONTEXT;
import static io.micronaut.http.netty.reactive.HandlerPublisher.State.NO_SUBSCRIBER;
import static io.micronaut.http.netty.reactive.HandlerPublisher.State.NO_SUBSCRIBER_ERROR;
import static io.micronaut.http.netty.reactive.HandlerPublisher.State.NO_SUBSCRIBER_OR_CONTEXT;

/**
 * Publisher for a Netty Handler.
 * 

* This publisher supports only one subscriber. *

* All interactions with the subscriber are done from the handlers executor, hence, they provide the same happens before * semantics that Netty provides. *

* The handler publishes all messages that match the type as specified by the passed in class. Any non matching messages * are forwarded to the next handler. *

* The publisher will signal complete if it receives a channel inactive event. *

* The publisher will release any messages that it drops (for example, messages that are buffered when the subscriber * cancels), but other than that, it does not release any messages. It is up to the subscriber to release messages. *

* If the subscriber cancels, the publisher will send a close event up the channel pipeline. *

* All errors will short circuit the buffer, and cause publisher to immediately call the subscribers onError method, * dropping the buffer. *

* The publisher can be subscribed to or placed in a handler chain in any order. * * @param The publisher type * @author Graeme Rocher * @since 1.0 */ @Internal public abstract class HandlerPublisher extends ChannelDuplexHandler implements HotObservable { private static final Logger LOG = LoggerFactory.getLogger(HandlerPublisher.class); /** * Used for buffering a completion signal. */ private static final Object COMPLETE = new Object() { @Override public String toString() { return "COMPLETE"; } }; private final AtomicBoolean completed = new AtomicBoolean(false); private final EventExecutor executor; private final Queue buffer = new LinkedList<>(); /** * Whether a subscriber has been provided. This is used to detect whether two subscribers are subscribing * simultaneously. */ private final AtomicBoolean hasSubscriber = new AtomicBoolean(); private State state = NO_SUBSCRIBER_OR_CONTEXT; private volatile Subscriber subscriber; private ChannelHandlerContext ctx; private volatile long outstandingDemand = 0; private Throwable noSubscriberError; private boolean seenLast; /** * Create a handler publisher. *

* The supplied executor must be the same event loop as the event loop that this handler is eventually registered * with, if not, an exception will be thrown when the handler is registered. * * @param executor The executor to execute asynchronous events from the subscriber on. */ public HandlerPublisher(EventExecutor executor) { this.executor = executor; } @Override public void subscribe(final Subscriber subscriber) { if (subscriber == null) { throw new NullPointerException("Null subscriber"); } if (!hasSubscriber.compareAndSet(false, true)) { // Must call onSubscribe first. subscriber.onSubscribe(new Subscription() { @Override public void request(long n) { // no-op subscription } @Override public void cancel() { // no-op subscription } }); subscriber.onError(new IllegalStateException("This publisher only supports one subscriber")); } else { executor.execute(() -> provideSubscriber(subscriber)); } } /** * Returns {@code true} if the given message should be handled. If {@code false} it will be passed to the next * {@link io.netty.channel.ChannelInboundHandler} in the {@link io.netty.channel.ChannelPipeline}. * * @param msg The message to check. * @return True if the message should be accepted. */ protected abstract boolean acceptInboundMessage(Object msg); /** * Override to handle when a subscriber cancels the subscription. *

* By default, this method will simply close the channel. */ protected void cancelled() { ctx.close(); } /** * Override to intercept when demand is requested. *

* By default, a channel read is invoked. */ protected void requestDemand() { if (LOG.isTraceEnabled()) { LOG.trace("Demand received for next message (state = " + state + "). Calling context.read()"); } // prevent requesting demand beyond LastHttpContent if (!seenLast) { ctx.read(); } } /** * The state. */ enum State { /** * Initial state. There's no subscriber, and no context. */ NO_SUBSCRIBER_OR_CONTEXT, /** * A subscriber has been provided, but no context has been provided. */ NO_CONTEXT, /** * A context has been provided, but no subscriber has been provided. */ NO_SUBSCRIBER, /** * An error has been received, but there's no subscriber to receive it. */ NO_SUBSCRIBER_ERROR, /** * There is no demand, and we have nothing buffered. */ IDLE, /** * There is no demand, and we're buffering elements. */ BUFFERING, /** * We have nothing buffered, but there is demand. */ DEMANDING, /** * The stream is complete, however there are still elements buffered for which no demand has come from the subscriber. */ DRAINING, /** * We're done, in the terminal state. */ DONE } private void provideSubscriber(Subscriber subscriber) { this.subscriber = subscriber; switch (state) { case NO_SUBSCRIBER_OR_CONTEXT: state = NO_CONTEXT; break; case NO_SUBSCRIBER: if (buffer.isEmpty()) { state = IDLE; } else { state = BUFFERING; } subscriber.onSubscribe(new ChannelSubscription()); break; case DRAINING: subscriber.onSubscribe(new ChannelSubscription()); break; case NO_SUBSCRIBER_ERROR: cleanup(); state = DONE; subscriber.onSubscribe(new ChannelSubscription()); subscriber.onError(noSubscriberError); break; case DONE: subscriber.onSubscribe(new ChannelSubscription()); subscriber.onComplete(); break; default: // no-op } } @Override public void handlerAdded(ChannelHandlerContext ctx) { // If the channel is not yet registered, then it's not safe to invoke any methods on it, eg read() or close() // So don't provide the context until it is registered. if (ctx.channel().isRegistered()) { provideChannelContext(ctx); } } @Override public void channelRegistered(ChannelHandlerContext ctx) { provideChannelContext(ctx); ctx.fireChannelRegistered(); } private void provideChannelContext(ChannelHandlerContext ctx) { switch (state) { case NO_SUBSCRIBER_OR_CONTEXT: verifyRegisteredWithRightExecutor(); this.ctx = ctx; // It's set, we don't have a subscriber state = NO_SUBSCRIBER; break; case NO_CONTEXT: verifyRegisteredWithRightExecutor(); this.ctx = ctx; state = IDLE; subscriber.onSubscribe(new ChannelSubscription()); break; default: // Ignore, this could be invoked twice by both handlerAdded and channelRegistered. } } private void verifyRegisteredWithRightExecutor() { if (!executor.inEventLoop()) { throw new IllegalArgumentException("Channel handler MUST be registered with the same EventExecutor that it is created with."); } } @Override public void channelActive(ChannelHandlerContext ctx) { // If we subscribed before the channel was active, then our read would have been ignored. if (state == DEMANDING) { requestDemand(); } ctx.fireChannelActive(); } private void receivedCancel() { if (LOG.isTraceEnabled()) { LOG.trace("HandlerPublisher (state: {}) received cancellation request", state); } switch (state) { case BUFFERING: case DEMANDING: case IDLE: cancelled(); // fall through case DRAINING: state = DONE; break; default: // no-op } cleanup(); subscriber = null; } @Override public void channelRead(ChannelHandlerContext ctx, Object message) { if (message instanceof LastHttpContent) { seenLast = true; } if (acceptInboundMessage(message)) { publishMessageLater(message); } else { ctx.fireChannelRead(message); } } private Object messageForTrace(Object message) { Object msg = message; if (message instanceof HttpContent) { HttpContent content = (HttpContent) message; msg = content.content().toString(StandardCharsets.UTF_8); } return msg; } private void publishMessageLater(Object message) { ReferenceCountUtil.touch(message); switch (state) { case IDLE: if (LOG.isTraceEnabled()) { Object msg = messageForTrace(message); LOG.trace("HandlerPublisher (state: IDLE) buffering message: {}", msg); } buffer.add(message); state = BUFFERING; break; case NO_SUBSCRIBER: case BUFFERING: if (LOG.isTraceEnabled()) { Object msg = messageForTrace(message); LOG.trace("HandlerPublisher (state: BUFFERING) buffering message: {}", msg); } buffer.add(message); break; case DEMANDING: state = BUFFERING; buffer.add(message); flushBuffer(); break; case DRAINING: case DONE: if (LOG.isTraceEnabled()) { Object msg = messageForTrace(message); LOG.trace("HandlerPublisher (state: DONE) releasing message: {}", msg); } ReferenceCountUtil.release(message); break; case NO_CONTEXT: case NO_SUBSCRIBER_OR_CONTEXT: throw new IllegalStateException("Message received before added to the channel context"); default: // no-op } } private void publishMessage(Object message) { if (COMPLETE.equals(message)) { if (LOG.isTraceEnabled()) { LOG.trace("HandlerPublisher (state: {}) complete. Calling onComplete()", state); } subscriber.onComplete(); state = DONE; } else { @SuppressWarnings("unchecked") T next = (T) message; if (LOG.isTraceEnabled()) { LOG.trace("HandlerPublisher (state: {}) emitting next message: {}", state, messageForTrace(next)); } ReferenceCountUtil.touch(message, subscriber); subscriber.onNext(next); if (outstandingDemand < Long.MAX_VALUE) { outstandingDemand--; if (outstandingDemand == 0 && state != DRAINING) { if (buffer.isEmpty()) { state = IDLE; } else { state = BUFFERING; } } else if (outstandingDemand > 0 && (state == DEMANDING || state == BUFFERING || state == DRAINING)) { requestDemand(); } } else { requestDemand(); } } } @Override public void channelInactive(ChannelHandlerContext ctx) { complete(); } @Override public void handlerRemoved(ChannelHandlerContext ctx) { complete(); } private void complete() { if (completed.compareAndSet(false, true)) { publishMessageLater(COMPLETE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { switch (state) { case NO_SUBSCRIBER: noSubscriberError = cause; state = NO_SUBSCRIBER_ERROR; cleanup(); break; case BUFFERING: case DEMANDING: case IDLE: case DRAINING: state = DONE; cleanup(); subscriber.onError(cause); break; default: // no-op } } @Override public void closeIfNoSubscriber() { if (subscriber == null) { state = DONE; cleanup(); } } /** * Release all elements from the buffer. */ private void cleanup() { while (!buffer.isEmpty()) { ReferenceCountUtil.release(buffer.remove()); } } private void flushBuffer() { while (!buffer.isEmpty() && outstandingDemand > 0) { if (LOG.isTraceEnabled()) { LOG.trace("HandlerPublisher (state: {}) release message from buffer to satisfy demand: {}", state, outstandingDemand); } publishMessage(buffer.remove()); } if (buffer.isEmpty()) { if (outstandingDemand > 0) { if (state == BUFFERING) { state = DEMANDING; } // otherwise we're draining if (!completed.get()) { requestDemand(); } } else if (state == BUFFERING) { state = IDLE; } } } /** * A channel subscrition. */ private class ChannelSubscription implements Subscription { private volatile boolean cancelled = false; @Override public void request(final long demand) { executor.execute(() -> receivedDemand(demand)); } @Override public void cancel() { executor.execute(HandlerPublisher.this::receivedCancel); // *immediately* stop forwarding messages. The downstream might discard them, and leak buffers! cancelled = true; outstandingDemand = 0; } private void illegalDemand() { cleanup(); subscriber.onError(new IllegalArgumentException("Request for 0 or negative elements in violation of Section 3.9 of the Reactive Streams specification")); ctx.close(); state = DONE; } private boolean addDemand(long demand) { if (demand <= 0) { illegalDemand(); return false; } else { if (outstandingDemand < Long.MAX_VALUE) { outstandingDemand += demand; if (outstandingDemand < 0) { outstandingDemand = Long.MAX_VALUE; } } return true; } } private void receivedDemand(long demand) { // has this subscription been cancelled, since we were scheduled? if (cancelled) { return; } switch (state) { case BUFFERING: case DRAINING: case DEMANDING: if (LOG.isTraceEnabled()) { LOG.trace("HandlerPublisher (state: {}) received demand: {}", state, demand); } if (addDemand(demand)) { flushBuffer(); } break; case IDLE: if (LOG.isTraceEnabled()) { LOG.trace("HandlerPublisher (state: {}) received demand: {}", state, demand); } if (addDemand(demand)) { // Important to change state to demanding before doing a read, in case we get a synchronous // read back. state = DEMANDING; requestDemand(); } break; default: // no-op } } } }