├── 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 | Click here to lend your support to: Tomcat Redis Session Manager and make a donation at pledgie.com ! 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 | --------------------------------------------------------------------------------