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

org.glassfish.jersey.media.sse.internal.EventProcessor Maven / Gradle / Ivy

/*
 * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.media.sse.internal;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.ws.rs.ServiceUnavailableException;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.sse.SseEvent;

import org.glassfish.jersey.client.ClientExecutor;
import org.glassfish.jersey.internal.util.ExtendedLogger;
import org.glassfish.jersey.media.sse.EventInput;
import org.glassfish.jersey.media.sse.EventListener;
import org.glassfish.jersey.media.sse.EventSource;
import org.glassfish.jersey.media.sse.InboundEvent;
import org.glassfish.jersey.media.sse.LocalizationMessages;
import org.glassfish.jersey.media.sse.SseFeature;

/**
 * Private event processor task responsible for connecting to the SSE stream and processing
 * incoming SSE events as well as handling any connection issues.
 */
public class EventProcessor implements Runnable, EventListener {

    private static final Level CONNECTION_ERROR_LEVEL = Level.FINE;
    private static final ExtendedLogger LOGGER =
            new ExtendedLogger(Logger.getLogger(EventProcessor.class.getName()), Level.FINEST);

    /**
     * Open connection response arrival synchronization latch.
     */
    private final CountDownLatch firstContactSignal;
    /**
     * Last received event id.
     */
    private String lastEventId;
    /**
     * Re-connect delay.
     */
    private long reconnectDelay;
    /**
     * SSE streaming resource target.
     */
    private final WebTarget target;
    /**
     * Flag indicating if the persistent HTTP connections should be disabled.
     */
    private final boolean disableKeepAlive;
    /**
     * Incoming SSE event processing task executor.
     */
    private final ClientExecutor executor;
    /**
     * Event source internal state.
     */
    private final AtomicReference state;
    /**
     * List of all listeners not bound to receive only events of a particular name.
     */
    private final List unboundListeners;
    /**
     * A map of listeners bound to receive only events of a particular name.
     */
    private final Map> boundListeners;

    /**
     * Shutdown handler is invoked when Event processor reaches terminal stage.
     */
    private final ShutdownHandler shutdownHandler;

    /**
     * Invoked whenever an event is received.
     */
    private final EventListener eventListener;

    private EventProcessor(final EventProcessor that) {
        this.firstContactSignal = null;

        this.reconnectDelay = that.reconnectDelay;
        this.lastEventId = that.lastEventId;
        this.target = that.target;
        this.disableKeepAlive = that.disableKeepAlive;
        this.executor = that.executor;
        this.state = that.state;
        this.boundListeners = that.boundListeners;
        this.unboundListeners = that.unboundListeners;
        this.eventListener = that.eventListener;
        this.shutdownHandler = that.shutdownHandler;
    }

    private EventProcessor(Builder builder) {
        // Synchronization barrier used to signal that the initial contact with SSE endpoint
        // has been made.
        this.firstContactSignal = new CountDownLatch(1);

        this.reconnectDelay = builder.reconnectDelay;
        this.lastEventId = builder.lastEventId;
        this.target = builder.target;
        this.disableKeepAlive = builder.disableKeepAlive;
        this.executor = builder.clientExecutor;
        this.state = builder.state;
        this.boundListeners = builder.boundListeners == null ? Collections.EMPTY_MAP : builder.boundListeners;
        this.unboundListeners = builder.unboundListeners == null ? Collections.EMPTY_LIST : builder.unboundListeners;
        this.eventListener = builder.eventListener;
        this.shutdownHandler = builder.shutdownHandler;
    }

    /**
     * Create new Event processor builder.
     *
     * @param target web target to be used to call remote resource.
     * @param state state shared with the owner of event processor instance.
     * @param clientExecutor executor service used for consuming events and scheduling reconnects.
     * @param eventListener event listener.
     * @param shutdownHandler shutdown callback.
     * @return new {@link Builder} instance.
     */
    public static Builder builder(WebTarget target,
                                  AtomicReference state,
                                  ClientExecutor clientExecutor,
                                  EventListener eventListener,
                                  ShutdownHandler shutdownHandler) {

        return new Builder(target, state, clientExecutor, eventListener, shutdownHandler);
    }

    @Override
    public void run() {
        LOGGER.debugLog("Listener task started.");

        EventInput eventInput = null;
        try {
            try {
                final Invocation.Builder request = prepareHandshakeRequest();
                if (state.get() == State.OPEN) { // attempt to connect only if even source is open
                    LOGGER.debugLog("Connecting...");
                    eventInput = request.get(EventInput.class);
                    LOGGER.debugLog("Connected!");
                }
            } finally {
                if (firstContactSignal != null) {
                    // release the signal regardless of event source state or connection request outcome
                    firstContactSignal.countDown();
                }
            }

            final Thread execThread = Thread.currentThread();

            while (state.get() == State.OPEN && !execThread.isInterrupted()) {
                if (eventInput == null || eventInput.isClosed()) {
                    LOGGER.debugLog("Connection lost - scheduling reconnect in {0} ms", reconnectDelay);
                    scheduleReconnect(reconnectDelay);
                    break;
                } else {
                    this.onEvent(eventInput.read());
                }
            }
        } catch (ServiceUnavailableException ex) {
            LOGGER.debugLog("Received HTTP 503");
            long delay = reconnectDelay;
            if (ex.hasRetryAfter()) {
                LOGGER.debugLog("Recovering from HTTP 503 using HTTP Retry-After header value as a reconnect delay");
                final Date requestTime = new Date();
                delay = ex.getRetryTime(requestTime).getTime() - requestTime.getTime();
                delay = (delay > 0) ? delay : 0;
            }

            LOGGER.debugLog("Recovering from HTTP 503 - scheduling to reconnect in {0} ms", delay);
            scheduleReconnect(delay);
        } catch (Exception ex) {
            if (LOGGER.isLoggable(CONNECTION_ERROR_LEVEL)) {
                LOGGER.log(CONNECTION_ERROR_LEVEL, String.format("Unable to connect - closing the event source to %s.",
                        target.getUri().toASCIIString()), ex);
            }
            // if we're here, an unrecoverable error has occurred - just turn off the lights...
            shutdownHandler.shutdown();
        } finally {
            if (eventInput != null && !eventInput.isClosed()) {
                eventInput.close();
            }
            LOGGER.debugLog("Listener task finished.");
        }
    }

    /**
     * Called by the event source when an inbound event is received.
     *
     * This listener aggregator method is responsible for invoking {@link EventSource#onEvent(InboundEvent)}
     * method on the owning event source as well as for notifying all registered {@link EventListener event listeners}.
     *
     * @param event incoming {@link InboundEvent inbound event}.
     */
    @Override
    public void onEvent(final InboundEvent event) {
        if (event == null) {
            return;
        }

        LOGGER.debugLog("New event received.");

        if (event.getId() != null) {
            lastEventId = event.getId();
        }
        if (event.isReconnectDelaySet()) {
            reconnectDelay = event.getReconnectDelay();
        }

        notify(eventListener, event);
        notify(unboundListeners, event);

        final String eventName = event.getName();
        if (eventName != null) {
            final List eventListeners = boundListeners.get(eventName);
            if (eventListeners != null) {
                notify(eventListeners, event);
            }
        }
    }

    private void notify(final Collection listeners, final InboundEvent event) {
        for (EventListener listener : listeners) {
            notify(listener, event);
        }
    }

    private void notify(final EventListener listener, final InboundEvent event) {
        try {
            listener.onEvent(event);
        } catch (Exception ex) {
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.log(Level.FINE, String.format("Event notification in a listener of %s class failed.",
                        listener.getClass().getName()), ex);
            }
        }
    }

    /**
     * Schedule a new event processor task to reconnect after the specified {@code delay} [milliseconds].
     *
     * If the {@code delay} is zero or negative, the new reconnect task will be scheduled immediately.
     * The {@code reconnectDelay} and {@code lastEventId} field values are propagated into the newly
     * scheduled task.
     * 

* The method will silently abort in case the event source is not {@link EventSource#isOpen() open}. *

* * @param delay specifies the amount of time [milliseconds] to wait before attempting a reconnect. * If zero or negative, the new reconnect task will be scheduled immediately. */ private void scheduleReconnect(final long delay) { final State s = state.get(); if (s != State.OPEN) { LOGGER.debugLog("Aborting reconnect of event source in {0} state", state); return; } // propagate the current reconnectDelay, but schedule based on the delay parameter final EventProcessor processor = new EventProcessor(this); if (delay > 0) { executor.schedule(processor, delay, TimeUnit.MILLISECONDS); } else { executor.submit(processor); } } private Invocation.Builder prepareHandshakeRequest() { final Invocation.Builder request = target.request(SseFeature.SERVER_SENT_EVENTS_TYPE); if (lastEventId != null && !lastEventId.isEmpty()) { request.header(SseFeature.LAST_EVENT_ID_HEADER, lastEventId); } if (disableKeepAlive) { request.header("Connection", "close"); } return request; } /** * Await the initial contact with the SSE endpoint. */ public void awaitFirstContact() { LOGGER.debugLog("Awaiting first contact signal."); try { if (firstContactSignal == null) { return; } try { firstContactSignal.await(); } catch (InterruptedException ex) { LOGGER.log(CONNECTION_ERROR_LEVEL, LocalizationMessages.EVENT_SOURCE_OPEN_CONNECTION_INTERRUPTED(), ex); Thread.currentThread().interrupt(); } } finally { LOGGER.debugLog("First contact signal released."); } } /** * Event processor state, which is shared with the owner (to be able to control bootstrap and shutdown). */ public enum State { /** * Ready to connect. */ READY, /** * Connection established, events can be received. */ OPEN, /** * Closed, won't receive any events. */ CLOSED } /** * {@link EventProcessor} builder. */ public static class Builder { private final WebTarget target; private final AtomicReference state; private final ClientExecutor clientExecutor; private final EventListener eventListener; private final ShutdownHandler shutdownHandler; private long reconnectDelay; private TimeUnit reconnectUnit; private String lastEventId; private boolean disableKeepAlive; private List unboundListeners; private Map> boundListeners; private Builder(WebTarget target, AtomicReference state, ClientExecutor clientExecutor, EventListener eventListener, ShutdownHandler shutdownHandler) { this.target = target; this.state = state; this.clientExecutor = clientExecutor; this.eventListener = eventListener; this.shutdownHandler = shutdownHandler; } /** * Set initial reconnect delay. * * Reconnect delay can be controlled by the server side, adding specific properties to incoming events. * * @param reconnectDelay reconnect delay value. * @param unit reconnect delay timeunit. * @return updated builder instance. */ public Builder reconnectDelay(long reconnectDelay, TimeUnit unit) { this.reconnectDelay = reconnectDelay; this.reconnectUnit = reconnectUnit; return this; } /** * Unbounded listeners will get notified about any incoming event. * * @param unboundListeners list of listeners. * @return updated builder instance. */ public Builder unboundListeners(List unboundListeners) { this.unboundListeners = unboundListeners; return this; } /** * Unbounded listeners will get notified about incoming events with particular name. * * @param boundListeners map of bound listeners, key is a name to which listeners are bound to, value is a list * of listeners. * @return updated builder instance. * @see SseEvent#getName() */ public Builder boundListeners(Map> boundListeners) { this.boundListeners = boundListeners; return this; } /** * Disables keepalive. * * @return updated builder instance. */ public Builder disableKeepAlive() { this.disableKeepAlive = true; return this; } /** * Build the {@link EventProcessor}. * * @return built Event processor instance. */ public EventProcessor build() { return new EventProcessor(this); } } /** * Used to signal that the {@link EventProcessor} reached terminal stage. */ public interface ShutdownHandler { /** * Invoked when the {@link EventProcessor} reaches terminal stage. * * All resources should be freed at this point. */ void shutdown(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy