Websockets as Lucee core, or is an extension better?

I’ve been running into some hurdles as I try to add websocket support using Igal’s websocket extension. First, I want to be clear that this is not a complaint or critique of Igal’s amazing work (big thanks Igal), but rather a discussion about implementation possibilities moving forward, and to see see what other devs have to say about websockets in Lucee/ACF.

A disclaimer… I have never used Websockets in ACF, though I have spent quite a bit of time with Igal’s extension.

The main issue I’m having right now has to do with the extension not running in the same context as my application context. The endpoint listener components cannot access the session or application scopes directly like so: Application.helper.someMethod() or Session.isLoggedIn. Instead, the extension provides your listener methods with the session/application scopes as arguments. This is problematic if your existing code accesses these scope directly (mine does). As a work-around, I have to duplicate/refactor a bunch of my model code to use it in my listener components.

A couple questions:

  1. Is the reason why the websocket extension runs in its own context due to the constraints of how extensions are implemented, or because of the tomcat websocket library?

  2. Is it possible for Lucee to add websockets to the Lucee core, which would then allow websocket listeners to access your application/session scopes normally?

  3. Scaleability. Does ACF websocket support work nicely in a clustered environment? Consider a dedicated websocket server in a cluster… how would you tell that server that a change was made to the user’s session, like they logged out?

  4. Security. If websockets are run in a single context for the entire server, what happens if the server is running mulitple applications? How could you isolate each application so they can’t execute code inside a listener that might adversely affect or access data from another application?

Thanks!

This is common practice in asynchronous code that runs outside of the regular requests. It is exactly the same as onSessionEnd(sessionScope, applicationScope) is used, since it is also running async outside of the standard requests. See https://helpx.adobe.com/coldfusion/cfml-reference/application-cfc-reference/onsessionend.html

CFML developers have been using methods like that for about a decade now, and it may “feel” strange at first, but once you understand how it works it feels very natural coding that way.

Keep in mind that the WebSocket connections are not running inside any Servlet request, so you do not have a valid Request or Session, or even Application object.

The extension is build on top of the JSR-356 standard - http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html - The Java Specification for WebSockets. That allows it to be small (the extension jar is about 30kb), and portable (because the fact that it is written in accordance with the specification it can be added easily to any Servlet container that adheres to the standards, e.g. Tomcat, Jetty, WildFly, etc).

Lucee is a Servlet (so is ACF, by the way), and it runs inside a Servlet container like Tomcat, Jetty, etc. The extension does not open the socket server by itself. Instead, it relies on the JSR-356 specification API and requests the Servlet container to open that socket connection.

From that point, any incoming WebSocket connection go directly to the Servlet container, and from there to the extension – completely bypassing Lucee. In order to still get the Session and Application scopes I had to do some “hacking” as I referred to it in a different thread.

The whole idea behind WebSockets is that they keep the connection open, not having to open a new connection (which requires http handshake, onRequestStart(), onSessionStart(), and a bunch of other overhead). That allows WebSocket messages to be sent and received very fast because the connection is kept opened from before.

The WebSocket API is therefore asynchronous, and event driven. That means that you can not have in a WebSocket event a direct access to the Request scope, or the Session scope. It also means that from your regular request processing, where you do have the Request and Session scope, you can not have a direct access to the WebSocket, because the events, e.g. onMessage() can fire at any time.

You could (Don’t Do This) keep a reference to the WebSocket in your Session scope, but that would likely lead to a memory leak because it will probably maintain references to a lot of dead objects unless you really clean up behind you.

Adding WebSockets to the core will most likely only mean that you do not need to install the extension. WebSockets are separate connections and do not run in the same model to regular Requests.

What can help is to add an API to Lucee which will expose the Session store, so that I wouldn’t need to resort to “hacking”, but that will probably not change anything in the way that you use the WebSockets.

I’ve seen other implementations that use WebSockets in an Event Gateway. I’m not sure what the benefit is, but if you prefer the way they work then perhaps those implementations would work better for you.

That again has to do with the Session management. If Lucee will add an API as mentioned above then it will be much easier to implement this. Otherwise we will need a separate implementation for each Cluster implementation, which is not something that I have the resources to support.

There is no restriction for a single context. I have not experienced that issue myself, but TBH also didn’t have much time to spend on it as of yet.

There is a restriction on Application Names. You can not have the same Application Name in more than one context because of the way that I had to “hack” my way into the Session information. But:

That is actually something that I thought of too, but it has not been thoroughly tested so I didn’t even mention it in the documentation, so consider it experimental:

You can set for the endpoint the host + endpoint, e.g. WebsocketRegister("www.21solutions.net/ws/echo", someListener), and that should give you isolation between different contexts with the same Application Name.

Again, it has not been tested thoroughly, and I’m not sure, for example, if you need to add the port number to the hostname, e.g. 8080 or not, especially when you front your Servlet container with a Web server like httpd or nginx.

1 Like

Thanks for the detailed explanation Igal. It all makes sense to me. BTW, I didn’t assume there would be a Request scope available in a websocket listener component.

I would like to access the Application or Session scopes directly instead of through arguments, but if there’s a better pattern that I could be using that would not assume that those scopes are defined implicitly, then I would like to figure that out.

I would also like to instantiate cfcs from my listener component methods, which is where all my model layer code is implemented, but since my mappings aren’t defined, I can’t just call createObject('cfc.mycomponent') because the cfc mapping doesn’t exist.

These are the two main issues that I wish I had a clear understanding of what I could do differently to 1) define a model layer that doesn’t access Application.myVar directly, and 2) could be instantiated without my required mappings.

You can easily solve that problem by using the Factory Method Pattern (see https://en.wikipedia.org/wiki/Factory_method_pattern or search for that term), and setting a reference to the method in the Application scope.

e.g., add the following snippet somewhere where you initialize your application like onApplicationStart():

/** define factory method */
function createComponent(required string componentPath, args={}){
  return new "#arguments.componentPath#"(arguments.args);
}

/** set a reference to it in Application scope */
Application.methods.createComponent = createComponent;

Then in the event listener you call that method, by referencing the Application scope via arguments.applicationScope, e.g.

/** WebSocket Listener API event handler */
function onOpen(websocket, endpointConfig, sessionScope, applicationScope){

  /** get reference to Application scope */
  var app = arguments.applicationScope;

  /** call factory method with component path and optional args */
  var myc = app.methods.createComponent("cfc.myComponent");

  var anc = app.methods.createComponent(
    "cfc.anotherComponent", { cfid : arguments.sessionScope.cfid }
  );

  /** do something useful here ... */
}

I’m familiar with the factory pattern, and I actually tried it. But the problem still exists that the component will be created in the context of the websocket extension and not your application. So if the component you create requires a mapping to create a new object, or relies on the Application scope, you cannot use that method.

Is there any possible way to build the extension so that it runs in the same context as your application?

Is there a way to either configure, or add a feature to the extension where it becomes possible to configure, the context in which the extension will execute? I’m assuming that if the extension runs in my application’s context, then I would be able to access the session and/or application scopes directly from my listener components.

For example, I cannot use (as is) the following component, even utilizing the factory pattern as suggested, from a websocket listener component, because it accesses the Application scope directly to get the DSN for the query:

component {

Variables.myQuery = "";

public mycomponent function init (
   required numeric id) {

   Variables.myQuery = queryExecute(
      sql="select * from mytable where id = :id",
      params=[{name="id", value=Arguments.id, cfsqltype="cf_sql_integer"}],
      options={
         dsn: Application.app.getDNS()
      }
   );

   return this;
}

}

You can try the experimental solution I offered above at the bottom of the post: Websockets as Lucee core, or is an extension better?

That would still not run “in the context” because that happens at the servlet container level, completely outside of Lucee, but it might give you the correct Application scope so that you will be able to use the Factory pattern as described above.