Skip to content

Commit 357020c

Browse files
author
Philipp Eschenbach
committed
Merge pull request #2 from jcoleman/master
merged latest jcoleman master
2 parents 2975813 + 5a6e7b4 commit 357020c

39 files changed

+2366
-697
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
.gradle/*
2-
build/*
2+
build/*
3+
example-app/build/*
4+
example-app/.gradle/*
5+
.DS_Store
6+
.rspec

Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
source "https://rubygems.org"
2+
3+
gem 'redis'
4+
gem 'rspec'
5+
gem 'httparty'

Gemfile.lock

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
diff-lcs (1.2.5)
5+
httparty (0.12.0)
6+
json (~> 1.8)
7+
multi_xml (>= 0.5.2)
8+
json (1.8.1)
9+
multi_xml (0.5.5)
10+
redis (3.1.0)
11+
rspec (3.1.0)
12+
rspec-core (~> 3.1.0)
13+
rspec-expectations (~> 3.1.0)
14+
rspec-mocks (~> 3.1.0)
15+
rspec-core (3.1.4)
16+
rspec-support (~> 3.1.0)
17+
rspec-expectations (3.1.1)
18+
diff-lcs (>= 1.2.0, < 2.0)
19+
rspec-support (~> 3.1.0)
20+
rspec-mocks (3.1.1)
21+
rspec-support (~> 3.1.0)
22+
rspec-support (3.1.0)
23+
24+
PLATFORMS
25+
ruby
26+
27+
DEPENDENCIES
28+
httparty
29+
redis
30+
rspec

README.markdown

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ The manager relies on the native expiration capability of Redis to expire keys f
1212

1313
Data stored in the session must be Serializable.
1414

15+
16+
Support this project!
17+
---------------------
18+
19+
This is an open-source project. Currently I'm not using this for anything personal or professional, so I'm not able to commit much time to the project, though I attempt to merge in reasonable pull requests. If you like to support further development of this project, you can donate via Pledgie:
20+
21+
<a href='https://pledgie.com/campaigns/26802'><img alt='Click here to lend your support to: Tomcat Redis Session Manager and make a donation at pledgie.com !' src='https://pledgie.com/campaigns/26802.png?skin_name=chrome' border='0' ></a>
22+
23+
24+
Commercial Support
25+
------------------
26+
27+
If your business depends on Tomcat and persistent sessions and you need a specific feature this project doesn't yet support, a quick bug fix you don't have time to author, or commercial support when things go wrong, I can provide support on a contractual support through my consultancy, Orange Function, LLC. If you have any questions or would like to begin discussing a deal, please contact me via email at james@orangefunction.com.
28+
29+
1530
Tomcat Versions
1631
---------------
1732

@@ -38,19 +53,31 @@ Usage
3853

3954
Add the following into your Tomcat context.xml (or the context block of the server.xml if applicable.)
4055

41-
<Valve className="com.radiadesign.catalina.session.RedisSessionHandlerValve" />
42-
<Manager className="com.radiadesign.catalina.session.RedisSessionManager"
56+
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
57+
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
4358
host="localhost" <!-- optional: defaults to "localhost" -->
4459
port="6379" <!-- optional: defaults to "6379" -->
4560
database="0" <!-- optional: defaults to "0" -->
46-
maxInactiveInterval="60" <!-- optional: defaults to "60" (in seconds) --> />
61+
maxInactiveInterval="60" <!-- optional: defaults to "60" (in seconds) -->
62+
sessionPersistPolicies="PERSIST_POLICY_1,PERSIST_POLICY_2,.." <!-- optional -->
63+
sentinelMaster="SentinelMasterName" <!-- optional -->
64+
sentinels="sentinel-host-1:port,sentinel-host-2:port,.." <!-- optional --> />
4765

4866
The Valve must be declared before the Manager.
4967

50-
Copy the tomcat-redis-session-manager.jar and jedis-2.0.0.jar files into the `lib` directory of your Tomcat installation.
68+
Copy the following files into the `TOMCAT_BASE/lib` directory:
69+
70+
* tomcat-redis-session-manager-VERSION.jar
71+
* jedis-2.5.2.jar
72+
* commons-pool2-2.2.jar
5173

5274
Reboot the server, and sessions should now be stored in Redis.
5375

76+
Connection Pool Configuration
77+
-----------------------------
78+
79+
All of the configuration options from both `org.apache.commons.pool2.impl.GenericObjectPoolConfig` and `org.apache.commons.pool2.impl.BaseObjectPoolConfig` are also configurable for the Redis connection pool used by the session manager. To configure any of these attributes (e.g., `maxIdle` and `testOnBorrow`) just use the config attribute name prefixed with `connectionPool` (e.g., `connectionPoolMaxIdle` and `connectionPoolTestOnBorrow`) and set the desired value in the `<Manager>` declaration in your Tomcat context.xml.
80+
5481
Session Change Tracking
5582
-----------------------
5683

@@ -88,20 +115,34 @@ Then the example above would look like this:
88115
myArray.add(additionalArrayValue);
89116
session.setAttribute("customDirtyFlag");
90117

118+
Persistence Policies
119+
--------------------
91120

92-
Possible Issues
93-
---------------
121+
With an persistent session storage there is going to be the distinct possibility of race conditions when requests for the same session overlap/occur concurrently. Additionally, because the session manager works by serializing the entire session object into Redis, concurrent updating of the session will exhibit last-write-wins behavior for the entire session (not just specific session attributes).
122+
123+
Since each situation is different, the manager gives you several options which control the details of when/how sessions are persisted. Each of the following options may be selected by setting the `sessionPersistPolicies="PERSIST_POLICY_1,PERSIST_POLICY_2,.."` attributes in your manager declaration in Tomcat's context.xml. Unless noted otherwise, the various options are all combinable.
124+
125+
- `SAVE_ON_CHANGE`: every time `session.setAttribute()` or `session.removeAttribute()` is called the session will be saved. __Note:__ This feature cannot detect changes made to objects already stored in a specific session attribute. __Tradeoffs__: This option will degrade performance slightly as any change to the session will save the session synchronously to Redis.
126+
- `ALWAYS_SAVE_AFTER_REQUEST`: force saving after every request, regardless of whether or not the manager has detected changes to the session. This option is particularly useful if you make changes to objects already stored in a specific session attribute. __Tradeoff:__ This option make actually increase the liklihood of race conditions if not all of your requests change the session.
94127

95-
There is the possibility of a race condition that would cause seeming invisibility of the session immediately after your web application logs in a user: if the response has finished streaming and the client requests a new page before the valve has been able to complete saving the session into Redis, then the new request will not see the session.
96128

97-
This condition will be detected by the session manager and a java.lang.IllegalStateException with the message `Race condition encountered: attempted to load session[SESSION_ID] which has been created but not yet serialized.` will be thrown.
129+
Testing/Example App
130+
-------------------
98131

99-
Normally this should be incredibly unlikely (insert joke about programmers and "this should never happen" statements here) since the connection to save the session into Redis is almost guaranteed to be faster than the latency between a client receiving the response, processing it, and starting a new request.
132+
For full integration testing as well as a demonstration of how to use the session manager, this project contains an example app and a virtual server setup via Vagrant and Chef.
100133

101-
Possible solutions:
134+
To get the example server up and running, you'll need to do the following:
135+
1. Download and install Virtual Box (4.3.12 at the time of this writing) from https://www.virtualbox.org/wiki/Downloads
136+
1. Download and install the latest version (1.6.3 at the time of this writing) of Vagrant from http://www.vagrantup.com/downloads.html
137+
1. Install Ruby, if necessary.
138+
1. Install Berkshelf with `gem install berkshelf`
139+
1. Install the Vagrant Berkshelf plugin with `vagrant plugin install vagrant-berkshelf --plugin-version '>= 2.0.1'`
140+
1. Install the Vagrant Cachier plugin for _speed_ with `vagrant plugin install vagrant-cachier`
141+
1. Install the Vagrant Omnibus plugin with `vagrant plugin install vagrant-omnibus`
142+
1. Install the required Ruby gems with `PROJECT_ROOT/bundle install`
143+
1. Boot the virtual machine with `PROJECT_ROOT/vagrant up`
144+
1. Run the tests with `PROJECT_ROOT/rspec`
102145

103-
- Enable the "save on change" feature by setting `saveOnChange` to `true` in your manager declaration in Tomcat's context.xml. Using this feature will degrade performance slightly as any change to the session will save the session synchronously to Redis, and technically this will still exhibit slight race condition behavior, but it eliminates as much possiblity of errors occurring as possible.
104-
- If you encounter errors, then you can force save the session early (before sending a response to the client) then you can retrieve the current session, and call `currentSession.manager.save(currentSession)` to synchronously eliminate the race condition. Note: this will only work directly if your application has the actual session object directly exposed. Many frameworks (and often even Tomcat) will expose the session in their own wrapper HttpSession implementing class. You may be able to dig through these layers to expose the actual underlying RedisSession instance--if so, then using that instance will allow you to implement the workaround.
105146

106147
Acknowledgements
107148
----------------

build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ repositories {
55
mavenCentral()
66
}
77

8+
compileJava {
9+
sourceCompatibility = 1.7
10+
targetCompatibility = 1.7
11+
}
12+
813
dependencies {
914
compile group: 'org.apache.tomcat', name: 'tomcat-catalina', version: '7.0.27'
1015
compile group: 'redis.clients', name: 'jedis', version: '2.5.2'
16+
compile group: 'org.apache.commons', name: 'commons-pool2', version: '2.2'
1117
// compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
1218
// testCompile group: 'junit', name: 'junit', version: '4.+'
1319
}

example-app/build.gradle

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
apply plugin: 'java'
2+
apply plugin: 'war'
3+
4+
version = '0.1'
5+
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
compileJava {
11+
sourceCompatibility = 1.7
12+
targetCompatibility = 1.7
13+
}
14+
15+
dependencies {
16+
providedCompile group: 'org.apache.tomcat', name: 'tomcat-catalina', version: '7.0.27'
17+
compile group: 'redis.clients', name: 'jedis', version: '2.5.2'
18+
compile group: 'com.sparkjava', name: 'spark-core', version: '1.1.1'
19+
compile group: 'com.google.code.gson', name: 'gson', version: '2.3'
20+
compile group: 'org.slf4j', name: 'slf4j-simple',version: '1.7.5'
21+
providedCompile project(":tomcat-redis-session-manager")
22+
}
23+
24+
war {
25+
//webAppDirName = 'source/main/'
26+
27+
//from 'src/rootContent' // adds a file-set to the root of the archive
28+
//webInf { from 'src/additionalWebInf' } // adds a file-set to the WEB-INF dir.
29+
//classpath fileTree('additionalLibs') // adds a file-set to the WEB-INF/lib dir.
30+
//classpath configurations.moreLibs // adds a configuration to the WEB-INF/lib dir.
31+
}

example-app/settings.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
include ":tomcat-redis-session-manager"
2+
project(":tomcat-redis-session-manager").projectDir = new File(rootDir, "../")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.orangefunction.tomcatredissessionmanager.exampleapp;
2+
3+
import com.google.gson.Gson;
4+
import com.orangefunction.tomcat.redissessions.RedisSession;
5+
import java.util.HashMap;
6+
import java.util.Collections;
7+
import spark.ResponseTransformerRoute;
8+
import spark.Session;
9+
import javax.servlet.http.HttpSession;
10+
11+
public abstract class JsonTransformerRoute extends ResponseTransformerRoute {
12+
13+
private Gson gson = new Gson();
14+
15+
protected JsonTransformerRoute(String path) {
16+
super(path);
17+
}
18+
19+
protected JsonTransformerRoute(String path, String acceptType) {
20+
super(path, acceptType);
21+
}
22+
23+
@Override
24+
public String render(Object jsonObject) {
25+
return gson.toJson(jsonObject);
26+
}
27+
28+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.orangefunction.tomcatredissessionmanager.exampleapp;
2+
3+
import com.google.gson.Gson;
4+
import com.orangefunction.tomcat.redissessions.RedisSession;
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
import java.util.Collections;
8+
import spark.ResponseTransformerRoute;
9+
import spark.Session;
10+
import javax.servlet.http.HttpSession;
11+
12+
public abstract class SessionJsonTransformerRoute extends ResponseTransformerRoute {
13+
14+
private Gson gson = new Gson();
15+
16+
protected SessionJsonTransformerRoute(String path) {
17+
super(path);
18+
}
19+
20+
protected SessionJsonTransformerRoute(String path, String acceptType) {
21+
super(path, acceptType);
22+
}
23+
24+
@Override
25+
public String render(Object object) {
26+
if (object instanceof Object[]) {
27+
Object[] tuple = (Object[])object;
28+
Session sparkSession = (Session)tuple[0];
29+
HttpSession session = (HttpSession)(sparkSession).raw();
30+
HashMap<String, Object> map = new HashMap<String, Object>();
31+
map.putAll((Map<String, Object>)tuple[1]);
32+
map.put("sessionId", session.getId());
33+
return gson.toJson(map);
34+
} else if (object instanceof Session) {
35+
Session sparkSession = (Session)object;
36+
HashMap<String, Object> sessionMap = new HashMap<String, Object>();
37+
if (null != sparkSession) {
38+
HttpSession session = (HttpSession)(sparkSession).raw();
39+
sessionMap.put("sessionId", session.getId());
40+
HashMap<String, Object> attributesMap = new HashMap<String, Object>();
41+
for (String key : Collections.list(session.getAttributeNames())) {
42+
attributesMap.put(key, session.getAttribute(key));
43+
}
44+
sessionMap.put("attributes", attributesMap);
45+
}
46+
return gson.toJson(sessionMap);
47+
} else {
48+
return "{}";
49+
}
50+
}
51+
52+
}

0 commit comments

Comments
 (0)