isapir / lucee-websocket

Enables server WebSockets for Lucee via JSR-356 compliant servlet containers (e.g. Tomcat 8, Jetty 9.1, etc.)
GNU Lesser General Public License v2.1
17 stars 6 forks source link

Multiple Endpoints may not be deployed to the same path #9

Open redtopia opened 6 years ago

redtopia commented 6 years ago

I'm getting the following error in websocket.log when calling WebsocketRegister() after this function has already been called.

DEBUG","http-nio-8888-exec-6","08/23/2017","17:57:53","myapp","Failed to register endpoint /ws/messages/thread/{channel}: Multiple Endpoints may not be deployed to the same path [/ws/messages/thread/{channel}] : existing endpoint was class net.twentyonesolutions.lucee.websocket.LuceeEndpoint and new endpoint is class net.twentyonesolutions.lucee.websocket.LuceeEndpoint

When this happens, my websockets are failing in the browser during the onHandshake phase with a 500 error. I do not see any other error information in the logs. To fix this, I have to restart Lucee.

Would it make sense to have another function that can be called that will cleanup and close all websockets on the server?

isapir commented 6 years ago

This error comes from the servlet container (Tomcat) and is part of the Java specification IIRC.

Why do you need to call this function multiple times? In general, you should do that in onApplicationStart() or when you modify the Listener component so that the new "version" of the Listener component is registered.

When you do that, the new Listener component is still registered before the error happens, that's the reason I only log it in debug level, because in general it should not affect your application in any way.

I have done that many times and have not experienced that 500 error. Try to see if you can find more information about that error.

redtopia commented 6 years ago

In development, I can set a URL flag that "resets" the application. Basically it calls onApplicationStart() again, reloading everything into the application scope the same way as when the application starts up for the first time. I do this when I've made changes to code that need to be reloaded into the application scope. This includes changes to websocket listener components. WebsocketRegister() is being called in onApplicationStart() as it should be, and to reload listener components, I set the URL flag and reload the page.

The 500 error does not happen each and every time I reset the application, but seems to happen after it's been called a bunch of times during heavy development, which makes me think that there could be a memory leak or perhaps there could be a more complete way to cleanup and reset the websocket extension prior to calling WebsocketRegister() again.

isapir commented 6 years ago

Do your Listener methods have the code wrapped in a try/catch? I'd add that if no, and log all errors. Usually if you get an error from the server it's because the listener threw an error, but I'm not sure why you don't see anything else in the logs. The extension should log that as well IIRC.

redtopia commented 6 years ago

Yes, there are try/catches around all the code in each method (in all my listeners), and exceptions are being logged. Aside from websocket.log (and my own logs), where else might I find other logging information?

isapir commented 6 years ago

If it's in the Listener or your Application then the regular Lucee logs (e.g. application.log, exception.log, etc.); If low level issue then Tomcat logs. I'd check all.

isapir commented 6 years ago

Do you have more information on this issue? Anything from the logs?

redtopia commented 6 years ago

I don't have any additional information on this. I've gotten into the habit of restarting Lucee each time I make a code change in my listeners, which is obviously a PIA when you're iterating through your build cycle frequently.

LanceLake commented 6 years ago

I can concur. The restarting Lucee is very much a PIA.

On Mon, Sep 25, 2017 at 8:12 AM, JP notifications@github.com wrote:

I don't have any additional information on this. I've gotten into the habit of restarting Lucee each time I make a code change in my listeners, which is obviously a PIA when you're iterating through your build cycle frequently.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/isapir/lucee-websocket/issues/9#issuecomment-331913029, or mute the thread https://github.com/notifications/unsubscribe-auth/AF7ko1oRwiaTg5QIVwDNwiONErc94SJRks5sl8LrgaJpZM4PAvMm .

redtopia commented 5 years ago

Igal, this is still a problem when updating code on a live server. Is there any workaround to avoid a server restart?

cubiclabs commented 4 years ago

I am also having problems with this. In particular, because I am trying to register more than one websocket. I see the 'Multiple Endpoints may not be deployed to the same path' error first: "DEBUG","ajp-nio-8009-exec-1","11/06/2019","09:11:07","WebSocketDemo_22239930A5394B86323E198C5E785681","Failed to register endpoint /ws/echo: Multiple Endpoints may not be deployed to the same path [/ws/echo] : existing endpoint was [class net.twentyonesolutions.lucee.websocket.LuceeEndpoint] and new endpoint is [class net.twentyonesolutions.lucee.websocket.LuceeEndpoint]"

Then, any additional websocketRegister() calls fail with: "DEBUG","ajp-nio-8009-exec-1","11/06/2019","09:11:07","WebSocketDemo_22239930A5394B86323E198C5E785681","Failed to register endpoint /ws/livebid/{channel}: Deployment of WebSocket Endpoints to the web application with path [] in host [Catalina/w3svc4] is not permitted due to the failure of a previous deployment"

Any subsequent calls to attempt to re-register then all fail with this: "DEBUG","ajp-nio-8009-exec-5","11/06/2019","09:23:27","WebSocketDemo_22239930A5394B86323E198C5E785681","Failed to register endpoint /ws/livebid/{channel}: Deployment of WebSocket Endpoints to the web application with path [] in host [Catalina/w3svc4] is not permitted due to the failure of a previous deployment" "DEBUG","ajp-nio-8009-exec-5","11/06/2019","09:23:27","WebSocketDemo_22239930A5394B86323E198C5E785681","Failed to register endpoint /ws/echo: Deployment of WebSocket Endpoints to the web application with path [] in host [Catalina/w3svc4] is not permitted due to the failure of a previous deployment"

I notice this happens if the application expires. I guess that the websocket is still registered on Tomcat. When the application re-starts and attempts to register the websockets again, the calls fail. Restarting Lucee gets it working again, until an attempt is made to register the websockets again.

paologroppo commented 4 years ago

Someone has found any workaround to avoid a server restart? I experienced 404 errors if I try to WebsocketServer("/ws/chat/{channel}", new ChatListener()); more than 1 time. I need a server restart.

"DEBUG","http-nio-8888-exec-8","04/12/2020","18:50:30","myhost.mydomain.com","Failed to register endpoint /ws/chat/{channel}: Deployment of WebSocket Endpoints to the web application with path [] in host [Catalina/myhost-mydomain-com-myhost-mydomain-com-confl18] is not permitted due to the failure of a previous deployment

Register the component in application.cfc > onApplicationStart() does not help, 404 errors remains until server restart.

isapir commented 4 years ago

IIRC something has changed in Tomcat that is causing that error. It was working fine in Tomcat 8.5.35 and probably there equivalent 9.0.x version. I hadn't had time to look deeper into it so far. .

MNU-497 commented 4 years ago

I'm having the same problem.

"DEBUG","http-nio-8888-exec-3","05/01/2020","12:45:15","eac_websocket_open","Failed to register endpoint /ws/open/{channel}/{method}: Multiple Endpoints may not be deployed to the same path [/ws/open/{channel}/{method}] : existing endpoint was [class net.twentyonesolutions.lucee.websocket.LuceeEndpoint] and new endpoint is [class net.twentyonesolutions.lucee.websocket.LuceeEndpoint]"

michaeloffner commented 4 years ago

Problem is that you cannot redeploy a Endpoint

paologroppo commented 3 years ago

Problem is that you cannot redeploy a Endpoint

I know, but an error is expected, not a websocket freeze until server restart...

redtopia commented 3 years ago

Someone is going to have to pony up and pay Igal for some work. Alternatively there are other 3rd party websocket providers out there that ultimately make http calls to your backend. I also noticed that Ortus has been doing some interesting work using RabbitMQ as a websocket server that sends messages directly into your CFML app.

paologroppo commented 3 years ago

Someone is going to have to pony up and pay Igal for some work.

I agree.

MNU-497 commented 3 years ago

Just to step back in here, my deployment improved extensively when I turned off logging for the websocket plugin. Many of the errors were due to problems with my code where the issues persisted past the code change, until a full tomcat restart.

I had to fix my code, and then do a full restart of tomcat. And again, lower the level of logs recorded. Too many logs can cause huge problems. (I personally think that there is a deeper problem with how threading is effecting the log write commands)

That said, I would really like to see an improvement to the websocket support for Lucee, it's a tough thing to set up as it is right now.

redtopia commented 3 years ago

I got my websocket implementation to be fairly stable after fixing problems in my own code as well. This was not easy to do, especially considering how you often have to restart. Most of my issues had to do with null scopes being passed into the callbacks. I added a lot of this code inside my callbacks:

if (IsNull(Arguments.sessionScope)) {
    logEvent("msgs_thread.onOpen() NULL Session");
    return false;
}

...where logEvent() is a method that writes log messages when logging is turned on. It helped me to build some internal architecture, including a base class for my websocket .cfc files that includes a bunch of helper methods.

If this helps, I created a component named websockets.cfc that I initialize in onApplicationStart() and add to my application scope that registers all my websocket connections and makes it easy for me to broadcast to a channel, like: application.websockets.broadcast()

component {

/*
websockets
*/

This.enabled = true;

Variables.enableLogging = false;
Variables.debug = false;
Variables.logFile = "websockets";
Variables.mappingRoot = "websockets";
Variables.channels = {};
Variables.dsn = "";

public void function logEvent (
    required any data,
    boolean force=false) {

    // note: this is not the same logEvent() method that's in the base class for my registered websocket handlers

    if (!Variables.enableLogging) {
        if (!Arguments.force)
            return;
    }

    try {

        if (IsNull(Arguments.data)) {
            Arguments.data = "[NULL DATA]";
        }

        var theText = IsValid("string", Arguments.data) ? Arguments.data : serializeJSON(Arguments.data);
        writeLog(text=theText, file=Variables.logFile);

    } catch (any e) {

        writeLog(text=serializeJSON(e), file=Variables.logFile);

    }

} // logEvent()

private void function registerChannel (
    required string channel,
    required string endpoint) {

    logEvent("Registering channel: [#Arguments.channel#], endpoint: [#Arguments.endpoint#]");

    Variables.channels[Arguments.channel] = {
        "error": "uninitialized",
        "errorCode": -1
    }

    try {

        var listenerCFC = CreateObject("#Variables.mappingRoot#.#Arguments.channel#").init(
            channel=Arguments.channel,
            dsn=Variables.dsn,
            enableLogging=Variables.enableLogging,
            logFile="websockets-#Arguments.channel#",
            debug=Variables.debug
        );

        Variables.channels[Arguments.channel]["ws"] = WebsocketRegister(Arguments.endpoint, listenerCFC);
        Variables.channels[Arguments.channel].error = "";
        Variables.channels[Arguments.channel].errorCode = 0;

        logEvent("Channel: [#Arguments.channel#] was successfully registered");

    } catch (any e) {

        var theError = Application.ml.getErrorInfo(err=e, logFile=Variables.logFile);
        Variables.channels[Arguments.channel].errorCode = -1;
        Variables.channels[Arguments.channel].error = theError;
        logEvent("Channel: [#Arguments.channel#] registration error: [#theError#]");

    }

} // registerChannel()

private void function initChannels () {

    registerChannel("msgs_thread", "/ws/messages/thread/{channel}");
    registerChannel("comments", "/ws/comments/{channel}");
    registerChannel("user", "/ws/user/{channel}");

    registerChannel("products", "/ws/products/{channel}");

} // initChannels()

public struct function broadcast (
    required string channel,
    required string channelID,
    required any message) {

    var errorCode = -1;
    var r = {
        "errorCode": 0,
        "error": "",
        "wasSent": false
    };

    try {

        logEvent("broadcasting message on channel: [#Arguments.channel#], enabled: [#This.enabled#], message: [#serializeJSON(Arguments.message)#]");

        // don't do anything if websockets aren't enabled
        if (!This.enabled) {
            return(r);
        }

        // make sure the channel exists
        if (!Variables.channels.keyExists(Arguments.channel)) {
            r.errorCode = 404;
            r.error = "websocket channel: [#Arguments.channel#] was not found. Available channels are: [#StructKeyList(Variables.channels)#]";
            logEvent(r.error);
            return(r);
        }

        // make sure the channel doesn't have an error set
        if (Variables.channels[Arguments.channel].errorCode != 0) {
            r.errorCode = Variables.channels[Arguments.channel].errorCode;
            r.error = Variables.channels[Arguments.channel].error;
            logEvent("Did not broadcast on channel: [#Arguments.channel#] because the channel has the following error: [#r.error#]");
            return(r);
        }

        // broadcast the message to the channel
        Variables.channels[Arguments.channel].ws.broadcast(
            JavaCast("string", Arguments.channelID), 
            (IsValid("string", Arguments.message) ? Arguments.message : SerializeJSON(Arguments.message))
        );

        r.wasSent = true;

    } catch (any e) {

        r.errorCode = errorCode;
        r.error = Application.ml.getErrorInfo(err=e, logFile=Variables.logFile, format="text");

    }

    return(r);

} // broadcast()

public websockets function init (
    required string mappingRoot,
    required string dsn,
    boolean enabled=true,
    boolean enableLogging=false,
    string logFile="",
    boolean debug=false) {

    var errorCode = 500;

    try {

        Variables.mappingRoot = Arguments.mappingRoot;
        Variables.dsn = Arguments.dsn;

        // should set to false when websockets extension is not enabled - not sure how to detect this
        This.enabled = Arguments.enabled;

        if (Arguments.logFile != "") {
            Variables.logFile = Arguments.logFile;
        }

        This.errorCode = 0;
        This.error = "";

        Variables.enableLogging = Arguments.enableLogging;
        Variables.debug = Arguments.debug;

        if (This.enabled) {
            initChannels();
        }

        logEvent("Websockets initialized, enabled: [#This.enabled#], channels: [#StructKeyList(Variables.channels)#], logging: [#Variables.enableLogging#], debug: [#Variables.debug #], appName: [#Application.applicationName#]", true);

    } catch (any e) {

        This.error = Application.ml.getErrorInfo(err=e, logFile=Variables.logFile, format="text");
        This.errorCode = errorCode;
        This.enabled = false;

    }

    return(this);

} // init ()

}
cubiclabs commented 3 years ago

To get around the server restarts I have been experimenting with creating a websocket listener that is essentially a proxy for an application scoped component. This way, I can edit and update code in the proxied component without having the restart the server.

I am also setting a flag at in 'server' scope that tells me that the websocket listeners have been registered so that we do not get the multiple endpoints error.

The setup is something like this:

In application.cfc:

/**
* onApplicationStart event handler
*/
function onApplicationStart(){

    // other application initiaion code here
    // ....

    // register websocket listeners
    if(!structKeyExists(server, "wsRegistrations")){
        WebsocketServer("/ws/test/{channel}", new com.ws.TestListener());
        server.wsRegistrations = true;
    }
}

The TestListener.cfc is a stub that extends a generic proxy component

/** TestListener.cfc */
component extends="WebSocketListenerProxy"{
    variables._proxyName = "WSTest";
}

The WebSocketListenerProxy.cfc basically passes all the function calls onto whatever component is being proxied. I am using Wirebox in here, but it could just as easily be an application scoped component.

component{
    // https://github.com/isapir/lucee-websocket/wiki

    variables._proxyName = "";

    any function getProxy(applicationScope, proxyName=variables._proxyName){
        return arguments.applicationScope.wirebox.getInstance(variables._proxyName);
    }

    /**
    onHandshake
    @param endpointConfig - javax.websocket.server.ServerEndpointConfig - see https://docs.oracle.com/javaee/7/api/javax/websocket/server/ServerEndpointConfig.html
    @param request - javax.websocket.server.HandshakeRequest - see https://docs.oracle.com/javaee/7/api/javax/websocket/server/HandshakeRequest.html
    @param response - javax.websocket.HandshakeResponse - see https://docs.oracle.com/javaee/7/api/javax/websocket/HandshakeResponse.html
    @param sessionScope - the Session Scope associated with the incoming connection
    @param applicationScope - the Application Scope associated with the incoming connection
    returning (optional) a falsey value, or throwing an exception will terminate the incoming connection
    **/
    boolean function onHandshake(endpointConfig, request, response, sessionScope, applicationScope){
        return getProxy(arguments.applicationScope).onHandshake(argumentCollection:arguments);
    }

    /**
    onOpen
    @param websocket - see WebSocket API
    @param endpointConfig - javax.websocket.EndpointConfig - see https://docs.oracle.com/javaee/7/api/javax/websocket/EndpointConfig.html
    @param sessionScope - the Session Scope associated with the incoming connection
    @param applicationScope - the Application Scope associated with the incoming connection
    returning (optional) a falsey value, or throwing an exception will terminate the incoming connection
    **/
    boolean function onOpen(websocket, endpointConfig, sessionScope, applicationScope){
        return getProxy(arguments.applicationScope).onOpen(argumentCollection:arguments);
    }

    /**
    onMessage
    @param websocket - see WebSocket API
    @param message - String
    @param sessionScope - the Session Scope associated with the incoming connection
    @param applicationScope - the Application Scope associated with the incoming connection
    returning (optional) a String object will send it back to the websocket client as a reply.
    **/
    string function onMessage(websocket, message, sessionScope, applicationScope){
        return getProxy(arguments.applicationScope).onMessage(argumentCollection:arguments);
    }

    /**
    onError
    @param websocket - see WebSocket API
    @param throwable - see https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html
    @param sessionScope - the Session Scope associated with the incoming connection
    @param applicationScope - the Application Scope associated with the incoming connection
    **/
    void function onError(websocket, throwable, sessionSCope, applicationScope){
        getProxy(arguments.applicationScope).onError(argumentCollection:arguments);
    }

    /**
    onClose
    @param websocket - see WebSocket API
    @param closeReason - javax.websocket.CloseReason - see https://docs.oracle.com/javaee/7/api/javax/websocket/CloseReason.html
    @param sessionScope - the Session Scope associated with the incoming connection
    @param applicationScope - the Application Scope associated with the incoming connection
    **/
    void function onClose(websocket, throwable, sessionSCope, applicationScope){
        getProxy(arguments.applicationScope).onClose(argumentCollection:arguments);
    }

    /**
    onChannelOpen
    @param channel - String - the channel name
    @param connectionManager - see ConnectionManager API
    **/
    void function onChannelOpen(channel, connectionManager){

    }

    /**
    onChannelClose
    @param channel - String - the channel name
    @param connectionManager - see ConnectionManager API
    **/
    void function onChannelClose(channel, connectionManager){

    }

    /**
    onSubscribe
    @param channel - String - the channel name
    @param subscribers - int - the number of subscribers for that channel
    @param websocket - see WebSocket API - the websocket connection that has subscribed to the channel
    @param connectionManager - see ConnectionManager API
    **/
    void function onSubscribe(channel, subscribers, websocket, connectionManager){
        getProxy(arguments.websocket.getApplicationScope()).onSubscribe(argumentCollection:arguments);
    }

    /**
    onUnsubscribe
    @param channel - String - the channel name
    @param subscribers - int - the number of subscribers for that channel
    @param websocket - see WebSocket API - the websocket connection that has subscribed to the channel
    @param connectionManager - see ConnectionManager API
    **/
    void function onUnsubscribe(channel, subscribers, websocket, connectionManager){
        if(arguments.websocket.isOpen()){
            getProxy(arguments.websocket.getApplicationScope()).onUnsubscribe(argumentCollection:arguments);
        }
    }

}

I am not using this in product yet, but that is what I am working towards. This has been working well for me during development.

artknight commented 3 years ago

The way I handled it for now ( until the real fix comes out ) to circumvent the application reloads

if (server.keyExists("websockets"))
    application.sf.websockets = server.websockets;
else {
    application.sf.websockets = websocketServer("/ws/{channel}", new shared.cfc.websockets());
    server.websockets = application.sf.websockets;
}