Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Race conditions handling #95

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
# Offense count: 1
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 108
Max: 135
42 changes: 37 additions & 5 deletions lib/redis-session-store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class RedisSessionStore < ActionDispatch::Session::AbstractStore
# * +: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.
# * +:handle_race_conditions:+ Boolean, saving of initial session state
#
# ==== Examples
#
Expand All @@ -48,6 +49,7 @@ def initialize(app, options = {})
@on_redis_down = options[:on_redis_down]
@serializer = determine_serializer(options[:serializer])
@on_session_load_error = options[:on_session_load_error]
@handle_race_conditions = options[:handle_race_conditions]
verify_handlers!
end

Expand Down Expand Up @@ -92,13 +94,23 @@ def get_session(env, sid)
session = {}
end

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

def session_data(sid, session)
if @handle_race_conditions
session_with_initial_state = session.clone
session_with_initial_state['session_initial_state'] = session
[sid, session_with_initial_state]
else
[sid, session]
end
end

def load_session_from_redis(sid)
data = redis.get(prefixed(sid))
begin
Expand All @@ -116,10 +128,9 @@ def decode(data)

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))
updated_session_data = encoded_session_data(sid, session_data)
if updated_session_data
write_session_to_redis sid, expiry, updated_session_data
end
return sid
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
Expand All @@ -132,6 +143,27 @@ def encode(session_data)
serializer.dump(session_data)
end

def encoded_session_data(sid, session_data)
if @handle_race_conditions
session_initial = session_data.delete 'session_initial_state'
return false if session_initial == session_data

session_current = load_session_from_redis(sid)
if session_current && session_current != session_initial
session_data = session_current.deep_merge session_data
end
end
encode session_data
end

def write_session_to_redis(sid, expiry, session_data)
if expiry
redis.setex prefixed(sid), expiry, session_data
else
redis.set prefixed(sid), session_data
end
end

def destroy_session(env, sid, options)
destroy_session_from_sid(sid, (options || {}).to_hash.merge(env: env))
end
Expand Down
11 changes: 11 additions & 0 deletions spec/redis_session_store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@
end
end

describe 'when handling race conditions' do
let :options do
{
handle_race_conditions: true
}
end

it 'does nothing if session hasn`t been changed'
it 'merges new session data with current redis state'
end

describe 'rack 1.45 compatibility' do
# Rack 1.45 (which Rails 3.2.x depends on) uses the return value of
# set_session to set the cookie value. See:
Expand Down