In a clustered environment, it is sometimes necessary to limit to one instance of an EJB or CDI bean for an entire cluster.
Currently in Java EE specification (soon to be Jakarta EE), there is no way to accomplish this. Payara Server and Payara Micro's Clustered Singleton feature aims to easily fill in this gap.
How to Implement Clustered Singleton
The code below demonstrates simple clustered singleton for both EJB and CDI beans:
How to Make an Existing Singleton Clustered
- Add @Clustered to @Singleton or @ApplicationScoped bean
- Make the bean Serializable
Availability / Payara Platform Versions
Clustered Singletons are available in the Payara Platform 4.182 and Payara Platform 5.181 and later.
How it Works
The @Singleton / @ApplicationScoped bean is deployed and is available to all members in the cluster. Prior to every method invocation, latest version of the bean is retrieved from a Hazelcast map. After every method invocation, possibly updated version of the bean is stored in a Hazelcast map. During this process, a Hazelcast distributed lock for the bean is optionally acquired, so there is only one method call can be executed in a particular cluster at-a-time, so no data corruption / race conditions are possible.
Differences From Other Vendor's Implementations
The main difference is that the Payara Platform Clustered Singleton implementation stores the bean in Hazelcast map across the cluster. This makes it easier to manage and eliminates the need to administer nodes that the bean is deployed to, and managing primary / backup nodes is not necessary with the Payara Platform. Other vendors have configuration requirements where to deploy the singleton, and a list of backup nodes that requires management.
Examples and Documentation
Examples can be found here: clustered-singleton
Documentation can be found here: https://docs.payara.fish/documentation/payara-server/public-api/clustered-singleton.html
Locking and Distributed Locking
|@Clustered(lock = DistributedLockType.INHERIT)||Yes||For EJBs, this setting enables the lock by default, or follows the @ConcurrencyManagement setting for the bean|
|@Clustered(lock = DistributedLockType.LOCK)||No||distributed lock is enabled, not applicable to EJBs|
|@Clustered(lock = DistributedLockType.LOCK_NONE)||No||distributed lock is disabled|
Other @Clustered Annotation Parameters
|@Clustered(callPostConstructOnAttach = true|false)||true||Controls whether more than one instance calls postConstruct()|
|@Clustered(callPreDestoyOnDetach = true|false)||true||controls whether more than one instance calls preDestroy()|
|@Clustered(keyName = "XXX")||bean name||sets the key for the Hazelcast map under which the bean is stored|
@Clustered and EJB Timers
With persistent timers (default), they execute only on a single node in a cluster, so they work correctly with Clustered Singletons
@Clustered and @Startup / @Initialized @ApplicationScoped
Care must be taken in this scenario. By default, @PostConsctuct is called for every instance in the cluster for these beans. This behavior needs to be disabled by using @Clustered(callPostConstructOnAttach = false) and possibly callPreDestoyOnDetach = false as well, so only the @PostConstruct method will be called only once per cluster. @Observed @Initialized methods are called for every instance in the cluster. The only valid method for cluster-wide initialization is @PostConstruct method configured as described above.
@Clustered and Transactions
Transactions behave the same way for Clustered beans as non-clustered. Since all calls are local, and locks work in the same way, there is no behavioural differences.
CDI Considerations and Notes
Since there are no default locks for CDI, @Clustered CDI @ApplicationScoped beans do not have distributed locks on by default.
It is highly recommended to enable distributed locks for CDI Clustered beans: @Clustered(lock = DistributedLockType.LOCK) so there is no chance of race condition / data corruption for CDI Clustered beans.
Requirements and Assumptions
- Hazelcast is required to be enabled (default on Payara Platform 5)
- @Clustered only works with @ApplicationScoped or @Singleton annotation, and in no other circumstance
- All fields in the @Clustered @Singleton class need to be correctly Serializable
- Works correctly only if accessed via @Injected proxy, not through direct access
- Works with EJB timers correctly only if all timers are persistent (default)
- All method calls to @Clustered beans operate on a local instance
- Data corruption is possible if distributed lock is turned off and there is a race condition between method calls to the same bean on multiple cluster nodes simultaneously. In such a case, last method call wins the singleton state
- Clustered singletons are stored in Hazelcast map under "Payara/(EJB/CDI)/singleton/<singletonComponentName>/[lock]"
EJB XML configuration
glassfish-ejb-jar.xml have additional elements described below. The elements have the same meaning and work the same way as annotation parameters described above.
Clustered Singleton elements are added to glassfish-ejb-jar.xml as follows: