Skip to content

This bundle provides a plugin that expires session objects in a given map and their relevant entries in other Hazelcast maps.

License

Notifications You must be signed in to change notification settings

padogrid/bundle-hazelcast-4n5-cluster-session

Repository files navigation

PadoGrid PadoGrid | Catalogs | Manual | FAQ | Releases | Templates | Pods | Kubernetes | Docker | Apps | Quick Start


PadoGrid 1.x Host OS VM Docker Kubernetes

Session Expiration Management Plugin

This bundle provides a plugin that expires session objects in a given map and their relevant entries in other Hazelcast maps. The plugin also supports session expirations over the WAN (See bundle-hazelcast-4n5-cluster-session-wan.)

Installing Bundle

install_bundle -download bundle-hazelcast-4n5-cluster-session

Use Case

You are storing user session objects in multiple Hazelcast maps. A single user session touches one or more maps and when the user is idle for some time, you want to end the session and remove all the entries belonging to that particular session from all the relevant maps. To achieve this, you decide to embed the session ID (typically UUID) as a part of all the keys that you are storing in the maps. You also configure the maps with max-idle-seconds, but since each map times out individually, the session entries would expire undeterministically, i.e., some entries may expire earlier than others if there were no activities. Your task is to deterministcally expire all the entries from the relevant maps at the same time when the session times out.

Session Flow


SessionExpirationService

The SessionExpirationService plugin solves this use case by adding an EntryExpiredListener to the primary map that gets updated for all session activities. If an entry expires in the primary map, then the listener also expires (or removes) that session's entries from all the relevant maps. To lighten the load on the cluster, SessionExpirationService creates a thread pool from which a thread is assigned to a blocking queue that takes on the removal tasks. The default thread pool size is one (1) and configurable using the property, hazelcast.addon.cluster.expiration.thread.pool-size.

This plugin adds extra latency introduced by the EntryExpiredListener instance and generates additional network traffic triggered by entry removals. Within a single cluster, the additional listener latency and network traffic maybe tolerable. However, for multiple custers with WAN replication enabled, the network traffic may not be so tolerable as the entry removal events generated by IMap.delete() replicate over the WAN. If you have bidirectional WAN-enabled clusters, then multiple delete events of the same entries may occur from both clusters. Unfortunately, as of writing, Hazelcast does not provide an API that suppresses events.

SessionExpirationService_Get

The SessionExpirationService_Get plugin adds an EntryListener to the primary map that gets updated for all session activities. Instead of removing session entries as in SessionExpirationService, the SessionExpirationService_Get plugin resets the max idle time of all the specified session relevant entries from the pattern matching maps by reading (getting) session entries. For this plugin to work properly, the relevant maps' max idle time should be slightly higer than the primary map such that the relevant map entries do not expire prematurely.

This plugin eliminates the additional WAN traffic but may add a significant amount of upfront latency. Making an update to the primary map eventually invokes IMap.get() on all the relevant maps, which even though asynchrously executed, increases the overall latency. This is quite evident when you conduct load tests. To reduce this latency, you may need to use IMap.setAsync() instead of IMap.set().


Both SessionExpirationService and SessionExpirationService_Get plugins support the following key types for building predicates to remove entries from the relevant maps. Both primary map and relevant maps must be configured with the same key type.

KeyType Description
INTERFACE All key classes must implement the ISessionId interface
OBJECT All key classes must implement the user-specified property (getter method)
CUSTOM A predicate class that implements the ISessionIdPredicate interface must be provided
PARTITION_AWARE All key classes must implement the PartitionAware interface. It uses PartitionAware.getPartitionKey() as the session ID.
STRING If key type is not defined or any of the other key types fails, then the plugin defaults to the STRING key type. It applies the delimiter to extract the last token in the string value returned from toString(). The extracted token is the session ID. The default delimiter is '@'.

❗️ The use of the STRING key type in production is strongly discouraged. It is very expensive in terms of speed and CPU due to its use of LIKE predicates for searching the relevant maps.

The predicates are built as follows (See source code: SessionExpirationService.java):

INTERFACE

String sessionId = ((ISessionId) sessionInfo.key).getSessionId();
predicate = Predicates.equal("__key.sessionId", sessionId);

OBJECT

Method method = sessionInfo.key.getClass().getMethod(sessionData.getterMethodName);
Object sessionId = method.invoke(sessionInfo.key);
if (sessionId != null) {
    predicate = Predicates.equal("__key." + sessionData.keyProperty, sessionId.toString());
}

CUSTOM

predicate = sessionData.sessionIdPredicate.getPredicate(sessionInfo.sessionMapName,
sessionInfo.key);

PARTITION_AWARE

Object partitionKey = ((PartitionAware) sessionInfo.key).getPartitionKey();
if (partitionKey != null) {
    predicate = Predicates.equal("__key.partitionKey", partitionKey.toString());
}

STRING

String keyStr = sessionInfo.key.toString();
int index = keyStr.lastIndexOf(delimiter);
String sessionId;
if (index == -1) {
    sessionId = keyStr;
} else {
    sessionId = keyStr.substring(index + delimiter.length());
}
if (sessionId.length() != 0) {
    predicate = Predicates.like("__key", "%" + sessionId);
}

❗️ Make sure to index your keys to get the optimal predicate query performance. See the examples shown below.


SessionExpirationService_SessionMetadata_Delete

Unlike the previous plugins which automatically determine the relevant map entries to remove, the SessionExpirationService_SessionMetadata_Delete plugin requires the application to provide the relevant map entry removal information in the form of SessonMetadata. Since it does not search the relevant maps to remove entries, it has better expiration performance.

SessionExpirationService_SessionMetadata_Get

The SessionExpirationService_SessionMetadata_Get plugin also requires the application to provide the metadata, but it resets the idle timeout like SessionExpirationService_Get.


Installation Steps

Run the session cluster's build_app script which builds the expiration plugin jar packages and sets the correct Hazelcast XML schema version in the etc/hazelcast.xml file. It places the plugin jars in the workspace plugins directory.

switch_cluster session/bin_sh
./build_app

Startup Sequence

  1. Start the session cluster.
switch_cluster session

# Add two (2) or more members in the cluster
add_member -count 2

# Start the session cluster and management center
start_cluster -all
  1. Ingest data. The following maps have been preconfigured to timeout in 5 seconds. See hazelcast.yaml.
PrimaryMap Relevant Maps KeyType
smki_%TAG% mki1_%TAG%,mki2_%TAG% INTERFACE
smko_%TAG% mko1_%TAG%,mko2_%TAG% OBJECT
smkc_%TAG% mkc1_%TAG%,mkc2_%TAG% CUSTOM
smkp_%TAG% mkp1_%TAG%,mkp2_%TAG% PARTITION_AWARE
smks_%TAG% mks1_%TAG%,mks2_%TAG% STRING
mkp_session_web_session_fi_session_id_mapping_%TAG% mkp_session_fi_session_data_%TAG%,mkp_session_application_data_%TAG% OBJET
cd_cluster session/bin_sh

# Print usage
./test_session_ingestion -?

# INTERFACE: Ingest InterfaceKey that implements ISessionId into
#            smki_EN01, mki1_EN01, mki2_EN02
./test_session_ingestion -type INTERFACE -primary smki_EN01 -relevant mki1_EN01,mki2_EN01

# OBJECT: Ingest objects (ObjectKey) with the getSessionId() method into
#         smko_EN01, mko1_EN01, mko2_EN02
# The key property, sessionId, is specified in the cluster config file.
./test_session_ingestion -type OBJECT -primary smko_EN01 -relevant mko1_EN01,mko2_EN01

# CUSTOM: Ingest CustomKey objects into
#         smkc_EN01, mkc_EN01, mkc2_EN02
# SessionID is extracted by CustomPredicate specified in the cluster config file.
./test_session_ingestion -type CUSTOM -primary smkc_EN01 -relevant mkc1_EN01,mkc2_EN01

# PARTITION_AWARE: Ingest PartitionAwareKey that implements PartitionAware into
#        smkp_EN01, mkp1_EN01, mkp2_EN02
#        It uses PartitionAware.getPartionKey() as the session ID.
./test_session_ingestion -type PARTITION_AWARE -primary smkp_EN01 -relevant mkp1_EN01,mkp2_EN01

# STRING: Ingest String keys into
#         smks_EN01, mks_EN01, mks2_EN02
# SessionID is extracted from key objects using the specified delimiter in the cluster
# config file. The default delimiter is '@' and the session ID is the last part of the key.
./test_session_ingestion -type STRING -primary smks_EN01 -relevant mks1_EN01,mks2_EN01

# OBJECT: Ingest objects (ObjectKey) with the getSessionId() method into 
#         mkp_session_web_session_fi_session_id_mapping_EN0, 
#         mmkp_session_fi_session_data_EN01, 
#         mkp_session_application_data_EN0
# The key property, sessionId, is specified in the cluster config file.
./test_session_ingestion -type OBJECT -primary mkp_session_web_session_fi_session_id_mapping_EN01 -relevant mkp_session_fi_session_data_EN01,mkp_session_application_data_EN01
  1. Monitor the maps from the management center.

URL: http://localhost:8080/hazelcast-mancenter

Configuring SessionExpirationService

There are three (3) distinctive settings that must be included in the Hazelcast configuration file as follows.

  1. Properties for defining the primary map, relevant maps, and session key delimiter.
  2. org.hazelcast.addon.cluster.expiration.SessionExpirationServiceInitializer for initializing and starting the SessionExpirationService plugin.
  3. org.hazelcast.addon.cluster.expiration.SessionExpirationListener for each primary map configured with max-idle-seconds or time-to-live-seconds.
Property Description Default
hazelcast.addon.cluster.expiration.tag Tag used as a prefix to each log message and a part of JMX object name. SessionExpirationService
hazelcast.addon.cluster.expiration.jmx-use-hazelcast-object-name If true, then the standard Hazelcast JMX object name is registered for the session expiration service. Hazelcast metrics are registered with the header “com.hazelcast” and “type=Metrics”. If false or unspecified, then object name is registered with the header “org.hazelcast.addon” and “type=SessionExpirationService”. false
hazelcast.addon.cluster.expiration.key.delimiter Delimiter that separates key string and the sessionID. The sessionID is always at the tail end of the string value. @
hazelcast.addon.cluster.expiration.thread.pool-size Expiration thread pool size. Each thread has polls a blocking queue. 1
hazelcast.addon.cluster.expiration.queue.drain-size Property for setting the expiration drain size. Each expiration event is placed in a blocking queue that is drained by a separate worker thread to process them. The worker thread drains the queue based on this value and processes the expiration events in a batch at a time to provide better performance. Note that a large drain size will throw stack overflow exceptions for KeyType.STRING which ends up building lengthy OR predicates with LIKE conditions. Hazelcast appears to have undocumented limitations on lengthy predicates. 100
hazelcast.addon.cluster.expiration.string-key.postfix.enabled Property for enabling or disabling session ID postfix for String keys. false
hazelcast.addon.cluster.expiration.session. Property prefix for specifying a session map and the relevant maps. N/A
hazelcast.addon.cluster.expiration.session.foo%TAG%yong Primary map name that begins with "foo" and ends with "yong" with the pattern matcher %TAG% in between. This property's value must be a comma separated list of relevant map names with zero or more %TAG% and optional regex. See examples below. N/A
hazelcast.addon.cluster.expiration.session.foo%TAG%yong.key.type Key type. Valid types are CUSTOM, INTERFACE, OBJECT, PARTITION_AWARE, and STRING. STRING
hazelcast.addon.cluster.expiration.session.foo%TAG%yong.key.property Key property. The key class' "get" method that returns the session ID. N/A
hazelcast.addon.cluster.expiration.session.foo%TAG%yong.key.predicate Predicate class name. Applies to the CUSTOM key type only. N/A

✏️ %TAG% is a special replacement annotation that makes an exact match of its position in the string value. Regular expression is supported for listing relevant map names.

Example 1:

hazelcast.addon.cluster.expiration.session.foo%TAG%yong: abc_%TAG%,xyz_%TAG%,mymap

The above example matches the following map names.

Primary Map Relevant Maps
fooEN01yong abc_EN01, xyz_EN01, mymap
fooEN02yong abc_EN02, xyz_EN02, mymap

Example 2 (regex):

hazelcast.addon.cluster.expiration.session.foo%TAG%yong: abc_%TAG%_.*_xyz

The above example matches the following map names.

Primary Map Relevant Maps
fooEN01yong abc_EN01_a_xyz, abc_EN01_ab_xyz, abc_EN01_aaaa_xyz
foo_EN02_yong abc__EN02__a_xyz, abc__EN02__ab_xyz, abc__EN02__aaaa_xyz

Use the following configuration files as references. Note that there are two (2) sets of OBJECT maps: smko_* and mkp_session_web_session_fi_session_id_mapping_*. The relevant maps of the latter have been configured with an index to provide better performance.

hazelcast:
  ...
  properties:
    hazelcast.phone.home.enabled: false
    hazelcast.addon.cluster.expiration.tag: SessionExpirationService
    hazelcast.addon.cluster.expiration.key.delimiter: "@"

    # INTERFACE expects key classes to implement the ISessionId interface.
    hazelcast.addon.cluster.expiration.session.smki_%TAG%: mki1_%TAG%,mki2_%TAG%
    hazelcast.addon.cluster.expiration.session.smki_%TAG%.key.type: INTERFACE

    # OBJECT expects key classes with the specified property (getter method).
    hazelcast.addon.cluster.expiration.session.smko_%TAG%: mko1_%TAG%,mko2_%TAG%
    hazelcast.addon.cluster.expiration.session.smko_%TAG%.key.type: OBJECT
    hazelcast.addon.cluster.expiration.session.smko_%TAG%.key.property: sessionId

    # CUSTOM expects an predicate class that implements the ISessionIdPredicate interface.
    hazelcast.addon.cluster.expiration.session.smkc_%TAG%: mkc1_%TAG%,mkc2_%TAG%
    hazelcast.addon.cluster.expiration.session.smkc_%TAG%.key.type: CUSTOM
    hazelcast.addon.cluster.expiration.session.smkc_%TAG%.key.predicate: org.hazelcast.addon.cluster.expiration.test.CustomPredicate

    # PARTITION_AWARE expects key classes to implement the PartitionAware interface.
    # It uses PartitionAware.getPartitionKey() as the session ID.
    hazelcast.addon.cluster.expiration.session.smkp_%TAG%: mkp1_%TAG%,mkp2_%TAG%
    hazelcast.addon.cluster.expiration.session.smkp_%TAG%.key.type: PARTITION_AWARE

    # STRING uses string values of key objects, i.e. toString(). It applies the
    # delimiter to extract the last token in the string value as the session ID.
    hazelcast.addon.cluster.expiration.session.smks_%TAG%: mks1_%TAG%,mks2_%TAG%
    hazelcast.addon.cluster.expiration.session.smks_%TAG%.key.type: STRING

    # OBJECT
    hazelcast.addon.cluster.expiration.session.mkp_session_web_session_fi_session_id_mapping_%TAG%: mkp_session_fi_session_data_%TAG%,mkp_session_application_data_%TAG%
    hazelcast.addon.cluster.expiration.session.mkp_session_web_session_fi_session_id_mapping_%TAG%.key.type: OBJECT
    hazelcast.addon.cluster.expiration.session.mkp_session_web_session_fi_session_id_mapping_%TAG%.key.property: sessionId

  listeners:
    # SessionExpirationServiceInitializer is invoked during bootstrap to
    # initialize and start SessionExpirationService.
    - org.hazelcast.addon.cluster.expiration.SessionExpirationServiceInitializer
  ...
  map:
    # INTERFACE
    smki_*:
      max-idle-seconds: 5
      entry-listeners:
        - class-name: org.hazelcast.addon.cluster.expiration.SessionExpirationListener
          include-value: false

    # OBJECT
    smko_*:
      max-idle-seconds: 5
      entry-listeners:
        - class-name: org.hazelcast.addon.cluster.expiration.SessionExpirationListener
          include-value: false

    # CUSTOM
    smkc_*:
      max-idle-seconds: 5
      entry-listeners:
        - class-name: org.hazelcast.addon.cluster.expiration.SessionExpirationListener
          include-value: false

    # PARTITION_AWARE
    smkp_*:
      max-idle-seconds: 5
      entry-listeners:
        - class-name: org.hazelcast.addon.cluster.expiration.SessionExpirationListener
          include-value: false

    # STRING
    smks_*:
      max-idle-seconds: 5
      entry-listeners:
        - class-name: org.hazelcast.addon.cluster.expiration.SessionExpirationListener
          include-value: false

    # OBJECT
    mkp_session_web_session_fi_session_id_mapping_*:
      max-idle-seconds: 5
      entry-listeners:
        - class-name: org.hazelcast.addon.cluster.expiration.SessionExpirationListener
          include-value: false

    # Index relevant maps for better performance
    mkp_session_fi_session_data_*:
      indexes:
        - type: HASH
          attributes:
            - "__key.sessionId"

    # Index relevant maps for better performance
    mkp_session_application_data_*:
      indexes:
        - type: HASH
          attributes:
            - "__key.sessionId"
<hazelcast ...>
	<properties>
		<property name="hazelcast.phone.home.enabled">false</property>
		<property name="hazelcast.addon.cluster.expiration.tag">SessionExpirationService</property>
		<property name="hazelcast.addon.cluster.expiration.jmx-use-hazelcast-object-name">true</property>
		<property name="hazelcast.addon.cluster.expiration.key.delimiter">@</property>

		<!-- INTERFACE expects key classes to implement the ISessionId interface. -->
		<property name="hazelcast.addon.cluster.expiration.session.smki_%TAG%">mki1_%TAG%,mki2_%TAG%</property>
		<property name="hazelcast.addon.cluster.expiration.session.smki_%TAG%.key.type">INTERFACE</property>

		<!-- OBJECT expects key classes with the specified property (getter method). -->
		<property name="hazelcast.addon.cluster.expiration.session.smko_%TAG%">mko1_%TAG%,mko2_%TAG%</property>
		<property name="hazelcast.addon.cluster.expiration.session.smko_%TAG%.key.type">OBJECT</property>
		<property name="hazelcast.addon.cluster.expiration.session.smko_%TAG%.key.property">sessionId</property>

		<!-- CUSTOM expects a predicate class that implements the ISessionIdPredicate interface. -->
		<property name="hazelcast.addon.cluster.expiration.session.smkc_%TAG%">mkc1_%TAG%,mkc2_%TAG%</property>
		<property name="hazelcast.addon.cluster.expiration.session.smkc_%TAG%.key.type">CUSTOM</property>
		<property name="hazelcast.addon.cluster.expiration.session.smkc_%TAG%.key.predicate">org.hazelcast.addon.expiration.test.CustomPredicate</property>

		<!-- PARTITION_AWARE expects key classes to implement the PartitionAware interface.
			It uses PartitionAware.getPartitionKey() as the session ID. -->
		<property name="hazelcast.addon.cluster.expiration.session.smkp_%TAG%">mkp1_%TAG%,mkp2_%TAG%</property>
		<property name="hazelcast.addon.cluster.expiration.session.smkp_%TAG%.key.type">PARTITION_AWARE</property>

		<!-- STRING uses string values of key objects, i.e. toString(). It applies the
			delimiter to extract the last token in the string value as the session ID. -->
		<property name="hazelcast.addon.cluster.expiration.session.smks_%TAG%">mks1_%TAG%,mks2_%TAG%</property>
		<property name="hazelcast.addon.cluster.expiration.session.smks_%TAG%.key.type">STRING</property>

		<!-- OBJECT -->
		<property name="hazelcast.addon.cluster.expiration.session.mkp_session_web_session_fi_session_id_mapping_%TAG%">mkp_session_fi_session_data_%TAG%,mkp_session_application_data_%TAG%</property>
		<property name="hazelcast.addon.cluster.expiration.session.mkp_session_web_session_fi_session_id_mapping_%TAG%.key.type">OBJECT</property>
		<property name="hazelcast.addon.cluster.expiration.session.mkp_session_web_session_fi_session_id_mapping_%TAG%.key.property">sessionId</property>

	</properties>
    <listeners>
        <!-- org.hazelcast.addon.cluster.expiration.SessionExpirationServiceInitializer
         is invoked during bootstrap to initialize SessionExpirationService.
         -->
        <listener>
		org.hazelcast.addon.cluster.expiration.SessionExpirationServiceInitializer
		</listener>
    </listeners>
	...
	<!-- INTERFACE -->
	<map name="smki_*">
		<max-idle-seconds>5</max-idle-seconds>
		<entry-listeners>
			<entry-listener include-value="false">org.hazelcast.addon.cluster.expiration.SessionExpirationListener</entry-listener>
		</entry-listeners>
	</map>

	<!-- OBJECT -->
	<map name="smko_*">
		<max-idle-seconds>5</max-idle-seconds>
		<entry-listeners>
			<entry-listener include-value="false">org.hazelcast.addon.cluster.expiration.SessionExpirationListener</entry-listener>
		</entry-listeners>
	</map>

	<!-- CUSTOM -->
	<map name="smkc_*">
		<max-idle-seconds>5</max-idle-seconds>
		<entry-listeners>
			<entry-listener include-value="false">org.hazelcast.addon.cluster.expiration.SessionExpirationListener</entry-listener>
		</entry-listeners>
	</map>

	<!-- PARTITION_AWARE -->
	<map name="smkp_*">
		<max-idle-seconds>5</max-idle-seconds>
		<entry-listeners>
			<entry-listener include-value="false">org.hazelcast.addon.cluster.expiration.SessionExpirationListener</entry-listener>
		</entry-listeners>
	</map>

	<!-- STRING -->
	<map name="smks_*">
		<max-idle-seconds>5</max-idle-seconds>
		<entry-listeners>
			<entry-listener include-value="false">org.hazelcast.addon.cluster.expiration.SessionExpirationListener</entry-listener>
		</entry-listeners>
	</map>

	<!-- OBJECT -->
	<map name="mkp_session_web_session_fi_session_id_mapping_*">
		<max-idle-seconds>5</max-idle-seconds>
		<entry-listeners>
			<entry-listener include-value="false">org.hazelcast.addon.cluster.expiration.SessionExpirationListener</entry-listener>
		</entry-listeners>
	</map>

	<!-- Index relevant maps for better performance -->
	<map name="mkp_session_fi_session_data_*">
		<indexes>
			<index type="HASH">
				<attributes>
					<attribute>__key.sessionId</attribute>
				</attributes>
			</index>
		</indexes>
	</map>

	<!-- Index relevant maps for better performance -->
	<map name="mkp_session_application_data_*">
		<indexes>
			<index type="HASH">
				<attributes>
					<attribute>__key.sessionId</attribute>
				</attributes>
			</index>
		</indexes>
	</map>
</hazelcast>

PadoGrid PadoGrid | Catalogs | Manual | FAQ | Releases | Templates | Pods | Kubernetes | Docker | Apps | Quick Start

About

This bundle provides a plugin that expires session objects in a given map and their relevant entries in other Hazelcast maps.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published