Skip to content

Commit

Permalink
added: adapter for java spring mvc (still lack ut for JavaSpringAdapter)
Browse files Browse the repository at this point in the history
  • Loading branch information
汪婧泠 committed Aug 31, 2016
1 parent 5667173 commit e62ecf2
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 49 deletions.
142 changes: 108 additions & 34 deletions lib/redis-session-store.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'redis'
require 'securerandom'

# Redis session storage for Rails, and for Rails only. Derived from
# the MemCacheStore code, simply dropping in Redis instead.
Expand All @@ -18,11 +19,13 @@ class RedisSessionStore < ActionDispatch::Session::AbstractStore
# * +:redis+ - A hash with redis-specific options
# * +:url+ - Redis url, default is redis://localhost:6379/0
# * +:key_prefix+ - Prefix for keys used in Redis, e.g. +myapp:+
# * +:hashkey_prefix+ - Prefix for hashkeys if session saved as hash
# * +:expire_after+ - A number in seconds for session timeout
# * +:client+ - Connect to Redis with given object rather than create one
# * +:on_redis_down:+ - Called with err, env, and SID on Errno::ECONNREFUSED
# * +:on_session_load_error:+ - Called with err and SID on Marshal.load fail
# * +:serializer:+ - Serializer to use on session data, default is :marshal.
# * +:adapter:+ - Adapter for other framework's session, default is :default.
#
# ==== Examples
#
Expand All @@ -47,6 +50,7 @@ def initialize(app, options = {})
@redis = redis_options[:client] || Redis.new(redis_options)
@on_redis_down = options[:on_redis_down]
@serializer = determine_serializer(options[:serializer])
@adapter = determine_adapter(options[:adapter])
@on_session_load_error = options[:on_session_load_error]
verify_handlers!
end
Expand All @@ -55,7 +59,7 @@ def initialize(app, options = {})

private

attr_reader :redis, :key, :default_options, :serializer
attr_reader :redis, :key, :default_options, :serializer, :adapter

# overrides method defined in rack to actually verify session existence
# Prevents needless new sessions from being created in scenario where
Expand Down Expand Up @@ -86,52 +90,38 @@ def prefixed(sid)
"#{default_options[:key_prefix]}#{sid}"
end

def get_session(env, sid)
unless sid && (session = load_session_from_redis(sid))
sid = generate_sid
session = {}
end

[sid, session]
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
on_redis_down.call(e, env, sid) if on_redis_down
[generate_sid, {}]
def generate_session_sid
adapter.generate_session_sid || generate_sid
end
alias find_session get_session

def load_session_from_redis(sid)
data = redis.get(prefixed(sid))
begin
data ? decode(data) : nil
rescue => e
destroy_session_from_sid(sid, drop: true)
on_session_load_error.call(e, sid) if on_session_load_error
nil
end
def get_session(env, sid)
session = load_session_from_redis(env, sid) if sid
return [sid, session] if sid && session
[generate_session_sid, {}]
end
alias find_session get_session

def decode(data)
serializer.load(data)
def load_session_from_redis(env, sid)
return adapter.load_session_from_redis(prefixed(sid))
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
on_redis_down.call(e, env, sid) if on_redis_down
nil
rescue => e
destroy_session_from_sid(sid, drop: true)
on_session_load_error.call(e, sid) if on_session_load_error
nil
end

def set_session(env, sid, session_data, options = nil)
expiry = (options || env.fetch(ENV_SESSION_OPTIONS_KEY))[:expire_after]
if expiry
redis.setex(prefixed(sid), expiry, encode(session_data))
else
redis.set(prefixed(sid), encode(session_data))
end
adapter.write_session_to_redis(prefixed(sid), session_data, expiry)
return sid
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
on_redis_down.call(e, env, sid) if on_redis_down
return false
false
end
alias write_session set_session

def encode(session_data)
serializer.dump(session_data)
end

def destroy_session(env, sid, options)
destroy_session_from_sid(sid, (options || {}).to_hash.merge(env: env))
end
Expand All @@ -147,7 +137,7 @@ def destroy(env)

def destroy_session_from_sid(sid, options = {})
redis.del(prefixed(sid))
(options || {})[:drop] ? nil : generate_sid
(options || {})[:drop] ? nil : generate_session_sid
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
on_redis_down.call(e, options[:env] || {}, sid) if on_redis_down
end
Expand Down Expand Up @@ -189,4 +179,88 @@ def self.needs_migration?(value)
value.start_with?(MARSHAL_SIGNATURE)
end
end

def determine_adapter(adapter)
adapter ||= :default
case adapter
when :default then DefaultAdapter.new(redis, serializer)
when :java_spring then
JavaSpringAdapter.new(redis, serializer, default_options[:hashkey_prefix])
else adapter
end
end

# Default adapter, save session in redis as string
class DefaultAdapter
def initialize(redis, serializer)
@redis = redis
@serializer = serializer
end

attr_accessor :redis, :serializer

def generate_session_sid
end

def load_session_from_redis(s_key)
data = redis.get(s_key)
data ? serializer.load(data) : nil
end

def write_session_to_redis(s_key, session_data, expiry)
if expiry
redis.setex(s_key, expiry, serializer.dump(session_data))
else
redis.set(s_key, serializer.dump(session_data))
end
end
end

# Java Spring adapter, save session in redis as hash
class JavaSpringAdapter < DefaultAdapter
def initialize(redis, serializer, hashkey_prefix)
@redis = redis
@serializer = serializer
@hashkey_prefix = hashkey_prefix || ''
end

attr_accessor :redis, :serializer, :hashkey_prefix

def generate_session_sid
SecureRandom.uuid
end

def load_session_from_redis(s_key)
data = {}
redis.hkeys(s_key).each do |key|
next unless key.start_with?(hashkey_prefix)
value = redis.hget(s_key, key)
key = key[hashkey_prefix.length..-1]
data[key] = value ? serializer.load(value) : nil
end
data
end

def write_session_to_redis(s_key, session_data, expiry)
keys = []
session_data.each do |key, value|
key = "#{hashkey_prefix}#{key}"
unless value.nil?
redis.hset(s_key, key, serializer.dump(value))
keys << key
end
end
clear_old_keys(s_key, keys)
redis.expire(s_key, expiry) if expiry
end

private

def clear_old_keys(s_key, new_keys)
redis.hkeys(s_key).each do |key|
next unless key.start_with?(hashkey_prefix) && !new_keys.include?(key)
redis.hdel(s_key, key)
end
end
end
end
37 changes: 22 additions & 15 deletions spec/redis_session_store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@

context 'when unsuccessfully persisting the session' do
before do
allow(store).to receive(:redis).and_raise(Redis::CannotConnectError)
allow(store.instance_variable_get(:@adapter))
.to receive(:redis).and_raise(Redis::CannotConnectError)
end

it 'returns false' do
Expand All @@ -168,7 +169,8 @@

context 'when redis is down' do
before do
allow(store).to receive(:redis).and_raise(Redis::CannotConnectError)
allow(store.instance_variable_get(:@adapter))
.to receive(:redis).and_raise(Redis::CannotConnectError)
store.on_redis_down = ->(*_a) { @redis_down_handled = true }
end

Expand Down Expand Up @@ -260,7 +262,8 @@

it 'retrieves the prefixed key from redis' do
redis = double('redis')
allow(store).to receive(:redis).and_return(redis)
allow(store.instance_variable_get(:@adapter))
.to receive(:redis).and_return(redis)
allow(store).to receive(:generate_sid).and_return(fake_key)
expect(redis).to receive(:get).with("#{options[:key_prefix]}#{fake_key}")

Expand All @@ -269,7 +272,8 @@

context 'when redis is down' do
before do
allow(store).to receive(:redis).and_raise(Redis::CannotConnectError)
allow(store.instance_variable_get(:@adapter))
.to receive(:redis).and_raise(Redis::CannotConnectError)
allow(store).to receive(:generate_sid).and_return('foop')
end

Expand Down Expand Up @@ -357,7 +361,8 @@
let(:expected_encoding) { encoded_data }

before do
allow(store).to receive(:redis).and_return(redis)
allow(store.instance_variable_get(:@adapter))
.to receive(:redis).and_return(redis)
end

shared_examples_for 'serializer' do
Expand All @@ -367,8 +372,8 @@
end

it 'decodes correctly' do
expect(store.send(:get_session, env, session_id))
.to eq([session_id, session_data])
expect(store.send(:load_session_from_redis, env, session_id))
.to eq(session_data)
end
end

Expand Down Expand Up @@ -422,20 +427,21 @@ def self.dump(_value)
describe 'handling decode errors' do
context 'when a class is serialized that does not exist' do
before do
allow(store).to receive(:redis)
allow(store.instance_variable_get(:@adapter)).to receive(:redis)
.and_return(double('redis',
get: "\x04\bo:\nNonExistentClass\x00",
del: true))
end

it 'returns an empty session' do
expect(store.send(:load_session_from_redis, 'whatever')).to be_nil
expect(store.send(:load_session_from_redis, double('env'), 'whatever'))
.to be_nil
end

it 'destroys and drops the session' do
expect(store).to receive(:destroy_session_from_sid)
.with('wut', drop: true)
store.send(:load_session_from_redis, 'wut')
store.send(:load_session_from_redis, double('env'), 'wut')
end

context 'when a custom on_session_load_error handler is provided' do
Expand All @@ -447,7 +453,7 @@ def self.dump(_value)
end

it 'passes the error and the sid to the handler' do
store.send(:load_session_from_redis, 'foo')
store.send(:load_session_from_redis, double('env'), 'foo')
expect(@e).to be_kind_of(StandardError)
expect(@sid).to eq('foo')
end
Expand All @@ -456,18 +462,19 @@ def self.dump(_value)

context 'when the encoded data is invalid' do
before do
allow(store).to receive(:redis)
allow(store.instance_variable_get(:@adapter)).to receive(:redis)
.and_return(double('redis', get: "\x00\x00\x00\x00", del: true))
end

it 'returns an empty session' do
expect(store.send(:load_session_from_redis, 'bar')).to be_nil
expect(store.send(:load_session_from_redis, double('env'), 'bar'))
.to be_nil
end

it 'destroys and drops the session' do
expect(store).to receive(:destroy_session_from_sid)
.with('wut', drop: true)
store.send(:load_session_from_redis, 'wut')
store.send(:load_session_from_redis, double('env'), 'wut')
end

context 'when a custom on_session_load_error handler is provided' do
Expand All @@ -479,7 +486,7 @@ def self.dump(_value)
end

it 'passes the error and the sid to the handler' do
store.send(:load_session_from_redis, 'foo')
store.send(:load_session_from_redis, double('env'), 'foo')
expect(@e).to be_kind_of(StandardError)
expect(@sid).to eq('foo')
end
Expand Down

0 comments on commit e62ecf2

Please sign in to comment.