From f2443f5e21b64c193cd2c1e4c4d27d1f9f7b20ce Mon Sep 17 00:00:00 2001 From: Mark Anderson Date: Mon, 1 Feb 2016 19:10:09 +0000 Subject: [PATCH] Support External Hazelcast instance Previously the Hazelcast support only worked with embedded Hazelcast instances. This commit ensures that Hazelcast support works with external Hazelcast instances. Fixes gh-339 --- .../asciidoc/guides/hazelcast-spring.adoc | 14 +-- docs/src/docs/asciidoc/index.adoc | 7 +- .../src/main/java/sample/Config.java | 2 +- ...EnableHazelcastHttpSessionEventsTests.java | 25 +++- ...lcastHttpSessionConfigurationXmlTests.java | 79 +------------ .../web/http/EnableHazelcastHttpSession.java | 5 +- .../HazelcastHttpSessionConfiguration.java | 107 ++++++++++++------ 7 files changed, 107 insertions(+), 132 deletions(-) diff --git a/docs/src/docs/asciidoc/guides/hazelcast-spring.adoc b/docs/src/docs/asciidoc/guides/hazelcast-spring.adoc index a78d055..95e304e 100644 --- a/docs/src/docs/asciidoc/guides/hazelcast-spring.adoc +++ b/docs/src/docs/asciidoc/guides/hazelcast-spring.adoc @@ -81,7 +81,7 @@ The filter is what is in charge of replacing the `HttpSession` implementation to In this instance Spring Session is backed by Hazelcast. <2> We create a `HazelcastInstance` that connects Spring Session to Hazelcast. By default, an embedded instance of Hazelcast is started and connected to by the application. -For more information on configuring Hazelcast, refer to the http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#hazelcast-configuration[reference documentation]. +For more information on configuring Hazelcast, refer to the http://docs.hazelcast.org/docs/latest/manual/html-single/index.html#hazelcast-configuration[reference documentation]. == Servlet Container Initialization @@ -126,7 +126,7 @@ You can run the sample by obtaining the {download-url}[source code] and invoking ==== Hazelcast will run in embedded mode with your application by default, but if you want to connect to a stand alone instance instead, you can configure it by following the instructions in the -http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#hazelcast-configuration[reference documentation]. +http://docs.hazelcast.org/docs/latest/manual/html-single/index.html#hazelcast-configuration[reference documentation]. ==== ---- @@ -157,9 +157,9 @@ Go ahead and view the cookies (click for help with https://developer.chrome.com/ === Interact with the data store -If you like, you can remove the session using http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#hazelcast-java-client[a Java client], -http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#other-client-implementations[one of the other clients], or the -http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#management-center[management center]. +If you like, you can remove the session using http://docs.hazelcast.org/docs/latest/manual/html-single/index.html#hazelcast-java-client[a Java client], +http://docs.hazelcast.org/docs/latest/manual/html-single/index.html#other-client-implementations[one of the other clients], or the +http://docs.hazelcast.org/docs/latest/manual/html-single/index.html#management-center[management center]. ==== Using the console @@ -168,7 +168,7 @@ For example, using the management center console after connecting to your Hazelc default> ns spring:session:sessions spring:session:sessions> m.clear -TIP: The Hazelcast documentation has instructions for http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#console[the console]. +TIP: The Hazelcast documentation has instructions for http://docs.hazelcast.org/docs/latest/manual/html-single/index.html#executing-console-commands[the console]. Alternatively, you can also delete the explicit key. Enter the following into the console ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie: @@ -179,7 +179,7 @@ Now visit the application at http://localhost:8080/ and observe that we are no l ==== Using the REST API As described in the other clients section of the documentation, there is a -http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#rest-client[REST API] +http://docs.hazelcast.org/docs/latest/manual/html-single/index.html#rest-client[REST API] provided by the Hazelcast node(s). For example, you could delete an individual key as follows (replacing `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie): diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 2b27542..25e9efc 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -513,7 +513,7 @@ include::{docs-test-dir}docs/http/HazelcastHttpSessionConfig.java[tags=config] ---- This will configure Hazelcast in embedded mode with default configuration. -See the http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#hazelcast-configuration[Hazelcast documentation] for +See the http://docs.hazelcast.org/docs/latest/manual/html-single/index.html#hazelcast-configuration[Hazelcast documentation] for detailed information on configuration options for Hazelcast. [[api-enablehazelcasthttpsession-storage]] @@ -521,8 +521,9 @@ detailed information on configuration options for Hazelcast. Sessions will be stored in a distributed `Map` in Hazelcast using a <>. The `Map` interface methods will be used to `get()` and `put()` Sessions. -The expiration of a session in the `Map` is handled by Hazelcast's native support for configuring a map's `max-idle-seconds` setting. -Entries (sessions) that have been idle longer than the max idle setting will be automatically removed from the `Map`. +The expiration of a session in the `Map` is handled by Hazelcast's support for setting the time to live on an entry when it is `put()` into the `Map`. Entries (sessions) that have been idle longer than the time to live will be automatically removed from the `Map`. + +You shouldn't need to configure any settings such as `max-idle-seconds` or `time-to-live-seconds` for the `Map` within the Hazelcast configuration. [[api-enablehazelcasthttpsession-customize]] ==== Basic Customization diff --git a/samples/hazelcast-spring/src/main/java/sample/Config.java b/samples/hazelcast-spring/src/main/java/sample/Config.java index 343359e..4fcbcf1 100644 --- a/samples/hazelcast-spring/src/main/java/sample/Config.java +++ b/samples/hazelcast-spring/src/main/java/sample/Config.java @@ -26,7 +26,7 @@ import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; // tag::class[] -@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = "300") +@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = 300) @Configuration public class Config { diff --git a/spring-session/src/integration-test/java/org/springframework/session/hazelcast/config/annotation/web/http/EnableHazelcastHttpSessionEventsTests.java b/spring-session/src/integration-test/java/org/springframework/session/hazelcast/config/annotation/web/http/EnableHazelcastHttpSessionEventsTests.java index facde1c..aa88bb1 100644 --- a/spring-session/src/integration-test/java/org/springframework/session/hazelcast/config/annotation/web/http/EnableHazelcastHttpSessionEventsTests.java +++ b/spring-session/src/integration-test/java/org/springframework/session/hazelcast/config/annotation/web/http/EnableHazelcastHttpSessionEventsTests.java @@ -59,8 +59,7 @@ import com.hazelcast.core.HazelcastInstance; @WebAppConfiguration public class EnableHazelcastHttpSessionEventsTests { - private final static String MAX_INACTIVE_INTERVAL_IN_SECONDS_STR = "1"; - private final static int MAX_INACTIVE_INTERVAL_IN_SECONDS = Integer.valueOf(MAX_INACTIVE_INTERVAL_IN_SECONDS_STR); + private final static int MAX_INACTIVE_INTERVAL_IN_SECONDS = 1; @Autowired private SessionRepository repository; @@ -143,9 +142,29 @@ public class EnableHazelcastHttpSessionEventsTests { assertThat(repository.getSession(sessionToSave.getId())).isNull(); } + @Test + public void saveUpdatesTimeToLiveTest() throws InterruptedException { + S sessionToSave = repository.createSession(); + + repository.save(sessionToSave); + + synchronized (lock) { + lock.wait((sessionToSave.getMaxInactiveIntervalInSeconds() * 1000) - 500); + } + + // Get and save the session like SessionRepositoryFilter would. + S sessionToUpdate = repository.getSession(sessionToSave.getId()); + repository.save(sessionToUpdate); + + synchronized (lock) { + lock.wait((sessionToUpdate.getMaxInactiveIntervalInSeconds() * 1000) - 100); + } + + assertThat(repository.getSession(sessionToUpdate.getId())).isNotNull(); + } @Configuration - @EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS_STR) + @EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS) static class HazelcastSessionConfig { @Bean diff --git a/spring-session/src/integration-test/java/org/springframework/session/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfigurationXmlTests.java b/spring-session/src/integration-test/java/org/springframework/session/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfigurationXmlTests.java index 6838dbf..a5b2c43 100644 --- a/spring-session/src/integration-test/java/org/springframework/session/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfigurationXmlTests.java +++ b/spring-session/src/integration-test/java/org/springframework/session/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfigurationXmlTests.java @@ -43,59 +43,6 @@ import com.hazelcast.core.HazelcastInstance; */ public class HazelcastHttpSessionConfigurationXmlTests { - @RunWith(SpringJUnit4ClassRunner.class) - @ContextConfiguration - @WebAppConfiguration - public static class CustomXmlInactiveIntervalTest { - - @Autowired - private SessionRepository repository; - - @Autowired - private HazelcastInstance hazelcast; - - @Test - public void saveSessionTest() throws InterruptedException { - - S sessionToSave = repository.createSession(); - - assertThat(sessionToSave.getMaxInactiveIntervalInSeconds()) - .isEqualTo(150); - - repository.save(sessionToSave); - - S session = repository.getSession(sessionToSave.getId()); - - assertThat(session.getId()).isEqualTo(sessionToSave.getId()); - assertThat(session.getMaxInactiveIntervalInSeconds()) - .isEqualTo(150); - } - - @Test - public void checkUnderlyingMapSettingsTest() { - assertThat( - hazelcast.getConfig() - .getMapConfig("spring:session:sessions") - .getMaxIdleSeconds()) - .isEqualTo(150); - } - - @Configuration - @EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = "") - static class HazelcastSessionXmlConfigCustomIdle { - - @Bean - public HazelcastInstance embeddedHazelcast() { - Config hazelcastConfig = new ClasspathXmlConfig( - "org/springframework/session/hazelcast/config/annotation/web/http/hazelcast-custom-idle-time.xml"); - NetworkConfig netConfig = new NetworkConfig(); - netConfig.setPort(SocketUtils.findAvailableTcpPort()); - hazelcastConfig.setNetworkConfig(netConfig); - return Hazelcast.newHazelcastInstance(hazelcastConfig); - } - } - } - @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @WebAppConfiguration @@ -104,9 +51,6 @@ public class HazelcastHttpSessionConfigurationXmlTests repository; - @Autowired - private HazelcastInstance hazelcast; - @Test public void saveSessionTest() throws InterruptedException { @@ -120,15 +64,6 @@ public class HazelcastHttpSessionConfigurationXmlTests repository; - @Autowired - private HazelcastInstance hazelcast; - @Test public void saveSessionTest() throws InterruptedException { @@ -169,17 +101,8 @@ public class HazelcastHttpSessionConfigurationXmlTestsIf you wish to use external configuration (outside of this annotation) to set this value, you can - * set this to "" (an empty String), which will prevent this configuration from overriding - * the external configuration for this value.

* * @return the seconds a session can be inactive before expiring */ - String maxInactiveIntervalInSeconds() default "1800"; + int maxInactiveIntervalInSeconds() default 1800; /** * This is the name of the Map that will be used in Hazelcast to store the session data. diff --git a/spring-session/src/main/java/org/springframework/session/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfiguration.java b/spring-session/src/main/java/org/springframework/session/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfiguration.java index a31a9dc..a09e37c 100644 --- a/spring-session/src/main/java/org/springframework/session/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfiguration.java +++ b/spring-session/src/main/java/org/springframework/session/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfiguration.java @@ -15,7 +15,10 @@ */ package org.springframework.session.hazelcast.config.annotation.web.http; +import java.util.Collection; import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.annotation.PreDestroy; @@ -32,7 +35,6 @@ import org.springframework.session.config.annotation.web.http.SpringHttpSessionC import org.springframework.session.hazelcast.SessionEntryListener; import org.springframework.session.web.http.SessionRepositoryFilter; -import com.hazelcast.config.MapConfig; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.IMap; @@ -48,10 +50,6 @@ import com.hazelcast.core.IMap; @Configuration public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware { - /** This is the magic value to use if you do not want this configuration - * overriding the maxIdleSeconds value for the Map backing the session data. */ - private static final String DO_NOT_CONFIGURE_INACTIVE_INTERVAL_STRING = ""; - private Integer maxInactiveIntervalInSeconds = 1800; private String sessionMapName = "spring:session:sessions"; @@ -62,11 +60,10 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur @Bean public SessionRepository sessionRepository(HazelcastInstance hazelcastInstance, SessionEntryListener sessionListener) { - configureSessionMap(hazelcastInstance); this.sessionsMap = hazelcastInstance.getMap(sessionMapName); this.sessionListenerUid = this.sessionsMap.addEntryListener(sessionListener, true); - MapSessionRepository sessionRepository = new MapSessionRepository(this.sessionsMap); + MapSessionRepository sessionRepository = new MapSessionRepository(new ExpiringSessionMap(this.sessionsMap)); sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds); return sessionRepository; @@ -82,22 +79,6 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur return new SessionEntryListener(eventPublisher); } - /** - * Make a {@link MapConfig} for the given sessionMapName if one does not exist. - * Set Hazelcast's maxIdleSeconds to maxInactiveIntervalInSeconds if set (not ""). - * Otherwise get the externally configured maxIdleSeconds for the distributed sessions map. - * - * @param hazelcastInstance the {@link HazelcastInstance} to configure - */ - private void configureSessionMap(HazelcastInstance hazelcastInstance) { - MapConfig sessionMapConfig = hazelcastInstance.getConfig().getMapConfig(sessionMapName); - if (this.maxInactiveIntervalInSeconds != null) { - sessionMapConfig.setMaxIdleSeconds(this.maxInactiveIntervalInSeconds); - } else { - this.maxInactiveIntervalInSeconds = sessionMapConfig.getMaxIdleSeconds(); - } - } - public void setImportMetadata(AnnotationMetadata importMetadata) { Map enableAttrMap = importMetadata.getAnnotationAttributes(EnableHazelcastHttpSession.class.getName()); AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap); @@ -106,19 +87,7 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur } private void transferAnnotationAttributes(AnnotationAttributes enableAttrs) { - String maxInactiveIntervalString = enableAttrs.getString("maxInactiveIntervalInSeconds"); - - if (DO_NOT_CONFIGURE_INACTIVE_INTERVAL_STRING.equals(maxInactiveIntervalString)) { - this.maxInactiveIntervalInSeconds = null; - } else { - try { - this.maxInactiveIntervalInSeconds = Integer.parseInt(maxInactiveIntervalString); - } catch (NumberFormatException nfe) { - throw new IllegalArgumentException( - "@EnableHazelcastHttpSession's maxInactiveIntervalInSeconds expects an int format String but was '" - + maxInactiveIntervalString + "' instead.", nfe); - } - } + setMaxInactiveIntervalInSeconds((Integer) enableAttrs.getNumber("maxInactiveIntervalInSeconds")); setSessionMapName(enableAttrs.getString("sessionMapName")); } @@ -129,4 +98,70 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur public void setSessionMapName(String sessionMapName) { this.sessionMapName = sessionMapName; } + + static class ExpiringSessionMap implements Map { + private IMap delegate; + + ExpiringSessionMap(IMap delegate) { + this.delegate = delegate; + } + public ExpiringSession put(String key, ExpiringSession value) { + if(value == null) { + return delegate.put(key, value); + } + return delegate.put(key, value, value.getMaxInactiveIntervalInSeconds(), TimeUnit.SECONDS); + } + + public int size() { + return delegate.size(); + } + + public boolean isEmpty() { + return delegate.isEmpty(); + } + + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + public ExpiringSession get(Object key) { + return delegate.get(key); + } + + public ExpiringSession remove(Object key) { + return delegate.remove(key); + } + + public void putAll(Map m) { + delegate.putAll(m); + } + + public void clear() { + delegate.clear(); + } + + public Set keySet() { + return delegate.keySet(); + } + + public Collection values() { + return delegate.values(); + } + + public Set> entrySet() { + return delegate.entrySet(); + } + + public boolean equals(Object o) { + return delegate.equals(o); + } + + public int hashCode() { + return delegate.hashCode(); + } + } }