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

com.sun.faces.push.WebsocketSessionManager Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 1997, 2020 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 com.sun.faces.push;

import static com.sun.faces.cdi.CdiUtils.getBeanReference;
import static com.sun.faces.push.WebsocketEndpoint.PARAM_CHANNEL;
import static jakarta.websocket.CloseReason.CloseCodes.NORMAL_CLOSURE;
import static java.lang.String.format;
import static java.util.Collections.emptySet;
import static java.util.logging.Level.WARNING;

import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Logger;

import com.sun.faces.util.Util;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.WebsocketEvent;
import jakarta.faces.event.WebsocketEvent.Closed;
import jakarta.faces.event.WebsocketEvent.Opened;
import jakarta.faces.push.Push;
import jakarta.inject.Inject;
import jakarta.websocket.CloseReason;
import jakarta.websocket.Session;

/**
 * 

* This web socket session manager holds all web socket sessions by their channel identifier. * * @author Bauke Scholtz * @see Push * @since 2.3 */ @ApplicationScoped public class WebsocketSessionManager { // Constants ------------------------------------------------------------------------------------------------------ private static final Logger logger = Logger.getLogger(WebsocketSessionManager.class.getName()); private static final CloseReason REASON_EXPIRED = new CloseReason(NORMAL_CLOSURE, "Expired"); private static final AnnotationLiteral SESSION_OPENED = new AnnotationLiteral() { private static final long serialVersionUID = 1L; }; private static final AnnotationLiteral SESSION_CLOSED = new AnnotationLiteral() { private static final long serialVersionUID = 1L; }; private static final long TOMCAT_WEB_SOCKET_RETRY_TIMEOUT = 10; // Milliseconds. private static final long TOMCAT_WEB_SOCKET_MAX_RETRIES = 100; // So, that's retrying for about 1 second. private static final String WARNING_TOMCAT_WEB_SOCKET_BOMBED = "Tomcat cannot handle concurrent push messages." + " A push message has been sent only after %s retries of " + TOMCAT_WEB_SOCKET_RETRY_TIMEOUT + "ms apart." + " Consider rate limiting sending push messages. For example, once every 500ms."; private static final String ERROR_TOMCAT_WEB_SOCKET_BOMBED = "Tomcat cannot handle concurrent push messages." + " A push message could NOT be sent after %s retries of " + TOMCAT_WEB_SOCKET_RETRY_TIMEOUT + "ms apart." + " Consider rate limiting sending push messages. For example, once every 500ms."; // Properties ----------------------------------------------------------------------------------------------------- private final ConcurrentMap> socketSessions = new ConcurrentHashMap<>(); @Inject private WebsocketUserManager socketUsers; // Actions -------------------------------------------------------------------------------------------------------- /** * Register given channel identifier. * * @param channelId The channel identifier to register. */ protected void register(String channelId) { if (!socketSessions.containsKey(channelId)) { socketSessions.putIfAbsent(channelId, new ConcurrentLinkedQueue()); } } /** * Register given channel identifiers. * * @param channelIds The channel identifiers to register. */ protected void register(Iterable channelIds) { for (String channelId : channelIds) { register(channelId); } } /** * On open, add given web socket session to the mapping associated with its channel identifier and returns * true if it's accepted (i.e. the channel identifier is known) and the same session hasn't been added * before, otherwise false. * * @param session The opened web socket session. * @return true if given web socket session is accepted and is new, otherwise false. */ protected boolean add(Session session) { String channelId = getChannelId(session); Collection sessions = socketSessions.get(channelId); if (sessions != null && sessions.add(session)) { Serializable user = socketUsers.getUser(getChannel(session), channelId); if (user != null) { session.getUserProperties().put("user", user); } fireEvent(session, null, SESSION_OPENED); return true; } return false; } /** * Encode the given message object as JSON and send it to all open web socket sessions associated with given web socket * channel identifier. * * @param channelId The web socket channel identifier. * @param message The push message string. * @return The results of the send operation. If it returns an empty set, then there was no open session associated with * given channel identifier. The returned futures will return null on {@link Future#get()} if the message * was successfully delivered and otherwise throw {@link ExecutionException}. */ protected Set> send(String channelId, String message) { Collection sessions = channelId != null ? socketSessions.get(channelId) : null; if (sessions != null && !sessions.isEmpty()) { Set> results = new HashSet<>(sessions.size()); for (Session session : sessions) { if (session.isOpen()) { results.add(send(session, message, true)); } } return results; } return emptySet(); } private Future send(Session session, String text, boolean retrySendTomcatWebSocket) { try { return session.getAsyncRemote().sendText(text); } catch (IllegalStateException e) { // Awkward workaround for Tomcat not willing to queue/synchronize asyncRemote(). // https://bz.apache.org/bugzilla/show_bug.cgi?id=56026 if (session.getClass().getName().startsWith("org.apache.tomcat.websocket.") && e.getMessage().contains("[TEXT_FULL_WRITING]")) { if (retrySendTomcatWebSocket) { return CompletableFuture.supplyAsync(() -> retrySendTomcatWebSocket(session, text)); } else { return null; } } else { throw e; } } } private Void retrySendTomcatWebSocket(Session session, String text) { int retries = 0; Exception cause = null; while (++retries < TOMCAT_WEB_SOCKET_MAX_RETRIES) { try { Thread.sleep(TOMCAT_WEB_SOCKET_RETRY_TIMEOUT); if (!session.isOpen()) { cause = new IllegalStateException("Too bad, session is now closed"); break; } Future result = send(session, text, false); if (result == null) { continue; } if (logger.isLoggable(WARNING)) { logger.log(WARNING, format(WARNING_TOMCAT_WEB_SOCKET_BOMBED, retries)); } return result.get(); } catch (InterruptedException | ExecutionException e) { Thread.currentThread().interrupt(); cause = e; break; } } throw new UnsupportedOperationException(format(ERROR_TOMCAT_WEB_SOCKET_BOMBED, retries), cause); } /** * On close, remove given web socket session from the mapping. * * @param session The closed web socket session. * @param reason The close reason. */ protected void remove(Session session, CloseReason reason) { Collection sessions = socketSessions.get(getChannelId(session)); if (sessions != null && sessions.remove(session)) { fireEvent(session, reason, SESSION_CLOSED); } } /** * Deregister given channel identifiers and explicitly close all open web socket sessions associated with it. * * @param channelIds The channel identifiers to deregister. */ protected void deregister(Iterable channelIds) { for (String channelId : channelIds) { Collection sessions = socketSessions.remove(channelId); if (sessions != null) { for (Session session : sessions) { if (session.isOpen()) { try { session.close(REASON_EXPIRED); } catch (IOException ignore) { continue; } } } } } } // Internal ------------------------------------------------------------------------------------------------------- private static volatile WebsocketSessionManager instance; /** * Internal usage only. Awkward workaround for it being unavailable via @Inject in endpoint in Tomcat+Weld/OWB. */ static WebsocketSessionManager getInstance() { if (instance == null) { instance = getBeanReference(WebsocketSessionManager.class); } return instance; } // Helpers -------------------------------------------------------------------------------------------------------- private static String getChannel(Session session) { return session.getPathParameters().get(PARAM_CHANNEL); } private static String getChannelId(Session session) { return session.getQueryString(); } private static void fireEvent(Session session, CloseReason reason, AnnotationLiteral qualifier) { Serializable user = (Serializable) session.getUserProperties().get("user"); Util.getCdiBeanManager(FacesContext.getCurrentInstance()).getEvent().select(WebsocketEvent.class, qualifier) .fire(new WebsocketEvent(getChannel(session), user, reason != null ? reason.getCloseCode() : null)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy