class RedisSessionStore

Redis session storage for Rails, and for Rails only. Derived from the MemCacheStore code, simply dropping in Redis instead.

Constants

ENV_SESSION_OPTIONS_KEY
USE_INDIFFERENT_ACCESS
VERSION

Attributes

default_options[R]
key[R]
on_redis_down[RW]
on_session_load_error[RW]
redis[R]
serializer[R]

Public Class Methods

new(app, options = {}) click to toggle source

Options

  • :key - Same as with the other cookie stores, key name

  • :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:

    • :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.

Examples

Rails.application.config.session_store :redis_session_store,
  key: 'your_session_key',
  redis: {
    expire_after: 120.minutes,
    key_prefix: 'myapp:session:',
    url: 'redis://localhost:6379/0'
  },
  on_redis_down: ->(*a) { logger.error("Redis down! #{a.inspect}") },
  serializer: :hybrid # migrate from Marshal to JSON
Calls superclass method
# File lib/redis-session-store.rb, line 40
def initialize(app, options = {})
  super

  @default_options[:namespace] = 'rack:session'
  @default_options.merge!(options[:redis] || {})
  init_options = options[:redis]&.reject { |k, _v| %i[expire_after key_prefix].include?(k) } || {}
  @redis = init_options[:client] || Redis.new(init_options)
  @on_redis_down = options[:on_redis_down]
  @serializer = determine_serializer(options[:serializer])
  @on_session_load_error = options[:on_session_load_error]
  verify_handlers!
end

Private Instance Methods

decode(data) click to toggle source
# File lib/redis-session-store.rb, line 139
def decode(data)
  session = serializer.load(data)
  USE_INDIFFERENT_ACCESS ? session.with_indifferent_access : session
end
delete_session(env, sid, options)
Alias for: destroy_session
destroy(env) click to toggle source
# File lib/redis-session-store.rb, line 173
def destroy(env)
  if env['rack.request.cookie_hash'] &&
     (sid = env['rack.request.cookie_hash'][key])
    sid = Rack::Session::SessionId.new(sid)
    destroy_session_from_sid(sid.private_id, drop: true, env: env)
    destroy_session_from_sid(sid.public_id, drop: true, env: env)
  end
  false
end
destroy_session(env, sid, options) click to toggle source
# File lib/redis-session-store.rb, line 167
def destroy_session(env, sid, options)
  destroy_session_from_sid(sid.public_id, (options || {}).to_hash.merge(env: env, drop: true))
  destroy_session_from_sid(sid.private_id, (options || {}).to_hash.merge(env: env))
end
Also aliased as: delete_session
destroy_session_from_sid(sid, options = {}) click to toggle source
# File lib/redis-session-store.rb, line 183
def destroy_session_from_sid(sid, options = {})
  redis.del(prefixed(sid))
  (options || {})[:drop] ? nil : generate_sid
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
  on_redis_down.call(e, options[:env] || {}, sid) if on_redis_down
end
determine_serializer(serializer) click to toggle source
# File lib/redis-session-store.rb, line 190
def determine_serializer(serializer)
  serializer ||= :marshal
  case serializer
  when :marshal then Marshal
  when :json    then JsonSerializer
  when :hybrid  then HybridSerializer
  else serializer
  end
end
encode(session_data) click to toggle source
# File lib/redis-session-store.rb, line 163
def encode(session_data)
  serializer.dump(session_data)
end
find_session(env, sid)
Alias for: get_session
get_expiry(env, options) click to toggle source
# File lib/redis-session-store.rb, line 158
def get_expiry(env, options)
  session_storage_options = options || env.fetch(ENV_SESSION_OPTIONS_KEY, {})
  session_storage_options[:ttl] || session_storage_options[:expire_after]
end
get_session(env, sid) click to toggle source
# File lib/redis-session-store.rb, line 112
def get_session(env, sid)
  sid && (session = load_session_with_fallback(sid)) ? [sid, session] : session_default_values
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
  on_redis_down.call(e, env, sid) if on_redis_down
  session_default_values
end
Also aliased as: find_session
key_exists?(value) click to toggle source
# File lib/redis-session-store.rb, line 82
def key_exists?(value)
  if redis.respond_to?(:exists?)
    # added in redis gem v4.2
    redis.exists?(prefixed(value))
  else
    # older method, will return an integer starting in redis gem v4.3
    redis.exists(prefixed(value))
  end
end
key_exists_with_fallback?(value) click to toggle source
# File lib/redis-session-store.rb, line 76
def key_exists_with_fallback?(value)
  return false if private_session_id?(value.public_id)

  key_exists?(value.private_id) || key_exists?(value.public_id)
end
load_session_from_redis(sid) click to toggle source
# File lib/redis-session-store.rb, line 128
def load_session_from_redis(sid)
  data = redis.get(prefixed(sid))
  begin
    data ? decode(data) : nil
  rescue StandardError => e
    destroy_session_from_sid(sid, drop: true)
    on_session_load_error.call(e, sid) if on_session_load_error
    nil
  end
end
load_session_with_fallback(sid) click to toggle source
# File lib/redis-session-store.rb, line 120
def load_session_with_fallback(sid)
  return nil if private_session_id?(sid.public_id)

  load_session_from_redis(
    key_exists?(sid.private_id) ? sid.private_id : sid.public_id
  )
end
prefixed(sid) click to toggle source
# File lib/redis-session-store.rb, line 104
def prefixed(sid)
  "#{default_options[:key_prefix]}#{sid}"
end
private_session_id?(value) click to toggle source
# File lib/redis-session-store.rb, line 92
def private_session_id?(value)
  value.match?(/\A\d+::/)
end
session_default_values() click to toggle source
# File lib/redis-session-store.rb, line 108
def session_default_values
  [generate_sid, USE_INDIFFERENT_ACCESS ? {}.with_indifferent_access : {}]
end
session_exists?(env) click to toggle source

overrides method defined in rack to actually verify session existence Prevents needless new sessions from being created in scenario where user HAS session id, but it already expired, or is invalid for some other reason, and session was accessed only for reading.

# File lib/redis-session-store.rb, line 63
def session_exists?(env)
  value = current_session_id(env)

  !!(
    value && !value.empty? &&
    key_exists_with_fallback?(value)
  )
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
  on_redis_down.call(e, env, value) if on_redis_down

  true
end
set_session(env, sid, session_data, options = nil) click to toggle source
# File lib/redis-session-store.rb, line 144
def set_session(env, sid, session_data, options = nil)
  expiry = get_expiry(env, options)
  if expiry
    redis.setex(prefixed(sid.private_id), expiry, encode(session_data))
  else
    redis.set(prefixed(sid.private_id), encode(session_data))
  end
  sid
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
  on_redis_down.call(e, env, sid) if on_redis_down
  false
end
Also aliased as: write_session
verify_handlers!() click to toggle source
# File lib/redis-session-store.rb, line 96
def verify_handlers!
  %w(on_redis_down on_session_load_error).each do |h|
    next unless (handler = public_send(h)) && !handler.respond_to?(:call)

    raise ArgumentError, "#{h} handler is not callable"
  end
end
write_session(env, sid, session_data, options = nil)
Alias for: set_session