├── Gemfile
├── .gitignore
├── example-app
├── settings.gradle
├── src
│ └── main
│ │ ├── webapp
│ │ ├── META-INF
│ │ │ └── context.xml
│ │ └── WEB-INF
│ │ │ └── web.xml
│ │ └── java
│ │ └── com
│ │ └── orangefunction
│ │ └── tomcatredissessionmanager
│ │ └── exampleapp
│ │ ├── JsonTransformerRoute.java
│ │ ├── SessionJsonTransformerRoute.java
│ │ └── WebApp.java
└── build.gradle
├── vagrant
└── tomcat-redis-example
│ ├── Berksfile
│ ├── .kitchen.yml
│ ├── .gitignore
│ ├── Thorfile
│ ├── metadata.rb
│ ├── Gemfile
│ ├── chefignore
│ ├── recipes
│ └── default.rb
│ └── Vagrantfile
├── src
└── main
│ └── java
│ └── com
│ └── orangefunction
│ └── tomcat
│ └── redissessions
│ ├── Serializer.java
│ ├── RedisSessionHandlerValve.java
│ ├── SessionSerializationMetadata.java
│ ├── JavaSerializer.java
│ ├── RedisSession.java
│ └── RedisSessionManager.java
├── spec
├── requests
│ ├── session_creation_spec.rb
│ ├── session_expiration_spec.rb
│ ├── save_on_change_spec.rb
│ ├── always_save_after_request_spec.rb
│ └── session_updating_spec.rb
├── support
│ └── requests_helper.rb
└── spec_helper.rb
├── Gemfile.lock
├── license.txt
└── README.markdown
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem 'redis'
4 | gem 'rspec'
5 | gem 'httparty'
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/*
2 | build/*
3 | example-app/build/*
4 | example-app/.gradle/*
5 | .DS_Store
6 | .rspec
7 | *.iml
8 | .idea/*
9 |
--------------------------------------------------------------------------------
/example-app/settings.gradle:
--------------------------------------------------------------------------------
1 | include ":tomcat-redis-session-manager"
2 | project(":tomcat-redis-session-manager").projectDir = new File(rootDir, "../")
3 |
--------------------------------------------------------------------------------
/vagrant/tomcat-redis-example/Berksfile:
--------------------------------------------------------------------------------
1 | source "https://api.berkshelf.com"
2 |
3 | metadata
4 |
5 | cookbook 'java', github: 'agileorbit-cookbooks/java', branch: 'master'
6 |
--------------------------------------------------------------------------------
/vagrant/tomcat-redis-example/.kitchen.yml:
--------------------------------------------------------------------------------
1 | ---
2 | driver:
3 | name: vagrant
4 |
5 | provisioner:
6 | name: chef_solo
7 |
8 | platforms:
9 | - name: ubuntu-12.04
10 | - name: centos-6.4
11 |
12 | suites:
13 | - name: default
14 | run_list:
15 | attributes:
16 |
--------------------------------------------------------------------------------
/vagrant/tomcat-redis-example/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *#
3 | .#*
4 | \#*#
5 | .*.sw[a-z]
6 | *.un~
7 | pkg/
8 |
9 | # Berkshelf
10 | .vagrant
11 | /cookbooks
12 | Berksfile.lock
13 |
14 | # Bundler
15 | Gemfile.lock
16 | bin/*
17 | .bundle/*
18 |
19 | .kitchen/
20 | .kitchen.local.yml
21 |
--------------------------------------------------------------------------------
/vagrant/tomcat-redis-example/Thorfile:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | require 'bundler'
4 | require 'bundler/setup'
5 | require 'berkshelf/thor'
6 |
7 | begin
8 | require 'kitchen/thor_tasks'
9 | Kitchen::ThorTasks.new
10 | rescue LoadError
11 | puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
12 | end
13 |
--------------------------------------------------------------------------------
/example-app/src/main/webapp/META-INF/context.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/vagrant/tomcat-redis-example/metadata.rb:
--------------------------------------------------------------------------------
1 | name 'tomcat-redis-example'
2 | maintainer 'James Coleman'
3 | maintainer_email 'jtc331@gmail.com'
4 | license 'MIT'
5 | description 'Installs/Configures tomcat-redis-example'
6 | long_description 'Installs/Configures tomcat-redis-example'
7 | version '0.1.0'
8 |
9 | depends 'apt', '~> 2.4'
10 | depends 'tomcat', '~> 0.16.2'
11 | depends 'redisio', '~> 2.2.3'
12 |
13 | %w{ ubuntu debian }.each do |os|
14 | supports os
15 | end
--------------------------------------------------------------------------------
/vagrant/tomcat-redis-example/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'berkshelf'
4 |
5 | # Uncomment these lines if you want to live on the Edge:
6 | #
7 | # group :development do
8 | # gem "berkshelf", github: "berkshelf/berkshelf"
9 | # gem "vagrant", github: "mitchellh/vagrant", tag: "v1.5.2"
10 | # end
11 | #
12 | # group :plugins do
13 | # gem "vagrant-berkshelf", github: "berkshelf/vagrant-berkshelf"
14 | # gem "vagrant-omnibus", github: "schisamo/vagrant-omnibus"
15 | # end
16 |
17 | gem 'test-kitchen'
18 | gem 'kitchen-vagrant'
19 |
--------------------------------------------------------------------------------
/src/main/java/com/orangefunction/tomcat/redissessions/Serializer.java:
--------------------------------------------------------------------------------
1 | package com.orangefunction.tomcat.redissessions;
2 |
3 | import javax.servlet.http.HttpSession;
4 | import java.io.IOException;
5 |
6 | public interface Serializer {
7 | void setClassLoader(ClassLoader loader);
8 |
9 | byte[] attributesHashFrom(RedisSession session) throws IOException;
10 | byte[] serializeFrom(RedisSession session, SessionSerializationMetadata metadata) throws IOException;
11 | void deserializeInto(byte[] data, RedisSession session, SessionSerializationMetadata metadata) throws IOException, ClassNotFoundException;
12 | }
13 |
--------------------------------------------------------------------------------
/spec/requests/session_creation_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'session creation' do
4 |
5 | it 'should begin without a session' do
6 | get(SESSION_PATH)
7 | json.should_not have_key('sessionId')
8 | end
9 |
10 | it 'should generate a session ID' do
11 | post(SESSION_PATH)
12 | json.should have_key('sessionId')
13 | end
14 |
15 | it 'should not create a session when requesting session creation with existing session ID' do
16 | pending
17 | end
18 |
19 | it 'should detect a session ID collision and generate a new session ID' do
20 | pending
21 | end
22 |
23 | it 'should detect and report race conditions when creating new sessions' do
24 | pending
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | diff-lcs (1.2.5)
5 | httparty (0.21.0)
6 | mini_mime (>= 1.0.0)
7 | multi_xml (>= 0.5.2)
8 | mini_mime (1.1.2)
9 | multi_xml (0.6.0)
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
31 |
--------------------------------------------------------------------------------
/example-app/src/main/webapp/WEB-INF/web.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | Tomcat Redis Session Manager Example App
7 |
8 | SparkFilter
9 | spark.servlet.SparkFilter
10 |
11 | applicationClass
12 | com.orangefunction.tomcatredissessionmanager.exampleapp.WebApp
13 |
14 |
15 |
16 |
17 | SparkFilter
18 | /*
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/spec/requests/session_expiration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'session expiration' do
4 |
5 | before :each do
6 | get("#{SETTINGS_PATH}/maxInactiveInterval")
7 | @oldMaxInactiveIntervalValue = json['value']
8 | post("#{SETTINGS_PATH}/maxInactiveInterval", body: {value: '1'})
9 | end
10 |
11 | after :each do
12 | post("#{SETTINGS_PATH}/maxInactiveInterval", body: {value: @oldMaxInactiveIntervalValue})
13 | end
14 |
15 | it 'should no longer contain a session after the expiration timeout has passed' do
16 | post(SESSION_PATH)
17 | created_session_id = json['sessionId']
18 | get(SESSION_PATH)
19 | json['sessionId'].should == created_session_id
20 | sleep 1.0
21 | get(SESSION_PATH)
22 | json['sessionId'].should be_nil
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/example-app/src/main/java/com/orangefunction/tomcatredissessionmanager/exampleapp/JsonTransformerRoute.java:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionHandlerValve.java:
--------------------------------------------------------------------------------
1 | package com.orangefunction.tomcat.redissessions;
2 |
3 | import org.apache.catalina.Session;
4 | import org.apache.catalina.connector.Request;
5 | import org.apache.catalina.connector.Response;
6 | import org.apache.catalina.valves.ValveBase;
7 |
8 | import javax.servlet.ServletException;
9 | import java.io.IOException;
10 |
11 | import org.apache.juli.logging.Log;
12 | import org.apache.juli.logging.LogFactory;
13 |
14 |
15 | public class RedisSessionHandlerValve extends ValveBase {
16 | private final Log log = LogFactory.getLog(RedisSessionManager.class);
17 | private RedisSessionManager manager;
18 |
19 | public void setRedisSessionManager(RedisSessionManager manager) {
20 | this.manager = manager;
21 | }
22 |
23 | @Override
24 | public void invoke(Request request, Response response) throws IOException, ServletException {
25 | try {
26 | getNext().invoke(request, response);
27 | } finally {
28 | manager.afterRequest();
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example-app/build.gradle:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011-2014 James Coleman
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/vagrant/tomcat-redis-example/chefignore:
--------------------------------------------------------------------------------
1 | # Put files/directories that should be ignored in this file when uploading
2 | # or sharing to the community site.
3 | # Lines that start with '# ' are comments.
4 |
5 | # OS generated files #
6 | ######################
7 | .DS_Store
8 | Icon?
9 | nohup.out
10 | ehthumbs.db
11 | Thumbs.db
12 |
13 | # SASS #
14 | ########
15 | .sass-cache
16 |
17 | # EDITORS #
18 | ###########
19 | \#*
20 | .#*
21 | *~
22 | *.sw[a-z]
23 | *.bak
24 | REVISION
25 | TAGS*
26 | tmtags
27 | *_flymake.*
28 | *_flymake
29 | *.tmproj
30 | .project
31 | .settings
32 | mkmf.log
33 |
34 | ## COMPILED ##
35 | ##############
36 | a.out
37 | *.o
38 | *.pyc
39 | *.so
40 | *.com
41 | *.class
42 | *.dll
43 | *.exe
44 | */rdoc/
45 |
46 | # Testing #
47 | ###########
48 | .watchr
49 | .rspec
50 | spec/*
51 | spec/fixtures/*
52 | test/*
53 | features/*
54 | Guardfile
55 | Procfile
56 |
57 | # SCM #
58 | #######
59 | .git
60 | */.git
61 | .gitignore
62 | .gitmodules
63 | .gitconfig
64 | .gitattributes
65 | .svn
66 | */.bzr/*
67 | */.hg/*
68 | */.svn/*
69 |
70 | # Berkshelf #
71 | #############
72 | cookbooks/*
73 | tmp
74 |
75 | # Cookbooks #
76 | #############
77 | CONTRIBUTING
78 | CHANGELOG*
79 |
80 | # Strainer #
81 | ############
82 | Colanderfile
83 | Strainerfile
84 | .colander
85 | .strainer
86 |
87 | # Vagrant #
88 | ###########
89 | .vagrant
90 | Vagrantfile
91 |
92 | # Travis #
93 | ##########
94 | .travis.yml
95 |
--------------------------------------------------------------------------------
/src/main/java/com/orangefunction/tomcat/redissessions/SessionSerializationMetadata.java:
--------------------------------------------------------------------------------
1 | package com.orangefunction.tomcat.redissessions;
2 |
3 | import java.io.*;
4 |
5 |
6 | public class SessionSerializationMetadata implements Serializable {
7 |
8 | private byte[] sessionAttributesHash;
9 |
10 | public SessionSerializationMetadata() {
11 | this.sessionAttributesHash = new byte[0];
12 | }
13 |
14 | public byte[] getSessionAttributesHash() {
15 | return sessionAttributesHash;
16 | }
17 |
18 | public void setSessionAttributesHash(byte[] sessionAttributesHash) {
19 | this.sessionAttributesHash = sessionAttributesHash;
20 | }
21 |
22 | public void copyFieldsFrom(SessionSerializationMetadata metadata) {
23 | this.setSessionAttributesHash(metadata.getSessionAttributesHash());
24 | }
25 |
26 | private void writeObject(java.io.ObjectOutputStream out) throws IOException {
27 | out.writeInt(sessionAttributesHash.length);
28 | out.write(this.sessionAttributesHash);
29 | }
30 |
31 | private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
32 | int hashLength = in.readInt();
33 | byte[] sessionAttributesHash = new byte[hashLength];
34 | in.read(sessionAttributesHash, 0, hashLength);
35 | this.sessionAttributesHash = sessionAttributesHash;
36 | }
37 |
38 | private void readObjectNoData() throws ObjectStreamException {
39 | this.sessionAttributesHash = new byte[0];
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/spec/support/requests_helper.rb:
--------------------------------------------------------------------------------
1 | require 'httparty'
2 |
3 | module RequestsHelper
4 |
5 | class ExampleAppClient
6 | include ::HTTParty
7 | base_uri 'http://172.28.128.3:8080/example'
8 | end
9 |
10 | def client
11 | @client ||= ExampleAppClient.new
12 | end
13 |
14 | def get(path, options={})
15 | send_request(:get, path, options)
16 | end
17 |
18 | def put(path, options={})
19 | send_request(:put, path, options)
20 | end
21 |
22 | def post(path, options={})
23 | send_request(:post, path, options)
24 | end
25 |
26 | def delete(path, options={})
27 | send_request(:delete, path, options)
28 | end
29 |
30 | def send_request(method, path, options={})
31 | options ||= {}
32 | headers = options[:headers] || {}
33 | if cookie && !options.key('Cookie')
34 | headers['Cookie'] = cookie
35 | end
36 | options = options.merge(headers: headers)
37 | self.response = self.client.class.send(method, path, options)
38 | end
39 |
40 | def request
41 | response.request
42 | end
43 |
44 | def response
45 | @response
46 | end
47 |
48 | def response=(r)
49 | if r
50 | if r.headers.key?('Set-Cookie')
51 | @cookie = r.headers['Set-Cookie']
52 | end
53 | else
54 | @cookie = nil
55 | end
56 | @json = nil
57 | @response = r
58 | end
59 |
60 | def cookie
61 | @cookie
62 | end
63 |
64 | def json
65 | @json ||= JSON.parse(response.body)
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/spec/requests/save_on_change_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "SAVE_ON_CHANGE" do
4 |
5 | before :each do
6 | get("#{SETTINGS_PATH}/sessionPersistPolicies")
7 | @oldSessionPersistPoliciesValue = json['value']
8 | enums = @oldSessionPersistPoliciesValue.split(',')
9 | enums << 'SAVE_ON_CHANGE'
10 | post("#{SETTINGS_PATH}/sessionPersistPolicies", body: {value: enums.join(',')})
11 | end
12 |
13 | after :each do
14 | post("#{SETTINGS_PATH}/sessionPersistPolicies", body: {value: @oldSessionPersistPoliciesValue})
15 | end
16 |
17 | it 'should support persisting the session on change to minimize race conditions on simultaneous updates' do
18 | post(SESSION_PATH, body: {param2: '5'})
19 | get("#{SESSION_ATTRIBUTES_PATH}/param2")
20 | json['value'].should == '5'
21 |
22 | # This is not a perfect guarantee, but in general we're assuming
23 | # that the requests will happen in the following order:
24 | # - Post(value=5) starts
25 | # - Post(value=6) starts
26 | # - Post(value=6) finishes
27 | # - Get() returns 6
28 | # - Post(value=5) finishes
29 | # - Get() returns 6 (because the change value=5 saved immediately rather than on request finish)
30 | long_request = Thread.new do
31 | post("#{SESSION_ATTRIBUTES_PATH}/param2", body: {value: '5', sleep: 2000})
32 | end
33 |
34 | sleep 0.5
35 | get("#{SESSION_ATTRIBUTES_PATH}/param2")
36 | json['value'].should == '5'
37 |
38 | post("#{SESSION_ATTRIBUTES_PATH}/param2", body: {value: '6'})
39 | get("#{SESSION_ATTRIBUTES_PATH}/param2")
40 | json['value'].should == '6'
41 |
42 | long_request.join
43 |
44 | get("#{SESSION_ATTRIBUTES_PATH}/param2")
45 | json['value'].should == '6'
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/requests/always_save_after_request_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "ALWAYS_SAVE_AFTER_REQUEST" do
4 |
5 | before :each do
6 | get("#{SETTINGS_PATH}/sessionPersistPolicies")
7 | @oldSessionPersistPoliciesValue = json['value']
8 | enums = @oldSessionPersistPoliciesValue.split(',')
9 | enums << 'ALWAYS_SAVE_AFTER_REQUEST'
10 | post("#{SETTINGS_PATH}/sessionPersistPolicies", body: {value: enums.join(',')})
11 | end
12 |
13 | after :each do
14 | post("#{SETTINGS_PATH}/sessionPersistPolicies", body: {value: @oldSessionPersistPoliciesValue})
15 | end
16 |
17 | it 'should optionally support persisting the session after every request regardless of changed status' do
18 | post(SESSION_PATH, body: {param2: '5'})
19 | get("#{SESSION_ATTRIBUTES_PATH}/param2")
20 | json['value'].should == '5'
21 |
22 | # This is not a perfect guarantee, but in general we're assuming
23 | # that the requests will happen in the following order:
24 | # - Post(value=5) starts
25 | # - Post(value=6) starts
26 | # - Post(value=6) finishes
27 | # - Get() returns 6
28 | # - Post(value=5) finishes
29 | # - Get() returns 5 (because the change value=5 saved on request finish even though it wasn't a change)
30 | long_request = Thread.new do
31 | post("#{SESSION_ATTRIBUTES_PATH}/param2", body: {value: '5', sleep: 2000})
32 | end
33 |
34 | sleep 0.5
35 | get("#{SESSION_ATTRIBUTES_PATH}/param2")
36 | json['value'].should == '5'
37 |
38 | post("#{SESSION_ATTRIBUTES_PATH}/param2", body: {value: '6'})
39 | get("#{SESSION_ATTRIBUTES_PATH}/param2")
40 | json['value'].should == '6'
41 |
42 | long_request.join
43 |
44 | get("#{SESSION_ATTRIBUTES_PATH}/param2")
45 | json['value'].should == '5'
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/vagrant/tomcat-redis-example/recipes/default.rb:
--------------------------------------------------------------------------------
1 | node.set['java']['jdk_version'] = '7'
2 |
3 | node.set["tomcat"]["base_version"] = 7
4 | node.set['tomcat']['user'] = "tomcat#{node["tomcat"]["base_version"]}"
5 | node.set['tomcat']['group'] = "tomcat#{node["tomcat"]["base_version"]}"
6 | node.set['tomcat']['home'] = "/usr/share/tomcat#{node["tomcat"]["base_version"]}"
7 | node.set['tomcat']['base'] = "/var/lib/tomcat#{node["tomcat"]["base_version"]}"
8 | node.set['tomcat']['config_dir'] = "/etc/tomcat#{node["tomcat"]["base_version"]}"
9 | node.set['tomcat']['log_dir'] = "/var/log/tomcat#{node["tomcat"]["base_version"]}"
10 | node.set['tomcat']['tmp_dir'] = "/tmp/tomcat#{node["tomcat"]["base_version"]}-tmp"
11 | node.set['tomcat']['work_dir'] = "/var/cache/tomcat#{node["tomcat"]["base_version"]}"
12 | node.set['tomcat']['context_dir'] = "#{node["tomcat"]["config_dir"]}/Catalina/localhost"
13 | node.set['tomcat']['webapp_dir'] = "/var/lib/tomcat#{node["tomcat"]["base_version"]}/webapps"
14 | node.set['tomcat']['lib_dir'] = "#{node["tomcat"]["home"]}/lib"
15 | node.set['tomcat']['endorsed_dir'] = "#{node["tomcat"]["lib_dir"]}/endorsed"
16 |
17 | #Chef::Log.info "node: #{node.to_hash.inspect}"
18 |
19 | include_recipe 'redisio'
20 | include_recipe 'redisio::enable'
21 |
22 | include_recipe 'tomcat'
23 |
24 | lib_dir = File.join(node['tomcat']['base'], 'lib')
25 |
26 | directory(lib_dir) do
27 | user node['tomcat']['user']
28 | group node['tomcat']['group']
29 | end
30 |
31 | remote_file(File.join(lib_dir, 'spark-core-1.1.1.jar')) do
32 | source 'http://central.maven.org/maven2/com/sparkjava/spark-core/1.1.1/spark-core-1.1.1.jar'
33 | action :create_if_missing
34 | end
35 |
36 | remote_file(File.join(lib_dir, 'commons-pool2-2.2.jar')) do
37 | source 'http://central.maven.org/maven2/org/apache/commons/commons-pool2/2.2/commons-pool2-2.2.jar'
38 | action :create_if_missing
39 | end
40 |
41 | remote_file(File.join(lib_dir, 'jedis-2.5.2.jar')) do
42 | source 'http://central.maven.org/maven2/redis/clients/jedis/2.5.2/jedis-2.5.2.jar'
43 | action :create_if_missing
44 | end
45 |
--------------------------------------------------------------------------------
/example-app/src/main/java/com/orangefunction/tomcatredissessionmanager/exampleapp/SessionJsonTransformerRoute.java:
--------------------------------------------------------------------------------
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 map = new HashMap();
31 | map.putAll((Map)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 sessionMap = new HashMap();
37 | if (null != sparkSession) {
38 | HttpSession session = (HttpSession)(sparkSession).raw();
39 | sessionMap.put("sessionId", session.getId());
40 | HashMap attributesMap = new HashMap();
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 | }
--------------------------------------------------------------------------------
/src/main/java/com/orangefunction/tomcat/redissessions/JavaSerializer.java:
--------------------------------------------------------------------------------
1 | package com.orangefunction.tomcat.redissessions;
2 |
3 | import org.apache.catalina.util.CustomObjectInputStream;
4 |
5 | import javax.servlet.http.HttpSession;
6 |
7 | import java.util.Enumeration;
8 | import java.util.HashMap;
9 | import java.io.*;
10 | import java.security.MessageDigest;
11 | import java.security.NoSuchAlgorithmException;
12 |
13 | import org.apache.juli.logging.Log;
14 | import org.apache.juli.logging.LogFactory;
15 |
16 | public class JavaSerializer implements Serializer {
17 | private ClassLoader loader;
18 |
19 | private final Log log = LogFactory.getLog(JavaSerializer.class);
20 |
21 | @Override
22 | public void setClassLoader(ClassLoader loader) {
23 | this.loader = loader;
24 | }
25 |
26 | public byte[] attributesHashFrom(RedisSession session) throws IOException {
27 | HashMap attributes = new HashMap();
28 | for (Enumeration enumerator = session.getAttributeNames(); enumerator.hasMoreElements();) {
29 | String key = enumerator.nextElement();
30 | attributes.put(key, session.getAttribute(key));
31 | }
32 |
33 | byte[] serialized = null;
34 |
35 | try (
36 | ByteArrayOutputStream bos = new ByteArrayOutputStream();
37 | ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));
38 | ) {
39 | oos.writeUnshared(attributes);
40 | oos.flush();
41 | serialized = bos.toByteArray();
42 | }
43 |
44 | MessageDigest digester = null;
45 | try {
46 | digester = MessageDigest.getInstance("MD5");
47 | } catch (NoSuchAlgorithmException e) {
48 | log.error("Unable to get MessageDigest instance for MD5");
49 | }
50 | return digester.digest(serialized);
51 | }
52 |
53 | @Override
54 | public byte[] serializeFrom(RedisSession session, SessionSerializationMetadata metadata) throws IOException {
55 | byte[] serialized = null;
56 |
57 | try (
58 | ByteArrayOutputStream bos = new ByteArrayOutputStream();
59 | ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));
60 | ) {
61 | oos.writeObject(metadata);
62 | session.writeObjectData(oos);
63 | oos.flush();
64 | serialized = bos.toByteArray();
65 | }
66 |
67 | return serialized;
68 | }
69 |
70 | @Override
71 | public void deserializeInto(byte[] data, RedisSession session, SessionSerializationMetadata metadata) throws IOException, ClassNotFoundException {
72 | try(
73 | BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data));
74 | ObjectInputStream ois = new CustomObjectInputStream(bis, loader);
75 | ) {
76 | SessionSerializationMetadata serializedMetadata = (SessionSerializationMetadata)ois.readObject();
77 | metadata.copyFieldsFrom(serializedMetadata);
78 | session.readObjectData(ois);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../support/requests_helper', __FILE__)
2 |
3 | SESSION_PATH = '/session'
4 | SESSIONS_PATH = '/sessions'
5 | SESSION_ATTRIBUTES_PATH = '/session/attributes'
6 | SETTINGS_PATH = '/settings'
7 |
8 | # This file was generated by the `rspec --init` command. Conventionally, all
9 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
10 | # Require this file using `require "spec_helper"` to ensure that it is only
11 | # loaded once.
12 | #
13 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
14 | RSpec.configure do |config|
15 | config.treat_symbols_as_metadata_keys_with_true_values = true
16 | config.run_all_when_everything_filtered = true
17 | config.filter_run :focus
18 |
19 | # Run specs in random order to surface order dependencies. If you find an
20 | # order dependency and want to debug it, you can fix the order by providing
21 | # the seed, which is printed after each run.
22 | # --seed 1234
23 | config.order = 'random'
24 |
25 | config.include RequestsHelper
26 |
27 | should_build_and_install_on_vagrant = true
28 | has_built_and_installed_on_vagrant = false
29 |
30 | config.before :all do
31 | root_project_path = File.expand_path('../../', __FILE__)
32 | vagrant_path = File.join(root_project_path, 'vagrant', 'tomcat-redis-example')
33 | example_app_path = File.join(root_project_path, 'example-app')
34 | #unless `cd #{vagrant_path} && vagrant status --machine-readable | awk -F, '$3 ~ /^state$/ { print $4}'`.strip == 'running'
35 | # raise "Expected vagrant to be running."
36 | #end
37 |
38 | if should_build_and_install_on_vagrant && !has_built_and_installed_on_vagrant
39 | # build manager
40 | build_manager_command = <<-eos
41 | cd #{root_project_path} \
42 | && gradle clean \
43 | && gradle build
44 | eos
45 | `#{build_manager_command}`
46 |
47 | # build example app
48 | build_example_app_command = <<-eos
49 | cd #{example_app_path} \
50 | && gradle clean \
51 | && gradle war
52 | eos
53 | `#{build_example_app_command}`
54 |
55 | deploy_command = <<-eos
56 | cd #{vagrant_path} \
57 | && vagrant ssh -c "\
58 | sudo service tomcat7 stop \
59 | && sudo mkdir -p /var/lib/tomcat7/lib \
60 | && sudo rm -rf /var/lib/tomcat7/lib/tomcat-redis-session-manager* \
61 | && sudo cp /opt/tomcat-redis-session-manager/build/libs/tomcat-redis-session-manager*.jar /var/lib/tomcat7/lib/ \
62 | && sudo rm -rf /var/lib/tomcat7/webapps/example* \
63 | && sudo cp /opt/tomcat-redis-session-manager/example-app/build/libs/example-app*.war /var/lib/tomcat7/webapps/example.war \
64 | && sudo rm -f /var/log/tomcat7/* \
65 | && sudo service tomcat7 start \
66 | "
67 | eos
68 | `#{deploy_command}`
69 |
70 | has_built_and_installed_on_vagrant = true
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/src/main/java/com/orangefunction/tomcat/redissessions/RedisSession.java:
--------------------------------------------------------------------------------
1 | package com.orangefunction.tomcat.redissessions;
2 |
3 | import java.security.Principal;
4 | import org.apache.catalina.Manager;
5 | import org.apache.catalina.session.StandardSession;
6 | import java.util.HashMap;
7 | import java.io.IOException;
8 |
9 | import org.apache.juli.logging.Log;
10 | import org.apache.juli.logging.LogFactory;
11 |
12 |
13 | public class RedisSession extends StandardSession {
14 |
15 | private final Log log = LogFactory.getLog(RedisSession.class);
16 |
17 | protected static Boolean manualDirtyTrackingSupportEnabled = false;
18 |
19 | public static void setManualDirtyTrackingSupportEnabled(Boolean enabled) {
20 | manualDirtyTrackingSupportEnabled = enabled;
21 | }
22 |
23 | protected static String manualDirtyTrackingAttributeKey = "__changed__";
24 |
25 | public static void setManualDirtyTrackingAttributeKey(String key) {
26 | manualDirtyTrackingAttributeKey = key;
27 | }
28 |
29 |
30 | protected HashMap changedAttributes;
31 | protected Boolean dirty;
32 |
33 | public RedisSession(Manager manager) {
34 | super(manager);
35 | resetDirtyTracking();
36 | }
37 |
38 | public Boolean isDirty() {
39 | return dirty || !changedAttributes.isEmpty();
40 | }
41 |
42 | public HashMap getChangedAttributes() {
43 | return changedAttributes;
44 | }
45 |
46 | public void resetDirtyTracking() {
47 | changedAttributes = new HashMap<>();
48 | dirty = false;
49 | }
50 |
51 | @Override
52 | public void setAttribute(String key, Object value) {
53 | if (manualDirtyTrackingSupportEnabled && manualDirtyTrackingAttributeKey.equals(key)) {
54 | dirty = true;
55 | return;
56 | }
57 |
58 | Object oldValue = getAttribute(key);
59 | super.setAttribute(key, value);
60 |
61 | if ( (value != null || oldValue != null)
62 | && ( value == null && oldValue != null
63 | || oldValue == null && value != null
64 | || !value.getClass().isInstance(oldValue)
65 | || !value.equals(oldValue) ) ) {
66 | if (this.manager instanceof RedisSessionManager
67 | && ((RedisSessionManager)this.manager).getSaveOnChange()) {
68 | try {
69 | ((RedisSessionManager)this.manager).save(this, true);
70 | } catch (IOException ex) {
71 | log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + ex.getMessage());
72 | }
73 | } else {
74 | changedAttributes.put(key, value);
75 | }
76 | }
77 | }
78 |
79 | @Override
80 | public void removeAttribute(String name) {
81 | super.removeAttribute(name);
82 | if (this.manager instanceof RedisSessionManager
83 | && ((RedisSessionManager)this.manager).getSaveOnChange()) {
84 | try {
85 | ((RedisSessionManager)this.manager).save(this, true);
86 | } catch (IOException ex) {
87 | log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + ex.getMessage());
88 | }
89 | } else {
90 | dirty = true;
91 | }
92 | }
93 |
94 | @Override
95 | public void setId(String id) {
96 | // Specifically do not call super(): it's implementation does unexpected things
97 | // like calling manager.remove(session.id) and manager.add(session).
98 |
99 | this.id = id;
100 | }
101 |
102 | @Override
103 | public void setPrincipal(Principal principal) {
104 | dirty = true;
105 | super.setPrincipal(principal);
106 | }
107 |
108 | @Override
109 | public void writeObjectData(java.io.ObjectOutputStream out) throws IOException {
110 | super.writeObjectData(out);
111 | out.writeLong(this.getCreationTime());
112 | }
113 |
114 | @Override
115 | public void readObjectData(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
116 | super.readObjectData(in);
117 | this.setCreationTime(in.readLong());
118 | }
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/spec/requests/session_updating_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'session updating' do
4 | it 'should support setting a value in the session' do
5 | post(SESSION_PATH, body: {param1: '5'})
6 | json['attributes'].should have_key('param1')
7 | json['attributes']['param1'].should == '5'
8 | end
9 |
10 | it 'should support updating a value in the session' do
11 | post(SESSION_PATH, body: {param1: '5'})
12 | json['attributes'].should have_key('param1')
13 | json['attributes']['param1'].should == '5'
14 |
15 | put(SESSION_PATH, query: {param1: '6'})
16 | json['attributes']['param1'].should == '6'
17 | end
18 |
19 | it 'should support setting a complex values in the session' do
20 | post(SESSION_PATH, body: {param1: {subparam: '5'}})
21 | json['attributes']['param1'].should have_key('subparam')
22 | json['attributes']['param1']['subparam'].should == '5'
23 | end
24 |
25 | it 'should persist session attributes between requests' do
26 | post(SESSION_PATH, body: {param1: '5'})
27 | get(SESSION_PATH)
28 | json['attributes']['param1'].should == '5'
29 | end
30 |
31 | it 'should persist updated session attributes between requests' do
32 | post(SESSION_PATH, body: {param1: '5'})
33 | put(SESSION_PATH, query: {param1: '6'})
34 | get(SESSION_PATH)
35 | json['attributes']['param1'].should == '6'
36 | end
37 |
38 | it 'should support removing a value in the session' do
39 | post("#{SESSION_ATTRIBUTES_PATH}/param1", body: {value: '5'})
40 | get(SESSION_ATTRIBUTES_PATH)
41 | json['keys'].should include('param1')
42 |
43 | delete("#{SESSION_ATTRIBUTES_PATH}/param1")
44 | get(SESSION_ATTRIBUTES_PATH)
45 | json['keys'].should_not include('param1')
46 | end
47 |
48 | it 'should only update if the session changes' do
49 | post(SESSION_PATH, body: {param1: '5'})
50 |
51 | # This is not a perfect guarantee, but in general we're assuming
52 | # that the requests will happen in the following order:
53 | # - Post(value=5) starts
54 | # - Post(value=6) starts
55 | # - Post(value=6) finishes
56 | # - Get() returns 6
57 | # - Post(value=5) finishes
58 | # Note: this would represent a change from the current persisted value
59 | # but it does not represent a change from when this request's
60 | # copy of the session was loaded, so it shouldn't be persisted.
61 | # - Get() returns 6
62 | last_request_to_finish = Thread.new do
63 | post("#{SESSION_ATTRIBUTES_PATH}/param1", body: {value: '5', sleep: 2000})
64 | end
65 | sleep 1
66 | post("#{SESSION_ATTRIBUTES_PATH}/param1", body: {value: '6'})
67 | # Verify our assumption that this request loaded it's session
68 | # before the request Post(value=7) finished.
69 | json['oldValue'].should == '5'
70 | get("#{SESSION_ATTRIBUTES_PATH}/param1")
71 | json['value'].should == '6'
72 |
73 | last_request_to_finish.join
74 |
75 | get("#{SESSION_ATTRIBUTES_PATH}/param1")
76 | json['value'].should == '6'
77 | end
78 |
79 | it 'should detect nested changes and persist them' do
80 | post(SESSION_PATH, body: {param1: {subparam: '5'}})
81 | json['attributes']['param1']['subparam'].should == '5'
82 | post(SESSION_PATH, body: {param1: {subparam: '6'}})
83 | get(SESSION_PATH)
84 | json['attributes']['param1']['subparam'].should == '6'
85 | end
86 |
87 | it 'should default to last-write-wins behavior for simultaneous updates' do
88 | post(SESSION_PATH, body: {param1: '5'})
89 |
90 | # This is not a perfect guarantee, but in general we're assuming
91 | # that the requests will happen in the following order:
92 | # - Post(value=7) starts
93 | # - Post(value=6) starts
94 | # - Post(value=6) finishes
95 | # - Get() returns 6
96 | # - Post(value=7) finishes
97 | # - Get() returns 7
98 | winner = Thread.new do
99 | post("#{SESSION_ATTRIBUTES_PATH}/param1", body: {value: '7', sleep: 2000})
100 | end
101 | sleep 1
102 | post("#{SESSION_ATTRIBUTES_PATH}/param1", body: {value: '6'})
103 | # Verify our assumption that this request loaded it's session
104 | # before the request Post(value=7) finished.
105 | json['oldValue'].should == '5'
106 | get("#{SESSION_ATTRIBUTES_PATH}/param1")
107 | json['value'].should == '6'
108 |
109 | winner.join
110 |
111 | get("#{SESSION_ATTRIBUTES_PATH}/param1")
112 | json['value'].should == '7'
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/vagrant/tomcat-redis-example/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
5 | VAGRANTFILE_API_VERSION = "2"
6 |
7 | Vagrant.require_version ">= 1.5.0"
8 |
9 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
10 | # All Vagrant configuration is done here. The most common configuration
11 | # options are documented and commented below. For a complete reference,
12 | # please see the online documentation at vagrantup.com.
13 |
14 | config.vm.hostname = "tomcat-redis-example-berkshelf"
15 |
16 | # Set the version of chef to install using the vagrant-omnibus plugin
17 | config.omnibus.chef_version = '11.10'
18 |
19 | # Every Vagrant virtual environment requires a box to build off of.
20 | config.vm.box = "ubuntu/trusty64"
21 |
22 | # The url from where the 'config.vm.box' box will be fetched if it
23 | # doesn't already exist on the user's system.
24 | config.vm.box_url = "http://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box"
25 |
26 | # Assign this VM to a host-only network IP, allowing you to access it
27 | # via the IP. Host-only networks can talk to the host machine as well as
28 | # any other machines on the same network, but cannot be accessed (through this
29 | # network interface) by any external networks.
30 | config.vm.network :private_network, type: "dhcp"
31 |
32 | # Create a forwarded port mapping which allows access to a specific port
33 | # within the machine from a port on the host machine. In the example below,
34 | # accessing "localhost:8080" will access port 80 on the guest machine.
35 | # config.vm.network "forwarded_port", guest: 80, host: 8080
36 |
37 | # Share an additional folder to the guest VM. The first argument is
38 | # the path on the host to the actual folder. The second argument is
39 | # the path on the guest to mount the folder. And the optional third
40 | # argument is a set of non-required options.
41 | # config.vm.synced_folder "../data", "/vagrant_data"
42 | config.vm.synced_folder "../../", "/opt/tomcat-redis-session-manager"
43 |
44 | # Provider-specific configuration so you can fine-tune various
45 | # backing providers for Vagrant. These expose provider-specific options.
46 | # Example for VirtualBox:
47 | #
48 | # config.vm.provider :virtualbox do |vb|
49 | # # Don't boot with headless mode
50 | # vb.gui = true
51 | #
52 | # # Use VBoxManage to customize the VM. For example to change memory:
53 | # vb.customize ["modifyvm", :id, "--memory", "1024"]
54 | # end
55 | #
56 | # View the documentation for the provider you're using for more
57 | # information on available options.
58 |
59 | # The path to the Berksfile to use with Vagrant Berkshelf
60 | # config.berkshelf.berksfile_path = "./Berksfile"
61 |
62 | # Enabling the Berkshelf plugin. To enable this globally, add this configuration
63 | # option to your ~/.vagrant.d/Vagrantfile file
64 | config.berkshelf.enabled = true
65 |
66 | # An array of symbols representing groups of cookbook described in the Vagrantfile
67 | # to exclusively install and copy to Vagrant's shelf.
68 | # config.berkshelf.only = []
69 |
70 | # An array of symbols representing groups of cookbook described in the Vagrantfile
71 | # to skip installing and copying to Vagrant's shelf.
72 | # config.berkshelf.except = []
73 |
74 | config.vm.provision :chef_solo do |chef|
75 | chef.json = {
76 | mysql: {
77 | server_root_password: 'rootpass',
78 | server_debian_password: 'debpass',
79 | server_repl_password: 'replpass'
80 | }
81 | }
82 |
83 | chef.run_list = [
84 | "recipe[tomcat-redis-example::default]"
85 | ]
86 | end
87 |
88 | if Vagrant.has_plugin?("vagrant-cachier")
89 | # Configure cached packages to be shared between instances of the same base box.
90 | # More info on http://fgrehm.viewdocs.io/vagrant-cachier/usage
91 | config.cache.scope = :box
92 |
93 | # If you are using VirtualBox, you might want to use that to enable NFS for
94 | # shared folders. This is also very useful for vagrant-libvirt if you want
95 | # bi-directional sync
96 | config.cache.synced_folder_opts = {
97 | type: :nfs,
98 | # The nolock option can be useful for an NFSv3 client that wants to avoid the
99 | # NLM sideband protocol. Without this option, apt-get might hang if it tries
100 | # to lock files needed for /var/cache/* operations. All of this can be avoided
101 | # by using NFSv4 everywhere. Please note that the tcp option is not the default.
102 | mount_options: ['rw', 'vers=3', 'tcp', 'nolock']
103 | }
104 | # For more information please check http://docs.vagrantup.com/v2/synced-folders/basic_usage.html
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | Redis Session Manager for Apache Tomcat
2 | =======================================
3 |
4 | Overview
5 | --------
6 |
7 | An session manager implementation that stores sessions in Redis for easy distribution of requests across a cluster of Tomcat servers. Sessions are implemented as as non-sticky--that is, each request is able to go to any server in the cluster (unlike the Apache provided Tomcat clustering setup.)
8 |
9 | Sessions are stored into Redis immediately upon creation for use by other servers. Sessions are loaded as requested directly from Redis (but subsequent requests for the session during the same request context will return a ThreadLocal cache rather than hitting Redis multiple times.) In order to prevent collisions (and lost writes) as much as possible, session data is only updated in Redis if the session has been modified.
10 |
11 | The manager relies on the native expiration capability of Redis to expire keys for automatic session expiration to avoid the overhead of constantly searching the entire list of sessions for expired sessions.
12 |
13 | Data stored in the session must be Serializable.
14 |
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 |
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 |
30 | Tomcat Versions
31 | ---------------
32 |
33 | This project supports both Tomcat 6 and Tomcat 7. Starting at project version 1.1, precompiled JAR downloads are available for either version of Tomcat while project versions before 1.1 are only available for Tomcat 6.
34 |
35 | The official release branches in Git are as follows:
36 | * `master`: Continuing work for Tomcat 7 releases. Compatible with Java 7.
37 | * `tomcat-6`: Deprecated; may accept submitted patches, but no new work is being done on this branch. Compatible with Tomcat 6 and Java 6.
38 |
39 | Finalized branches include:
40 | * `tomcat-7`: Has been merged into `master`. Compatible with Java 6 or 7.
41 | * `java-7`: Has been merged into `master`. All of the work from master for Tomcat 7 but taking advantage of new features in Java 7. Compatible with Java 7 only.
42 |
43 | Tomcat 8
44 | --------
45 |
46 | Tomcat 8 is not currently supported and has not been tested or developed for at all In fact, as noted in various bug reports, the project currently fails to compile when linked with Tomcat 8.
47 |
48 | I currently don't have the time to add Tomcat 8 support in my spare time. However if you're interested in Tomcat 8 support for your particular use case and/or business, as the README notes, I'm available as a consultancy on a contractual basis. If you'd like to pursue getting this feature added at a contract rate (and gain some commercial support as well), feel free to contact me at james@orangefunction.com.
49 |
50 | Architecture
51 | ------------
52 |
53 | * RedisSessionManager: provides the session creation, saving, and loading functionality.
54 | * RedisSessionHandlerValve: ensures that sessions are saved after a request is finished processing.
55 |
56 | Note: this architecture differs from the Apache PersistentManager implementation which implements persistent sticky sessions. Because that implementation expects all requests from a specific session to be routed to the same server, the timing persistence of sessions is non-deterministic since it is primarily for failover capabilities.
57 |
58 | Usage
59 | -----
60 |
61 | Add the following into your Tomcat context.xml (or the context block of the server.xml if applicable.)
62 |
63 |
64 |
66 | port="6379"
67 | database="0"
68 | maxInactiveInterval="60"
69 | sessionPersistPolicies="PERSIST_POLICY_1,PERSIST_POLICY_2,.."
70 | sentinelMaster="SentinelMasterName"
71 | sentinels="sentinel-host-1:port,sentinel-host-2:port,.." />
72 |
73 | The Valve must be declared before the Manager.
74 |
75 | Copy the following files into the `TOMCAT_BASE/lib` directory:
76 |
77 | * tomcat-redis-session-manager-VERSION.jar
78 | * jedis-2.5.2.jar
79 | * commons-pool2-2.2.jar
80 |
81 | Reboot the server, and sessions should now be stored in Redis.
82 |
83 | Connection Pool Configuration
84 | -----------------------------
85 |
86 | 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 `` declaration in your Tomcat context.xml.
87 |
88 | Session Change Tracking
89 | -----------------------
90 |
91 | As noted in the "Overview" section above, in order to prevent colliding writes, the Redis Session Manager only serializes the session object into Redis if the session object has changed (it always updates the expiration separately however.) This dirty tracking marks the session as needing serialization according to the following rules:
92 |
93 | * Calling `session.removeAttribute(key)` always marks the session as dirty (needing serialization.)
94 | * Calling `session.setAttribute(key, newAttributeValue)` marks the session as dirty if any of the following are true:
95 | * `previousAttributeValue == null && newAttributeValue != null`
96 | * `previousAttributeValue != null && newAttributeValue == null`
97 | * `!newAttributeValue.getClass().isInstance(previousAttributeValue)`
98 | * `!newAttributeValue.equals(previousAttributeValue)`
99 |
100 | This feature can have the unintended consequence of hiding writes if you implicitly change a key in the session or if the object's equality does not change even though the key is updated. For example, assuming the session already contains the key `"myArray"` with an Array instance as its corresponding value, and has been previously serialized, the following code would not cause the session to be serialized again:
101 |
102 | List myArray = session.getAttribute("myArray");
103 | myArray.add(additionalArrayValue);
104 |
105 | If your code makes these kind of changes, then the RedisSession provides a mechanism by which you can mark the session as dirty in order to guarantee serialization at the end of the request. For example:
106 |
107 | List myArray = session.getAttribute("myArray");
108 | myArray.add(additionalArrayValue);
109 | session.setAttribute("__changed__");
110 |
111 | In order to not cause issues with an application that may already use the key `"__changed__"`, this feature is disabled by default. To enable this feature, simple call the following code in your application's initialization:
112 |
113 | RedisSession.setManualDirtyTrackingSupportEnabled(true);
114 |
115 | This feature also allows the attribute key used to mark the session as dirty to be changed. For example, if you executed the following:
116 |
117 | RedisSession.setManualDirtyTrackingAttributeKey("customDirtyFlag");
118 |
119 | Then the example above would look like this:
120 |
121 | List myArray = session.getAttribute("myArray");
122 | myArray.add(additionalArrayValue);
123 | session.setAttribute("customDirtyFlag");
124 |
125 | Persistence Policies
126 | --------------------
127 |
128 | 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).
129 |
130 | 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.
131 |
132 | - `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.
133 | - `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.
134 |
135 |
136 | Testing/Example App
137 | -------------------
138 |
139 | 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.
140 |
141 | To get the example server up and running, you'll need to do the following:
142 | 1. Download and install Virtual Box (4.3.12 at the time of this writing) from https://www.virtualbox.org/wiki/Downloads
143 | 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
144 | 1. Install Ruby, if necessary.
145 | 1. Install Berkshelf with `gem install berkshelf`
146 | 1. Install the Vagrant Berkshelf plugin with `vagrant plugin install vagrant-berkshelf --plugin-version '>= 2.0.1'`
147 | 1. Install the Vagrant Cachier plugin for _speed_ with `vagrant plugin install vagrant-cachier`
148 | 1. Install the Vagrant Omnibus plugin with `vagrant plugin install vagrant-omnibus`
149 | 1. Install the required Ruby gems with `PROJECT_ROOT/bundle install`
150 | 1. Boot the virtual machine with `PROJECT_ROOT/vagrant up`
151 | 1. Run the tests with `PROJECT_ROOT/rspec`
152 |
153 |
154 | Acknowledgements
155 | ----------------
156 |
157 | The architecture of this project was based on the Mongo-Tomcat-Sessions project found at https://github.com/dawsonsystems/Mongo-Tomcat-Sessions
--------------------------------------------------------------------------------
/example-app/src/main/java/com/orangefunction/tomcatredissessionmanager/exampleapp/WebApp.java:
--------------------------------------------------------------------------------
1 | package com.orangefunction.tomcatredissessionmanager.exampleapp;
2 |
3 | import java.util.Map;
4 | import java.util.Map.Entry;
5 | import java.util.HashMap;
6 | import java.util.Set;
7 | import static spark.Spark.*;
8 | import spark.*;
9 | import redis.clients.jedis.JedisPool;
10 | import redis.clients.jedis.JedisPoolConfig;
11 | import redis.clients.jedis.Jedis;
12 | import redis.clients.jedis.Protocol;
13 | import com.orangefunction.tomcat.redissessions.*;
14 | import org.apache.catalina.session.StandardSession;
15 | import org.apache.catalina.session.StandardSessionFacade;
16 | import org.apache.catalina.core.ApplicationContextFacade;
17 | import org.apache.catalina.core.ApplicationContext;
18 | import org.apache.catalina.core.StandardContext;
19 | import java.lang.reflect.Field;
20 | import javax.servlet.*;
21 |
22 | import org.apache.juli.logging.Log;
23 | import org.apache.juli.logging.LogFactory;
24 |
25 | public class WebApp implements spark.servlet.SparkApplication {
26 |
27 | private final Log log = LogFactory.getLog(WebApp.class);
28 |
29 | protected String redisHost = "localhost";
30 | protected int redisPort = 6379;
31 | protected int redisDatabase = 0;
32 | protected String redisPassword = null;
33 | protected int redisTimeout = Protocol.DEFAULT_TIMEOUT;
34 | protected JedisPool redisConnectionPool;
35 |
36 | private void initializeJedisConnectionPool() {
37 | try {
38 | redisConnectionPool = new JedisPool(new JedisPoolConfig(), redisHost, redisPort, redisTimeout, redisPassword);
39 | } catch (Exception e) {
40 | e.printStackTrace();
41 | }
42 | }
43 |
44 | protected Jedis acquireConnection() {
45 | if (null == redisConnectionPool) {
46 | initializeJedisConnectionPool();
47 | }
48 | Jedis jedis = redisConnectionPool.getResource();
49 |
50 | if (redisDatabase != 0) {
51 | jedis.select(redisDatabase);
52 | }
53 |
54 | return jedis;
55 | }
56 |
57 | protected void returnConnection(Jedis jedis, Boolean error) {
58 | if (error) {
59 | redisConnectionPool.returnBrokenResource(jedis);
60 | } else {
61 | redisConnectionPool.returnResource(jedis);
62 | }
63 | }
64 |
65 | protected void returnConnection(Jedis jedis) {
66 | returnConnection(jedis, false);
67 | }
68 |
69 | protected RedisSessionManager getRedisSessionManager(Request request) {
70 | RedisSessionManager sessionManager = null;
71 | ApplicationContextFacade appContextFacadeObj = (ApplicationContextFacade)request.session().raw().getServletContext();
72 | try {
73 | Field applicationContextField = appContextFacadeObj.getClass().getDeclaredField("context");
74 | applicationContextField.setAccessible(true);
75 | ApplicationContext appContextObj = (ApplicationContext)applicationContextField.get(appContextFacadeObj);
76 | Field standardContextField = appContextObj.getClass().getDeclaredField("context");
77 | standardContextField.setAccessible(true);
78 | StandardContext standardContextObj = (StandardContext)standardContextField.get(appContextObj);
79 | sessionManager = (RedisSessionManager)standardContextObj.getManager();
80 | } catch (Exception e) { }
81 | return sessionManager;
82 | }
83 |
84 | protected void updateSessionFromQueryParamsMap(Session session, QueryParamsMap queryParamsMap) {
85 | for (Entry kv : queryParamsMap.toMap().entrySet()) {
86 | String key = kv.getKey();
87 | QueryParamsMap subParamsMap = queryParamsMap.get(kv.getKey());
88 | if (subParamsMap.hasKeys()) {
89 | Object currentValue = session.attribute(key);
90 | Map subMap;
91 | if (currentValue instanceof Map) {
92 | subMap = (Map)currentValue;
93 | } else {
94 | subMap = new HashMap();
95 | session.attribute(key, subMap);
96 | }
97 | updateMapFromQueryParamsMap(subMap, subParamsMap);
98 | } else if (subParamsMap.hasValue()) {
99 | Object value = subParamsMap.value();
100 | //log.info("found key " + key + " and value " + (null == value ? "`null`" : value.toString()));
101 | session.attribute(key, value);
102 | }
103 | }
104 | }
105 |
106 | protected void updateMapFromQueryParamsMap(Map map, QueryParamsMap queryParamsMap) {
107 | for (Entry kv : queryParamsMap.toMap().entrySet()) {
108 | String key = kv.getKey();
109 | QueryParamsMap subParamsMap = queryParamsMap.get(kv.getKey());
110 | if (subParamsMap.hasKeys()) {
111 | Object currentValue = map.get(key);
112 | Map subMap;
113 | if (currentValue instanceof Map) {
114 | subMap = (Map)currentValue;
115 | } else {
116 | subMap = new HashMap();
117 | map.put(key, subMap);
118 | }
119 | updateMapFromQueryParamsMap(subMap, subParamsMap);
120 | } else if (subParamsMap.hasValue()) {
121 | Object value = subParamsMap.value();
122 | //log.info("found key " + key + " and value " + (null == value ? "`null`" : value.toString()));
123 | map.put(key, value);
124 | }
125 | }
126 | }
127 |
128 | public void init() {
129 |
130 | // /session
131 |
132 | get(new SessionJsonTransformerRoute("/session", "application/json") {
133 | @Override
134 | public Object handle(Request request, Response response) {
135 | return request.session(false);
136 | }
137 | });
138 |
139 |
140 |
141 | put(new SessionJsonTransformerRoute("/session", "application/json") {
142 | @Override
143 | public Object handle(Request request, Response response) {
144 | Session session = request.session();
145 | QueryParamsMap queryMap = request.queryMap();
146 | updateSessionFromQueryParamsMap(session, queryMap);
147 | return session;
148 | }
149 | });
150 |
151 | post(new SessionJsonTransformerRoute("/session", "application/json") {
152 | @Override
153 | public Object handle(Request request, Response response) {
154 | Session session = request.session();
155 | QueryParamsMap queryMap = request.queryMap();
156 | updateSessionFromQueryParamsMap(session, queryMap);
157 | return session;
158 | }
159 | });
160 |
161 | delete(new SessionJsonTransformerRoute("/session", "application/json") {
162 | @Override
163 | public Object handle(Request request, Response response) {
164 | request.session().raw().invalidate();
165 | return null;
166 | }
167 | });
168 |
169 |
170 | // /session/attributes
171 |
172 | get(new SessionJsonTransformerRoute("/session/attributes", "application/json") {
173 | @Override
174 | public Object handle(Request request, Response response) {
175 | HashMap map = new HashMap();
176 | map.put("keys", request.session().attributes());
177 | return new Object[]{request.session(), map};
178 | }
179 | });
180 |
181 | get(new SessionJsonTransformerRoute("/session/attributes/:key", "application/json") {
182 | @Override
183 | public Object handle(Request request, Response response) {
184 | String key = request.params(":key");
185 | HashMap map = new HashMap();
186 | map.put("key", key);
187 | map.put("value", request.session().attribute(key));
188 | return new Object[]{request.session(), map};
189 | }
190 | });
191 |
192 | post(new SessionJsonTransformerRoute("/session/attributes/:key", "application/json") {
193 | @Override
194 | public Object handle(Request request, Response response) {
195 | String key = request.params(":key");
196 | String oldValue = request.session().attribute(key);
197 | request.session().attribute(key, request.queryParams("value"));
198 | HashMap map = new HashMap();
199 | map.put("key", key);
200 | map.put("value", request.session().attribute(key));
201 | map.put("oldValue", oldValue);
202 | if (null != request.queryParams("sleep")) {
203 | try {
204 | java.lang.Thread.sleep(Integer.parseInt(request.queryParams("sleep")));
205 | } catch (InterruptedException e) {}
206 | }
207 | return new Object[]{request.session(), map};
208 | }
209 | });
210 |
211 | delete(new SessionJsonTransformerRoute("/session/attributes/:key", "application/json") {
212 | @Override
213 | public Object handle(Request request, Response response) {
214 | String key = request.params(":key");
215 | String oldValue = request.session().attribute(key);
216 | request.session().raw().removeAttribute(key);
217 | HashMap map = new HashMap();
218 | map.put("key", key);
219 | map.put("value", request.session().attribute(key));
220 | map.put("oldValue", oldValue);
221 | return new Object[]{request.session(), map};
222 | }
223 | });
224 |
225 |
226 | // /sessions
227 |
228 | get(new JsonTransformerRoute("/sessions", "application/json") {
229 | @Override
230 | public Object handle(Request request, Response response) {
231 | Jedis jedis = null;
232 | Boolean error = true;
233 | try {
234 | jedis = acquireConnection();
235 | Set keySet = jedis.keys("*");
236 | error = false;
237 | return keySet.toArray(new String[keySet.size()]);
238 | } finally {
239 | if (jedis != null) {
240 | returnConnection(jedis, error);
241 | }
242 | }
243 | }
244 | });
245 |
246 | delete(new JsonTransformerRoute("/sessions", "application/json") {
247 | @Override
248 | public Object handle(Request request, Response response) {
249 | Jedis jedis = null;
250 | Boolean error = true;
251 | try {
252 | jedis = acquireConnection();
253 | jedis.flushDB();
254 | Set keySet = jedis.keys("*");
255 | error = false;
256 | return keySet.toArray(new String[keySet.size()]);
257 | } finally {
258 | if (jedis != null) {
259 | returnConnection(jedis, error);
260 | }
261 | }
262 | }
263 | });
264 |
265 |
266 | // /settings
267 |
268 | get(new SessionJsonTransformerRoute("/settings/:key", "application/json") {
269 | @Override
270 | public Object handle(Request request, Response response) {
271 | String key = request.params(":key");
272 | HashMap map = new HashMap();
273 | map.put("key", key);
274 |
275 | RedisSessionManager manager = getRedisSessionManager(request);
276 | if (null != manager) {
277 | if (key.equals("sessionPersistPolicies")) {
278 | map.put("value", manager.getSessionPersistPolicies());
279 | } else if (key.equals("maxInactiveInterval")) {
280 | map.put("value", new Integer(manager.getMaxInactiveInterval()));
281 | }
282 | } else {
283 | map.put("error", new Boolean(true));
284 | }
285 |
286 | return new Object[]{request.session(), map};
287 | }
288 | });
289 |
290 | post(new SessionJsonTransformerRoute("/settings/:key", "application/json") {
291 | @Override
292 | public Object handle(Request request, Response response) {
293 | String key = request.params(":key");
294 | String value = request.queryParams("value");
295 | HashMap map = new HashMap();
296 | map.put("key", key);
297 |
298 | RedisSessionManager manager = getRedisSessionManager(request);
299 | if (null != manager) {
300 | if (key.equals("sessionPersistPolicies")) {
301 | manager.setSessionPersistPolicies(value);
302 | map.put("value", manager.getSessionPersistPolicies());
303 | } else if (key.equals("maxInactiveInterval")) {
304 | manager.setMaxInactiveInterval(Integer.parseInt(value));
305 | map.put("value", new Integer(manager.getMaxInactiveInterval()));
306 | }
307 | } else {
308 | map.put("error", new Boolean(true));
309 | }
310 |
311 | return new Object[]{request.session(), map};
312 | }
313 | });
314 |
315 | }
316 |
317 | }
318 |
--------------------------------------------------------------------------------
/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionManager.java:
--------------------------------------------------------------------------------
1 | package com.orangefunction.tomcat.redissessions;
2 |
3 | import org.apache.catalina.Lifecycle;
4 | import org.apache.catalina.LifecycleException;
5 | import org.apache.catalina.LifecycleListener;
6 | import org.apache.catalina.util.LifecycleSupport;
7 | import org.apache.catalina.LifecycleState;
8 | import org.apache.catalina.Loader;
9 | import org.apache.catalina.Valve;
10 | import org.apache.catalina.Session;
11 | import org.apache.catalina.session.ManagerBase;
12 |
13 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
14 | import org.apache.commons.pool2.impl.BaseObjectPoolConfig;
15 |
16 | import redis.clients.util.Pool;
17 | import redis.clients.jedis.JedisPool;
18 | import redis.clients.jedis.JedisSentinelPool;
19 | import redis.clients.jedis.JedisPoolConfig;
20 | import redis.clients.jedis.Jedis;
21 | import redis.clients.jedis.Protocol;
22 |
23 | import java.io.IOException;
24 | import java.util.Arrays;
25 | import java.util.Collections;
26 | import java.util.Enumeration;
27 | import java.util.Set;
28 | import java.util.EnumSet;
29 | import java.util.HashSet;
30 | import java.util.Iterator;
31 |
32 | import org.apache.juli.logging.Log;
33 | import org.apache.juli.logging.LogFactory;
34 |
35 |
36 | public class RedisSessionManager extends ManagerBase implements Lifecycle {
37 |
38 | enum SessionPersistPolicy {
39 | DEFAULT,
40 | SAVE_ON_CHANGE,
41 | ALWAYS_SAVE_AFTER_REQUEST;
42 |
43 | static SessionPersistPolicy fromName(String name) {
44 | for (SessionPersistPolicy policy : SessionPersistPolicy.values()) {
45 | if (policy.name().equalsIgnoreCase(name)) {
46 | return policy;
47 | }
48 | }
49 | throw new IllegalArgumentException("Invalid session persist policy [" + name + "]. Must be one of " + Arrays.asList(SessionPersistPolicy.values())+ ".");
50 | }
51 | }
52 |
53 | protected byte[] NULL_SESSION = "null".getBytes();
54 |
55 | private final Log log = LogFactory.getLog(RedisSessionManager.class);
56 |
57 | protected String host = "localhost";
58 | protected int port = 6379;
59 | protected int database = 0;
60 | protected String password = null;
61 | protected int timeout = Protocol.DEFAULT_TIMEOUT;
62 | protected String sentinelMaster = null;
63 | Set sentinelSet = null;
64 |
65 | protected Pool connectionPool;
66 | protected JedisPoolConfig connectionPoolConfig = new JedisPoolConfig();
67 |
68 | protected RedisSessionHandlerValve handlerValve;
69 | protected ThreadLocal currentSession = new ThreadLocal<>();
70 | protected ThreadLocal currentSessionSerializationMetadata = new ThreadLocal<>();
71 | protected ThreadLocal currentSessionId = new ThreadLocal<>();
72 | protected ThreadLocal currentSessionIsPersisted = new ThreadLocal<>();
73 | protected Serializer serializer;
74 |
75 | protected static String name = "RedisSessionManager";
76 |
77 | protected String serializationStrategyClass = "com.orangefunction.tomcat.redissessions.JavaSerializer";
78 |
79 | protected EnumSet sessionPersistPoliciesSet = EnumSet.of(SessionPersistPolicy.DEFAULT);
80 |
81 | /**
82 | * The lifecycle event support for this component.
83 | */
84 | protected LifecycleSupport lifecycle = new LifecycleSupport(this);
85 |
86 | public String getHost() {
87 | return host;
88 | }
89 |
90 | public void setHost(String host) {
91 | this.host = host;
92 | }
93 |
94 | public int getPort() {
95 | return port;
96 | }
97 |
98 | public void setPort(int port) {
99 | this.port = port;
100 | }
101 |
102 | public int getDatabase() {
103 | return database;
104 | }
105 |
106 | public void setDatabase(int database) {
107 | this.database = database;
108 | }
109 |
110 | public int getTimeout() {
111 | return timeout;
112 | }
113 |
114 | public void setTimeout(int timeout) {
115 | this.timeout = timeout;
116 | }
117 |
118 | public String getPassword() {
119 | return password;
120 | }
121 |
122 | public void setPassword(String password) {
123 | this.password = password;
124 | }
125 |
126 | public void setSerializationStrategyClass(String strategy) {
127 | this.serializationStrategyClass = strategy;
128 | }
129 |
130 | public String getSessionPersistPolicies() {
131 | StringBuilder policies = new StringBuilder();
132 | for (Iterator iter = this.sessionPersistPoliciesSet.iterator(); iter.hasNext();) {
133 | SessionPersistPolicy policy = iter.next();
134 | policies.append(policy.name());
135 | if (iter.hasNext()) {
136 | policies.append(",");
137 | }
138 | }
139 | return policies.toString();
140 | }
141 |
142 | public void setSessionPersistPolicies(String sessionPersistPolicies) {
143 | String[] policyArray = sessionPersistPolicies.split(",");
144 | EnumSet policySet = EnumSet.of(SessionPersistPolicy.DEFAULT);
145 | for (String policyName : policyArray) {
146 | SessionPersistPolicy policy = SessionPersistPolicy.fromName(policyName);
147 | policySet.add(policy);
148 | }
149 | this.sessionPersistPoliciesSet = policySet;
150 | }
151 |
152 | public boolean getSaveOnChange() {
153 | return this.sessionPersistPoliciesSet.contains(SessionPersistPolicy.SAVE_ON_CHANGE);
154 | }
155 |
156 | public boolean getAlwaysSaveAfterRequest() {
157 | return this.sessionPersistPoliciesSet.contains(SessionPersistPolicy.ALWAYS_SAVE_AFTER_REQUEST);
158 | }
159 |
160 | public String getSentinels() {
161 | StringBuilder sentinels = new StringBuilder();
162 | for (Iterator iter = this.sentinelSet.iterator(); iter.hasNext();) {
163 | sentinels.append(iter.next());
164 | if (iter.hasNext()) {
165 | sentinels.append(",");
166 | }
167 | }
168 | return sentinels.toString();
169 | }
170 |
171 | public void setSentinels(String sentinels) {
172 | if (null == sentinels) {
173 | sentinels = "";
174 | }
175 |
176 | String[] sentinelArray = sentinels.split(",");
177 | this.sentinelSet = new HashSet(Arrays.asList(sentinelArray));
178 | }
179 |
180 | public Set getSentinelSet() {
181 | return this.sentinelSet;
182 | }
183 |
184 | public String getSentinelMaster() {
185 | return this.sentinelMaster;
186 | }
187 |
188 | public void setSentinelMaster(String master) {
189 | this.sentinelMaster = master;
190 | }
191 |
192 | @Override
193 | public int getRejectedSessions() {
194 | // Essentially do nothing.
195 | return 0;
196 | }
197 |
198 | public void setRejectedSessions(int i) {
199 | // Do nothing.
200 | }
201 |
202 | protected Jedis acquireConnection() {
203 | Jedis jedis = connectionPool.getResource();
204 |
205 | if (getDatabase() != 0) {
206 | jedis.select(getDatabase());
207 | }
208 |
209 | return jedis;
210 | }
211 |
212 | protected void returnConnection(Jedis jedis, Boolean error) {
213 | if (error) {
214 | connectionPool.returnBrokenResource(jedis);
215 | } else {
216 | connectionPool.returnResource(jedis);
217 | }
218 | }
219 |
220 | protected void returnConnection(Jedis jedis) {
221 | returnConnection(jedis, false);
222 | }
223 |
224 | @Override
225 | public void load() throws ClassNotFoundException, IOException {
226 |
227 | }
228 |
229 | @Override
230 | public void unload() throws IOException {
231 |
232 | }
233 |
234 | /**
235 | * Add a lifecycle event listener to this component.
236 | *
237 | * @param listener The listener to add
238 | */
239 | @Override
240 | public void addLifecycleListener(LifecycleListener listener) {
241 | lifecycle.addLifecycleListener(listener);
242 | }
243 |
244 | /**
245 | * Get the lifecycle listeners associated with this lifecycle. If this
246 | * Lifecycle has no listeners registered, a zero-length array is returned.
247 | */
248 | @Override
249 | public LifecycleListener[] findLifecycleListeners() {
250 | return lifecycle.findLifecycleListeners();
251 | }
252 |
253 |
254 | /**
255 | * Remove a lifecycle event listener from this component.
256 | *
257 | * @param listener The listener to remove
258 | */
259 | @Override
260 | public void removeLifecycleListener(LifecycleListener listener) {
261 | lifecycle.removeLifecycleListener(listener);
262 | }
263 |
264 | /**
265 | * Start this component and implement the requirements
266 | * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
267 | *
268 | * @exception LifecycleException if this component detects a fatal error
269 | * that prevents this component from being used
270 | */
271 | @Override
272 | protected synchronized void startInternal() throws LifecycleException {
273 | super.startInternal();
274 |
275 | setState(LifecycleState.STARTING);
276 |
277 | Boolean attachedToValve = false;
278 | for (Valve valve : getContainer().getPipeline().getValves()) {
279 | if (valve instanceof RedisSessionHandlerValve) {
280 | this.handlerValve = (RedisSessionHandlerValve) valve;
281 | this.handlerValve.setRedisSessionManager(this);
282 | log.info("Attached to RedisSessionHandlerValve");
283 | attachedToValve = true;
284 | break;
285 | }
286 | }
287 |
288 | if (!attachedToValve) {
289 | String error = "Unable to attach to session handling valve; sessions cannot be saved after the request without the valve starting properly.";
290 | log.fatal(error);
291 | throw new LifecycleException(error);
292 | }
293 |
294 | try {
295 | initializeSerializer();
296 | } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
297 | log.fatal("Unable to load serializer", e);
298 | throw new LifecycleException(e);
299 | }
300 |
301 | log.info("Will expire sessions after " + getMaxInactiveInterval() + " seconds");
302 |
303 | initializeDatabaseConnection();
304 |
305 | setDistributable(true);
306 | }
307 |
308 |
309 | /**
310 | * Stop this component and implement the requirements
311 | * of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
312 | *
313 | * @exception LifecycleException if this component detects a fatal error
314 | * that prevents this component from being used
315 | */
316 | @Override
317 | protected synchronized void stopInternal() throws LifecycleException {
318 | if (log.isDebugEnabled()) {
319 | log.debug("Stopping");
320 | }
321 |
322 | setState(LifecycleState.STOPPING);
323 |
324 | try {
325 | connectionPool.destroy();
326 | } catch(Exception e) {
327 | // Do nothing.
328 | }
329 |
330 | // Require a new random number generator if we are restarted
331 | super.stopInternal();
332 | }
333 |
334 | @Override
335 | public Session createSession(String requestedSessionId) {
336 | RedisSession session = null;
337 | String sessionId = null;
338 | String jvmRoute = getJvmRoute();
339 |
340 | Boolean error = true;
341 | Jedis jedis = null;
342 | try {
343 | jedis = acquireConnection();
344 |
345 | // Ensure generation of a unique session identifier.
346 | if (null != requestedSessionId) {
347 | sessionId = sessionIdWithJvmRoute(requestedSessionId, jvmRoute);
348 | if (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L) {
349 | sessionId = null;
350 | }
351 | } else {
352 | do {
353 | sessionId = sessionIdWithJvmRoute(generateSessionId(), jvmRoute);
354 | } while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L); // 1 = key set; 0 = key already existed
355 | }
356 |
357 | /* Even though the key is set in Redis, we are not going to flag
358 | the current thread as having had the session persisted since
359 | the session isn't actually serialized to Redis yet.
360 | This ensures that the save(session) at the end of the request
361 | will serialize the session into Redis with 'set' instead of 'setnx'. */
362 |
363 | error = false;
364 |
365 | if (null != sessionId) {
366 | session = (RedisSession)createEmptySession();
367 | session.setNew(true);
368 | session.setValid(true);
369 | session.setCreationTime(System.currentTimeMillis());
370 | session.setMaxInactiveInterval(getMaxInactiveInterval());
371 | session.setId(sessionId);
372 | session.tellNew();
373 | }
374 |
375 | currentSession.set(session);
376 | currentSessionId.set(sessionId);
377 | currentSessionIsPersisted.set(false);
378 | currentSessionSerializationMetadata.set(new SessionSerializationMetadata());
379 |
380 | if (null != session) {
381 | try {
382 | error = saveInternal(jedis, session, true);
383 | } catch (IOException ex) {
384 | log.error("Error saving newly created session: " + ex.getMessage());
385 | currentSession.set(null);
386 | currentSessionId.set(null);
387 | session = null;
388 | }
389 | }
390 | } finally {
391 | if (jedis != null) {
392 | returnConnection(jedis, error);
393 | }
394 | }
395 |
396 | return session;
397 | }
398 |
399 | private String sessionIdWithJvmRoute(String sessionId, String jvmRoute) {
400 | if (jvmRoute != null) {
401 | String jvmRoutePrefix = '.' + jvmRoute;
402 | return sessionId.endsWith(jvmRoutePrefix) ? sessionId : sessionId + jvmRoutePrefix;
403 | }
404 | return sessionId;
405 | }
406 |
407 | @Override
408 | public Session createEmptySession() {
409 | return new RedisSession(this);
410 | }
411 |
412 | @Override
413 | public void add(Session session) {
414 | try {
415 | save(session);
416 | } catch (IOException ex) {
417 | log.warn("Unable to add to session manager store: " + ex.getMessage());
418 | throw new RuntimeException("Unable to add to session manager store.", ex);
419 | }
420 | }
421 |
422 | @Override
423 | public Session findSession(String id) throws IOException {
424 | RedisSession session = null;
425 |
426 | if (null == id) {
427 | currentSessionIsPersisted.set(false);
428 | currentSession.set(null);
429 | currentSessionSerializationMetadata.set(null);
430 | currentSessionId.set(null);
431 | } else if (id.equals(currentSessionId.get())) {
432 | session = currentSession.get();
433 | } else {
434 | byte[] data = loadSessionDataFromRedis(id);
435 | if (data != null) {
436 | DeserializedSessionContainer container = sessionFromSerializedData(id, data);
437 | session = container.session;
438 | currentSession.set(session);
439 | currentSessionSerializationMetadata.set(container.metadata);
440 | currentSessionIsPersisted.set(true);
441 | currentSessionId.set(id);
442 | } else {
443 | currentSessionIsPersisted.set(false);
444 | currentSession.set(null);
445 | currentSessionSerializationMetadata.set(null);
446 | currentSessionId.set(null);
447 | }
448 | }
449 |
450 | return session;
451 | }
452 |
453 | public void clear() {
454 | Jedis jedis = null;
455 | Boolean error = true;
456 | try {
457 | jedis = acquireConnection();
458 | jedis.flushDB();
459 | error = false;
460 | } finally {
461 | if (jedis != null) {
462 | returnConnection(jedis, error);
463 | }
464 | }
465 | }
466 |
467 | public int getSize() throws IOException {
468 | Jedis jedis = null;
469 | Boolean error = true;
470 | try {
471 | jedis = acquireConnection();
472 | int size = jedis.dbSize().intValue();
473 | error = false;
474 | return size;
475 | } finally {
476 | if (jedis != null) {
477 | returnConnection(jedis, error);
478 | }
479 | }
480 | }
481 |
482 | public String[] keys() throws IOException {
483 | Jedis jedis = null;
484 | Boolean error = true;
485 | try {
486 | jedis = acquireConnection();
487 | Set keySet = jedis.keys("*");
488 | error = false;
489 | return keySet.toArray(new String[keySet.size()]);
490 | } finally {
491 | if (jedis != null) {
492 | returnConnection(jedis, error);
493 | }
494 | }
495 | }
496 |
497 | public byte[] loadSessionDataFromRedis(String id) throws IOException {
498 | Jedis jedis = null;
499 | Boolean error = true;
500 |
501 | try {
502 | log.trace("Attempting to load session " + id + " from Redis");
503 |
504 | jedis = acquireConnection();
505 | byte[] data = jedis.get(id.getBytes());
506 | error = false;
507 |
508 | if (data == null) {
509 | log.trace("Session " + id + " not found in Redis");
510 | }
511 |
512 | return data;
513 | } finally {
514 | if (jedis != null) {
515 | returnConnection(jedis, error);
516 | }
517 | }
518 | }
519 |
520 | public DeserializedSessionContainer sessionFromSerializedData(String id, byte[] data) throws IOException {
521 | log.trace("Deserializing session " + id + " from Redis");
522 |
523 | if (Arrays.equals(NULL_SESSION, data)) {
524 | log.error("Encountered serialized session " + id + " with data equal to NULL_SESSION. This is a bug.");
525 | throw new IOException("Serialized session data was equal to NULL_SESSION");
526 | }
527 |
528 | RedisSession session = null;
529 | SessionSerializationMetadata metadata = new SessionSerializationMetadata();
530 |
531 | try {
532 | session = (RedisSession)createEmptySession();
533 |
534 | serializer.deserializeInto(data, session, metadata);
535 |
536 | session.setId(id);
537 | session.setNew(false);
538 | session.setMaxInactiveInterval(getMaxInactiveInterval());
539 | session.access();
540 | session.setValid(true);
541 | session.resetDirtyTracking();
542 |
543 | if (log.isTraceEnabled()) {
544 | log.trace("Session Contents [" + id + "]:");
545 | Enumeration en = session.getAttributeNames();
546 | while(en.hasMoreElements()) {
547 | log.trace(" " + en.nextElement());
548 | }
549 | }
550 | } catch (ClassNotFoundException ex) {
551 | log.fatal("Unable to deserialize into session", ex);
552 | throw new IOException("Unable to deserialize into session", ex);
553 | }
554 |
555 | return new DeserializedSessionContainer(session, metadata);
556 | }
557 |
558 | public void save(Session session) throws IOException {
559 | save(session, false);
560 | }
561 |
562 | public void save(Session session, boolean forceSave) throws IOException {
563 | Jedis jedis = null;
564 | Boolean error = true;
565 |
566 | try {
567 | jedis = acquireConnection();
568 | error = saveInternal(jedis, session, forceSave);
569 | } catch (IOException e) {
570 | throw e;
571 | } finally {
572 | if (jedis != null) {
573 | returnConnection(jedis, error);
574 | }
575 | }
576 | }
577 |
578 | protected boolean saveInternal(Jedis jedis, Session session, boolean forceSave) throws IOException {
579 | Boolean error = true;
580 |
581 | try {
582 | log.trace("Saving session " + session + " into Redis");
583 |
584 | RedisSession redisSession = (RedisSession)session;
585 |
586 | if (log.isTraceEnabled()) {
587 | log.trace("Session Contents [" + redisSession.getId() + "]:");
588 | Enumeration en = redisSession.getAttributeNames();
589 | while(en.hasMoreElements()) {
590 | log.trace(" " + en.nextElement());
591 | }
592 | }
593 |
594 | byte[] binaryId = redisSession.getId().getBytes();
595 |
596 | Boolean isCurrentSessionPersisted;
597 | SessionSerializationMetadata sessionSerializationMetadata = currentSessionSerializationMetadata.get();
598 | byte[] originalSessionAttributesHash = sessionSerializationMetadata.getSessionAttributesHash();
599 | byte[] sessionAttributesHash = null;
600 | if (
601 | forceSave
602 | || redisSession.isDirty()
603 | || null == (isCurrentSessionPersisted = this.currentSessionIsPersisted.get())
604 | || !isCurrentSessionPersisted
605 | || !Arrays.equals(originalSessionAttributesHash, (sessionAttributesHash = serializer.attributesHashFrom(redisSession)))
606 | ) {
607 |
608 | log.trace("Save was determined to be necessary");
609 |
610 | if (null == sessionAttributesHash) {
611 | sessionAttributesHash = serializer.attributesHashFrom(redisSession);
612 | }
613 |
614 | SessionSerializationMetadata updatedSerializationMetadata = new SessionSerializationMetadata();
615 | updatedSerializationMetadata.setSessionAttributesHash(sessionAttributesHash);
616 |
617 | jedis.set(binaryId, serializer.serializeFrom(redisSession, updatedSerializationMetadata));
618 |
619 | redisSession.resetDirtyTracking();
620 | currentSessionSerializationMetadata.set(updatedSerializationMetadata);
621 | currentSessionIsPersisted.set(true);
622 | } else {
623 | log.trace("Save was determined to be unnecessary");
624 | }
625 |
626 | log.trace("Setting expire timeout on session [" + redisSession.getId() + "] to " + getMaxInactiveInterval());
627 | jedis.expire(binaryId, getMaxInactiveInterval());
628 |
629 | error = false;
630 |
631 | return error;
632 | } catch (IOException e) {
633 | log.error(e.getClass().getName() + ": " + e.getMessage());
634 |
635 | throw e;
636 | } finally {
637 | return error;
638 | }
639 | }
640 |
641 | @Override
642 | public void remove(Session session) {
643 | remove(session, false);
644 | }
645 |
646 | @Override
647 | public void remove(Session session, boolean update) {
648 | Jedis jedis = null;
649 | Boolean error = true;
650 |
651 | log.trace("Removing session ID : " + session.getId());
652 |
653 | try {
654 | jedis = acquireConnection();
655 | jedis.del(session.getId());
656 | error = false;
657 | } finally {
658 | if (jedis != null) {
659 | returnConnection(jedis, error);
660 | }
661 | }
662 | }
663 |
664 | public void afterRequest() {
665 | RedisSession redisSession = currentSession.get();
666 | if (redisSession != null) {
667 | try {
668 | if (redisSession.isValid()) {
669 | log.trace("Request with session completed, saving session " + redisSession.getId());
670 | save(redisSession, getAlwaysSaveAfterRequest());
671 | } else {
672 | log.trace("HTTP Session has been invalidated, removing :" + redisSession.getId());
673 | remove(redisSession);
674 | }
675 | } catch (Exception e) {
676 | log.error("Error storing/removing session", e);
677 | } finally {
678 | currentSession.remove();
679 | currentSessionId.remove();
680 | currentSessionIsPersisted.remove();
681 | log.trace("Session removed from ThreadLocal :" + redisSession.getIdInternal());
682 | }
683 | }
684 | }
685 |
686 | @Override
687 | public void processExpires() {
688 | // We are going to use Redis's ability to expire keys for session expiration.
689 |
690 | // Do nothing.
691 | }
692 |
693 | private void initializeDatabaseConnection() throws LifecycleException {
694 | try {
695 | if (getSentinelMaster() != null) {
696 | Set sentinelSet = getSentinelSet();
697 | if (sentinelSet != null && sentinelSet.size() > 0) {
698 | connectionPool = new JedisSentinelPool(getSentinelMaster(), sentinelSet, this.connectionPoolConfig, getTimeout(), getPassword());
699 | } else {
700 | throw new LifecycleException("Error configuring Redis Sentinel connection pool: expected both `sentinelMaster` and `sentiels` to be configured");
701 | }
702 | } else {
703 | connectionPool = new JedisPool(this.connectionPoolConfig, getHost(), getPort(), getTimeout(), getPassword());
704 | }
705 | } catch (Exception e) {
706 | e.printStackTrace();
707 | throw new LifecycleException("Error connecting to Redis", e);
708 | }
709 | }
710 |
711 | private void initializeSerializer() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
712 | log.info("Attempting to use serializer :" + serializationStrategyClass);
713 | serializer = (Serializer) Class.forName(serializationStrategyClass).newInstance();
714 |
715 | Loader loader = null;
716 |
717 | if (getContainer() != null) {
718 | loader = getContainer().getLoader();
719 | }
720 |
721 | ClassLoader classLoader = null;
722 |
723 | if (loader != null) {
724 | classLoader = loader.getClassLoader();
725 | }
726 | serializer.setClassLoader(classLoader);
727 | }
728 |
729 |
730 | // Connection Pool Config Accessors
731 |
732 | // - from org.apache.commons.pool2.impl.GenericObjectPoolConfig
733 |
734 | public int getConnectionPoolMaxTotal() {
735 | return this.connectionPoolConfig.getMaxTotal();
736 | }
737 |
738 | public void setConnectionPoolMaxTotal(int connectionPoolMaxTotal) {
739 | this.connectionPoolConfig.setMaxTotal(connectionPoolMaxTotal);
740 | }
741 |
742 | public int getConnectionPoolMaxIdle() {
743 | return this.connectionPoolConfig.getMaxIdle();
744 | }
745 |
746 | public void setConnectionPoolMaxIdle(int connectionPoolMaxIdle) {
747 | this.connectionPoolConfig.setMaxIdle(connectionPoolMaxIdle);
748 | }
749 |
750 | public int getConnectionPoolMinIdle() {
751 | return this.connectionPoolConfig.getMinIdle();
752 | }
753 |
754 | public void setConnectionPoolMinIdle(int connectionPoolMinIdle) {
755 | this.connectionPoolConfig.setMinIdle(connectionPoolMinIdle);
756 | }
757 |
758 |
759 | // - from org.apache.commons.pool2.impl.BaseObjectPoolConfig
760 |
761 | public boolean getLifo() {
762 | return this.connectionPoolConfig.getLifo();
763 | }
764 | public void setLifo(boolean lifo) {
765 | this.connectionPoolConfig.setLifo(lifo);
766 | }
767 | public long getMaxWaitMillis() {
768 | return this.connectionPoolConfig.getMaxWaitMillis();
769 | }
770 |
771 | public void setMaxWaitMillis(long maxWaitMillis) {
772 | this.connectionPoolConfig.setMaxWaitMillis(maxWaitMillis);
773 | }
774 |
775 | public long getMinEvictableIdleTimeMillis() {
776 | return this.connectionPoolConfig.getMinEvictableIdleTimeMillis();
777 | }
778 |
779 | public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
780 | this.connectionPoolConfig.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
781 | }
782 |
783 | public long getSoftMinEvictableIdleTimeMillis() {
784 | return this.connectionPoolConfig.getSoftMinEvictableIdleTimeMillis();
785 | }
786 |
787 | public void setSoftMinEvictableIdleTimeMillis(long softMinEvictableIdleTimeMillis) {
788 | this.connectionPoolConfig.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis);
789 | }
790 |
791 | public int getNumTestsPerEvictionRun() {
792 | return this.connectionPoolConfig.getNumTestsPerEvictionRun();
793 | }
794 |
795 | public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) {
796 | this.connectionPoolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
797 | }
798 |
799 | public boolean getTestOnCreate() {
800 | return this.connectionPoolConfig.getTestOnCreate();
801 | }
802 |
803 | public void setTestOnCreate(boolean testOnCreate) {
804 | this.connectionPoolConfig.setTestOnCreate(testOnCreate);
805 | }
806 |
807 | public boolean getTestOnBorrow() {
808 | return this.connectionPoolConfig.getTestOnBorrow();
809 | }
810 |
811 | public void setTestOnBorrow(boolean testOnBorrow) {
812 | this.connectionPoolConfig.setTestOnBorrow(testOnBorrow);
813 | }
814 |
815 | public boolean getTestOnReturn() {
816 | return this.connectionPoolConfig.getTestOnReturn();
817 | }
818 |
819 | public void setTestOnReturn(boolean testOnReturn) {
820 | this.connectionPoolConfig.setTestOnReturn(testOnReturn);
821 | }
822 |
823 | public boolean getTestWhileIdle() {
824 | return this.connectionPoolConfig.getTestWhileIdle();
825 | }
826 |
827 | public void setTestWhileIdle(boolean testWhileIdle) {
828 | this.connectionPoolConfig.setTestWhileIdle(testWhileIdle);
829 | }
830 |
831 | public long getTimeBetweenEvictionRunsMillis() {
832 | return this.connectionPoolConfig.getTimeBetweenEvictionRunsMillis();
833 | }
834 |
835 | public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
836 | this.connectionPoolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
837 | }
838 |
839 | public String getEvictionPolicyClassName() {
840 | return this.connectionPoolConfig.getEvictionPolicyClassName();
841 | }
842 |
843 | public void setEvictionPolicyClassName(String evictionPolicyClassName) {
844 | this.connectionPoolConfig.setEvictionPolicyClassName(evictionPolicyClassName);
845 | }
846 |
847 | public boolean getBlockWhenExhausted() {
848 | return this.connectionPoolConfig.getBlockWhenExhausted();
849 | }
850 |
851 | public void setBlockWhenExhausted(boolean blockWhenExhausted) {
852 | this.connectionPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
853 | }
854 |
855 | public boolean getJmxEnabled() {
856 | return this.connectionPoolConfig.getJmxEnabled();
857 | }
858 |
859 | public void setJmxEnabled(boolean jmxEnabled) {
860 | this.connectionPoolConfig.setJmxEnabled(jmxEnabled);
861 | }
862 | public String getJmxNameBase() {
863 | return this.connectionPoolConfig.getJmxNameBase();
864 | }
865 | public void setJmxNameBase(String jmxNameBase) {
866 | this.connectionPoolConfig.setJmxNameBase(jmxNameBase);
867 | }
868 |
869 | public String getJmxNamePrefix() {
870 | return this.connectionPoolConfig.getJmxNamePrefix();
871 | }
872 |
873 | public void setJmxNamePrefix(String jmxNamePrefix) {
874 | this.connectionPoolConfig.setJmxNamePrefix(jmxNamePrefix);
875 | }
876 | }
877 |
878 | class DeserializedSessionContainer {
879 | public final RedisSession session;
880 | public final SessionSerializationMetadata metadata;
881 | public DeserializedSessionContainer(RedisSession session, SessionSerializationMetadata metadata) {
882 | this.session = session;
883 | this.metadata = metadata;
884 | }
885 | }
886 |
--------------------------------------------------------------------------------