Wicket UI in the Cluster - know how and lessons learned
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
LoadableDetachableModel
s 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 CompoundPropertyModel
s. But you can still use a LoadableDetachableModel
by chaining them together:
new CompountPropertyModel[Foo](new LoadableDetachableModel(myModelObject))
Extend from Serializable
(or use Scala case class
es) 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:
- https://github.com/wicketstuff/core/tree/master/serializer-kryo2
- https://github.com/wicketstuff/core/tree/master/serializer-fast2
Both provide more performance and flexibility on serialization than the default Java serializer and should be considered.
-
[Polymorphism and Multimethods]
02-03-2023 -
[Global Day of CodeRetreat - recap]
07-11-2022 -
[House automation tooling - Part 4 - Finalized]
01-11-2022 -
[House automation tooling - Part 3 - London-School and Double-Loop]
02-07-2022 -
[Modern Programming]
14-05-2022 -
[House automation tooling - Part 2 - Getting Serial]
21-03-2022 -
[House automation tooling - Part 1 - CL on MacOSX Tiger]
07-03-2022 -
[Common Lisp - Oldie but goldie]
18-12-2021 -
[Functional Programming in (Common) Lisp]
29-05-2021 -
[Patterns - Builder-make our own]
13-03-2021 -
[Patterns - Builder]
24-02-2021 -
[Patterns - Abstract-Factory]
07-02-2021 -
[Lazy-sequences - part 2]
13-01-2021 -
[Lazy-sequences]
07-01-2021 -
[Thoughts about agile software development]
17-11-2020 -
[Test-driven Web application development with Common Lisp]
04-10-2020 -
[Wicket UI in the cluster - the alternative]
09-07-2020 -
[TDD - Mars Rover Kata Outside-in in Common Lisp]
03-05-2020 -
[MVC Web Application with Elixir]
16-02-2020 -
[Creating a HTML domain language in Elixir with macros]
15-02-2020 -
[TDD - Game of Life in Common Lisp]
01-07-2019 -
[TDD - classicist vs. London Style]
27-06-2019 -
[Wicket UI in the cluster - reflection]
10-05-2019 -
[Wicket UI in the Cluster - know how and lessons learned]
29-04-2019 -
[TDD - Mars Rover Kata classicist in Scala]
23-04-2019 -
[Burning your own Amiga ROMs (EPROMs)]
26-01-2019 -
[TDD - Game of Life in Clojure and Emacs]
05-01-2019 -
[TDD - Outside-in with Wicket and Scala-part 2]
24-12-2018 -
[TDD - Outside-in with Wicket and Scala-part 1]
04-12-2018 -
[Floating Point library in m68k Assembler on Amiga]
09-08-2018 -
[Cloning Compact Flash (CF) card for Amiga]
25-12-2017 -
[Writing tests is not the same as writing tests]
08-12-2017 -
[Dependency Injection in Objective-C... sort of]
20-01-2011