What you will learn

In this tutorial we’ll have at look at Sessions. We’ll learn about what they’re used for, as well as different ways of persisting, caching and clustering sessions using Jetty.

What is a session?

When a user visits a website (or opens a webapp), the server usually creates a Session object for the user. Users are usually linked to their Session by cookie with a sessionId value. This Session object can be used to store information about the current, well, session, like if the user is logged in.

The architecture of session management changed significantly in Jetty 9.4, and this tutorial is intended to get you up to speed. You can view the documentation on Jetty’s website if you need all the details.

Persisting sessions

By default Jetty will store all its session information in a HashMap, which is stored in memory (RAM). When the Jetty server restarts all of the sessions are cleared. Restarts can happen for example if you’re making changes on localhost, or if you’re deploying a new version of your app to your cloud provider.

Persisting to the file system

The simplest way to persist a Session is to store the Sessions as files on the file system. This can be done using a FileSessionDataStore.

This approach is well suited for a dev environment, since it’s easy to set up and has no dependencies. You need to create a SessionHandler with a SessionCache, and attach a FileSessionDataStore:

fun fileSessionHandler() = SessionHandler().apply { // create the session handler
    sessionCache = DefaultSessionCache(this).apply { // attach a cache to the handler
        sessionDataStore = FileSessionDataStore().apply { // attach a store to the cache
            val baseDir = File(System.getProperty("java.io.tmpdir"))
            this.storeDir = File(baseDir, "javalin-session-store").apply { mkdir() }
        }
    }
}

This approach can also work on a remote server, but some cloud providers wipe all files when you redeploy your service, so be careful. File IO can also be slow, depending on your hardware. If you want your sessions to be a bit more persistent, and faster, you can use a database.

Persisting to a database

Programmatically, persisting to a database is not very different from persisting to the file system. You need to create a SessionHandler with a SessionCache, but instead of using a FileSessionDataStore you need to use a datastore specific for your database. Here is an example using JDBC:

fun sqlSessionHandler(driver: String, url: String) = SessionHandler().apply {
    sessionCache = DefaultSessionCache(this).apply { // create the session handler
        sessionDataStore = JDBCSessionDataStoreFactory().apply { // attach a cache to the handler
            setDatabaseAdaptor(DatabaseAdaptor().apply { // attach a store to the cache
                setDriverInfo(driver, url)
            })
        }.getSessionDataStore(sessionHandler)
    }
}

If you want to use MongoDB you simply change the sessionDataStore part of the above code:

sessionDataStore = MongoSessionDataStoreFactory().apply {
    connectionString = "..."
    dbName = "..."
    collectionName = "..."
}.getSessionDataStore(sessionHandler)

Jetty supports JDBC, MongoDB, Inifinspan, Hazelcast, and Google Cloud DataStore. JDBC is included with in the core jetty-server dependency, while MongoDB and others require additional dependencies.

If you’re using a SQL database, Jetty will create a jettysessions table. If you’re using MongoDB it will create a document. They both contain the same data:

{
    "_id": {
        "$oid": "5b858d527d3c0f8722173292"
    },
    "id": "node0j4ii2zxu01i91g5f8odup78c30",
    "accessed": 1535479586876,
    "context": {
        "0_0_0_0:": {
            "__metadata__": {
                "lastNode": "node0",
                "lastSaved": 1535479586879,
                "version": 78
            },
            "signed-in-user": "tipsy" // custom data
        }
    },
    "created": 1535479122617,
    "expiry": 0,
    "lastAccessed": 1535479585053,
    "maxIdle": -1,
    "valid": true
}

For performance and security reasons, it’s advised to create a separate database (on the database instance) with credentials unique to Jetty.

Session cache and clustering

Since database and file systems operations are relatively slow, caching data in memory can increase the performance of your application. In both the previous examples we attached a SessionCache to our SessionHandler. There are two implementations of the SessionCache included in Jetty, DefaultSessionCache and NullSessionCache.

DefaultSessionCache

We used the DefaultSessionCache in our previous examples, and it caches sessions in memory. This is great if you have one instance of your app running, but it can lead to trouble if you have two instances behind a load-balancer. Jetty recommends you always use sticky-sessions with the DefaultSessionCache, but even with sticky sessions you can run into inconsistencies. If an instance goes down or gets overloaded, traffic will be routed to the other instance which won’t have the same session in its cache. This is where the NullSessionCache is useful.

NullSessionCache

The NullSessionCache doesn’t actually do any caching at all. Every time a Session is needed it’s fetched from the SessionDataStore. This means that all instances share the same source of truth, and there won’t be any inconsistencies. Jetty recommends this approach for clustering without sticky-sessions, but it’s the safer choice even if sticky-sessions are enabled.

There is a performance penalty to not caching, but if you’re running a dedicated database on the same network with small sessions, it should just be ~10ms per request. Using an external hosted MongoDB such as mlab it seems to be around 40ms.

Summary

  • Session handling in Jetty requires a SessionHandler, SessionCache, and a SessionDataStore
  • A FileSessionDataStore is well suited for development environments
  • The DefaultSessionCache works well if you only run one instance of your app
  • The NullSessionCache is suitable for multiple instances running behind a loadbalancer

Addendum

Since you are currently on javalin.io, it should be mentioned how to use this knowledge in your Javalin app. Since Javalin relies on Jetty for session handling can, you simply pass your SessionHandler:

val app = Javalin.create().apply {
    sessionHandler { fileSessionHandler() }
}.start(7000)

As we saw earlier, the SessionHandler has a SessionCache which again has a SessionDataStore, so no further configuration is required.