[blog] | [projects] | [about] | [imprint]

Wicket UI in the Cluster - know how and lessons learned

29 April 2019
 

While working on a Wicket UI cluster support feature in the last weeks I covered quite a bit of new territory that I was only partly aware of, even after ~9 years of doing Wicket. And I had to do quite a bit of research to collect know-how from different sources.

In this post I’d like to share what I have learned and things I want to emphasize that should be applied.

(I’m doing coding in Scala. So some things are Scala related but should generally apply to Java as well.)

 

Model separation

If your application has the potential to get bigger with multiple layers you should separate your models (and not only your models). Honor separation of concerns (SoC) and single responsibility (SRP). Create dedicated models at an architectural, module or package boundary (where necessary) and map your models. Apply orthogonality.

If you don’t it’ll hit you in the face at some point. And you are lucky if it does only once.

You can imagine what the disadvantages are if you don’t use dedicated models: changes to the model affect every part of the application where it’s directly used which makes the application ridgid.

After all ’soft’ware implies being ‘soft’ as in flexible and easy to change.

In regards to Wicket or other ‘external interfaces’ the problem is that a loaded model is partly stored in instance variables of Wicket components. The domain model can contain a ton of data and you have no control over what gets serialized and what not without changing your domain model, which you shouldn’t do to satisfy the requirements of an external interface.

So because in a cluster environment those components now must be (de)serialized to be distributed across the cluster nodes and there is no cache anymore it is:
a) a performance hit and
b) uses up quite some network bandwidth when the session changes a few times per second.

The approach should be to create a dedicated model for a view, because most probably not all data of a domain model is visualized. Further, when the domain model is used directly, submitting form data goes straight back to the domain model. Instead a dedicated ‘submit form’ model can be created that only holds the data of the submit and can be merged back into the domain model on a higher level that can better control when, where and how this is done (i.e. applying additional validations, etc.) This certainly takes a bit more time but is worth the effort in the longer run.

 

Use LoadableDetachableModel

LoadableDetachableModels load the model when a request is made and ‘forget’ it after the response was generated, and before the state is saved to the session. Which means that model data is not stored to the session but reloaded from scratch more often. One has to keep in mind that the session can change multiple times per request/response cycle, in particular if JavaScript based components load their data lazily. In a cluster environment, without the Servlet container’s second-level cache (see below), it is better to load the data on a request basis instead of serializing and deserializing large amounts of data which have to be synchronized between cluster nodes. Usually the application has a general caching mechanism on a higher level which makes loading the data acceptable.

Preferably no model is stored in the components at all but only the state of the components as such. With this the session size can be contained at a few kBytes.

This is something the Wicket developer has to sensibly consider when developing a component.

In Wicket models can be chained. I like using CompoundPropertyModels. But you can still use a LoadableDetachableModel by chaining them together:

new CompountPropertyModel[Foo](new LoadableDetachableModel(myModelObject))

 

Extend from Serializable (or use Scala case classes) for any model classes that are UI model

This should be obvious. Any class that should be serializable requires inheriting from Serializable interface.

In Wicket you can also interit from IClusterable, which is just a marker trait inheriting from Serializable.

 

Add Serializable to abstract parent classes if there is a class hierarchy

I’ve had a few cases where serialized classes could not be deserialized. The reason was that when you have a class hierarchy the abstract base class must also inherit from Serializable.

The deserialization of the code below fails even though class Bar inherits from Serializable. Class Foo also must inherit from Serializable.:

@SerialVersionUID(1L)
abstract class Foo(var1, var2)
  
class Bar extends Foo with Serializable

 

Add @SerialVersionUID, always

Wicket components, including the model classes are serializable by default. But to keep compatibility across temporarily different versions of the app when updating a cluster node, add a SerialVersionUID annotation to your component classes (for Scala, in Java it is a static final field). Also add this to every model data class.

When ommiting this annotation the serial version is dynamically created by Java for each compilation process and hence is incompatible to each other even if no code changes were made. So add this annotation to specify a constant version.

Add this to your IDEs class template mechnism. Any class created should have this annotation. It doesn’t hurt when it’s there but not used.

If you want to know more about this, and how to create compatible versions of classes read this: https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html

 

No Scala Enumeration, causes trouble at deserialization

Use Enumeratum instead or just a combination of Scala case class plus some constant definitions on the companion object.

 

Enumeratum, add no arg constructor with abstract class

The below code doesn’t deserialize if the auxiliary constructor is missing, keep that in mind:

@SerialVersionUID(1L)
sealed abstract class MyEnum(val displayName: String) extends EnumEntry {
  def this() = this("")
}

 

Use Wicket RenderStrategy.ONE_PASS_RENDER

By default Wicket uses a POST-REDIRECT-GET pattern implementation. This is to avoid the ‘double-submit’ problem.

However, in cluster environments it’s possible that the GET request goes to a different cluster node than the POST request and hence this could cause trouble.

So either you have to make certain that the cluster nodes got synchronized between POST and GET or you configure Wicket to the render strategy ONE_PASS_RENDER.

ONE_PASS_RENDER basically returns the page markup as part of the POST response.

See here for more details: https://ci.apache.org/projects/wicket/apidocs/8.x/index.html?org/apache/wicket/settings/RequestCycleSettings.RenderStrategy.html

 

Use Wicket HttpSessionStore

By default Wicket uses a file based session page store where the serialized pages are written to. Wicket stores those to support the browser back button and to render older versions of the page when the back button is pressed.

In a cluster setup the serialized pages must be stored in the session so that the pages can be synchronized between the cluster nodes.

In Wicket version 8 you do it like this (in Application#init()):

setPageManagerProvider(new DefaultPageManagerProvider(this) {
  override def newDataStore() = {
    new HttpSessionDataStore(getPageManagerContext, new PageNumberEvictionStrategy(5))
  }
})

The PageNumberEvictionStratety defines how many versions of one page are stored.

 

Disable the Servlet containers second-level cache

Jetty (or generally Servlet containers) usually uses a second-level cache (DefaultSessionCache) where session data, in form of the runtime objects, is stored for quick access without going through the (de)serialization.

In a cluster environment however this can cause issues because what the second-level cache contains is likely to be different on each cluster node and hence wrong states may be pulled out of it when the load-balancer is delegating to a different node for a request.

So it is better to not use a second-level cache. In Jetty you do this by setting up a NullSessionCache. To this NullSessionCache you also have to provide the backing SessionDataStore where the session data is written and read from.

You do this like this on a ServletContextHandler basis (Jetty 9.4):

val sessionHandler = new SessionHandler
handler.setSessionHandler(sessionHandler)

val sessionCache = new NullSessionCacheFactory().getSessionCache(handler.getSessionHandler)
val sessionStore = // set your `SessionDataStore` implementation here

sessionCache.setSessionDataStore(sessionStore)
sessionHandler.setSessionCache(sessionCache)

You have different options for the SessionDataStore implementation. Jetty provides a JDBCSessionDataStore which stores the session data into a database.

But there are also implementations for Memcached or Hazelcast, etc.

 

Serialization considerations

There are other options than the Java object serialization. I’d like to name two which are supported by Wicket:

Both provide more performance and flexibility on serialization than the default Java serializer and should be considered.

[atom/rss feed]