Use ActiveSupport::Cache::RedisCacheStore as session store
What does this MR do and why?
This MR Is broken up from !175735, where we'll be migrating Redis Sessions to a Redis Cluster mode in response to CPU saturation warning.
To migrate sessions workload, we need to wrap Gitlab::Redis::Sessions
with MultiStore
which handles dual-writing to both the old and new stores. This works for callsites within GitLab codebase, mainly from Gitlab::Redis::Sessions.with
. However, sessions management from the gem ActionDispatch::Session::RedisStore
uses redis-store
and has its own function signatures, ie the set
s expect different arguments than MultiStore
which uses redis-rb
gem. We also need to pass in the MultiStore
connection pool to the session store so we could override the .with
behavior in the gem.
The workaround is to replace the config.session_store
with ActionDispatch::Session::CacheStore
as suggested in https://github.com/redis/redis-rb/issues/1280#issuecomment-2111327985.
The main difference between both stores lie in the format of the session data. RedisStore
stores a marshal of the session hash, whereas CacheStore
first wraps the session hash in ActiveSupport::Cache::Entry
then being marshalled. This MR also supports CacheStore
to read the format of RedisStore
session by passing a custom coder
in the ActiveSupport::Cache::RedisCacheStore
initialization.
Comparison of the session data in Redis:
--- this branch
redis /Users/gregoriusmarco/gdk/redis/redis.socket[5]> get session:gitlab:2::40202a340d5d7b10dc728367de75595c12a85e4e1f05658b4fb98292e21584f6
"\x04\bo: ActiveSupport::Cache::Entry\t:\x0b@value{\bI\"\x19warden.user.user.key\x06:\x06ET[\a[\x06i\x06I\"\"$2a$10$y7/L42WUBppyY8sKToazCu\x06;\aTI\" ask_for_usage_stats_consent\x06;\aFFI\"\x10_csrf_token\x06;\aFI\"0CQs4KCq7TJmUGtYRiXVHTacWnMdoXx-NdHgwiA_vZHM\x06;\aF:\r@version0:\x10@created_atf\x060:\x10@expires_inf\x161736824501.133667"
--- master
redis /Users/gregoriusmarco/gdk/redis/redis.socket[5]> get session:gitlab:2::8a5bda9738eb9eb4a5d3741501e85ecb23bc2892e9a797032d7cd65d5c51d47d
"\x04\b{\bI\"\x19warden.user.user.key\x06:\x06ET[\a[\x06i\x06I\"\"$2a$10$y7/L42WUBppyY8sKToazCu\x06;\x00TI\" ask_for_usage_stats_consent\x06;\x00FFI\"\x10_csrf_token\x06;\x00FI\"0vfgp0tAfNdGAffYAnxy-bi4R6TeSUK7GZLcWvxf-r5Y\x06;\x00F"
Rolling back
In case unexpected things occur once we've rolled forward, the rollback is already supported in !176876 (merged). The Gitlab::Sessions::RedisStore
is able to read Entry
object introduced in this MR.
Test gap
I'd like to add specs (either request/feature specs) to test old sessions are still working with the new store, and vice versa - new sessions working with the old store. However it's pretty difficult to write one since the stores are initialized in initializers, and it's not possible to rerun the initializer.
Another alternative I've tried is stubbing the session similar to https://gitlab.com/gitlab-org/gitlab/-/blob/0bcde28a5700833dc3d97c577670e93f6f057079/spec/support/helpers/session_helpers.rb#L6-16, then attach the cookie to the request header without signing in, but the request is returning 302.
Click to expand
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Session store integration test', :clean_gitlab_redis_sessions, type: :request, feature_category: :system_access do
include SessionHelpers
let_it_be(:user) { create(:user) }
let(:cookies) { {} }
def stub_redis_store_session(session_data:, user_id: nil)
session_id = Rack::Session::SessionId.new(SecureRandom.hex)
Gitlab::Redis::Sessions.with do |redis|
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_data))
redis.sadd("session:lookup:user:gitlab:#{user_id}", [session_id.private_id]) if user_id
end
defined?(cookies) && cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
end
before do
stub_redis_store_session(session_data: { 'warden.user.user.key' => [[user.id], user.authenticatable_salt] }, user_id: user.id)
end
context 'when old session from Gitlab::Sessions::RedisStore exists' do
it 'session still works' do
key = Gitlab::Application.config.session_options[:key]
get root_path, headers: { "Cookies": "#{key}=cell-1-#{cookies[key]}" }
expect(response).not_to be_redirect # fails here, getting a 302 redirect
end
end
end
For now, we could only rely on the local validations below.
References
Please include cross links to any resources that are relevant to this MR. This will give reviewers and future readers helpful context to give an efficient review of the changes introduced.
- This is the 2nd MR from the migration strategy outlined in gitlab-com/gl-infra/data-access/durability/team#35 (comment 2278902354)
MR acceptance checklist
Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Screenshots or screen recordings
Screenshots are required for UI changes, and strongly recommended for all other merge requests.
Before | After |
---|---|
How to set up and validate locally
Browsing around with new sessions
- Check out this branch
- Restart
gdk restart rails-web
since the store is initialized on load time. - Sign in, browse around, submit some forms. Everything should work normally.
Simulating rolling forward from existing sessions
-
Check out master branch
-
Flush sessions by
gdk redis-cli -n 5 flushdb
-
Restart
gdk restart rails-web
-
Sign in, ensure the session exists in Redis
redis /Users/gregoriusmarco/gdk/redis/redis.socket[5]> keys * 1) "session:lookup:user:gitlab:1" 2) "session:user:gitlab::v2:1:2::4c2a2bb7385f73a0d13461dcff6ff9b94de9e1224f73e94e296c5faf509d7f2b" 3) "session:gitlab:2::4c2a2bb7385f73a0d13461dcff6ff9b94de9e1224f73e94e296c5faf509d7f2b" redis /Users/gregoriusmarco/gdk/redis/redis.socket[5]> get session:gitlab:2::4c2a2bb7385f73a0d13461dcff6ff9b94de9e1224f73e94e296c5faf509d7f2b "\x04\b{\bI\"\x19warden.user.user.key\x06:\x06ET[\a[\x06i\x06I\"\"$2a$10$y7/L42WUBppyY8sKToazCu\x06;\x00TI\" ask_for_usage_stats_consent\x06;\x00FFI\"\x10_csrf_token\x06;\x00FI\"0DcNX9jZzewvW6knMmlVKwS-zmdNX5PYaDsRdjVdMFUo\x06;\x00F"
-
Check out this branch
-
Restart
gdk restart rails-web
-
Refresh your browser, you should still be logged in. Browse around, make form submissions, everything should work normally.
-
Check redis, the session is overwritten with
ActiveSupport::Cache::Entry
:redis /Users/gregoriusmarco/gdk/redis/redis.socket[5]> get session:gitlab:2::d64aa9cf9ea0dc0aad17ab8033fe93ca4711f99fd3934989f700632ae4b841eb "\x04\bo: ActiveSupport::Cache::Entry\t:\x0b@value{\bI\"\x19warden.user.user.key\x06:\x06ET[\a[\x06i\x06I\"\"$2a$10$y7/L42WUBppyY8sKToazCu\x06;\aTI\" ask_for_usage_stats_consent\x06;\aFFI\"\x10_csrf_token\x06;\aFI\"0lkdgevlocYAO03xtXrqY-sBQDS70eGUj3crG31LNup8\x06;\aF:\r@version0:\x10@created_atf\x060:\x10@expires_inf\x161736834265.350129"
Rolling back from new sessions to old sessions
This is to test the functionality in !176876 (comment 2283975076)
-
While still logged in on this branch, confirm the current session is of
ActiveSupport::Cache::Entry
instance:redis /Users/gregoriusmarco/gdk/redis/redis.socket[5]> get session:gitlab:2::d64aa9cf9ea0dc0aad17ab8033fe93ca4711f99fd3934989f700632ae4b841eb "\x04\bo: ActiveSupport::Cache::Entry\t:\x0b@value{\bI\"\x19warden.user.user.key\x06:\x06ET[\a[\x06i\x06I\"\"$2a$10$y7/L42WUBppyY8sKToazCu\x06;\aTI\" ask_for_usage_stats_consent\x06;\aFFI\"\x10_csrf_token\x06;\aFI\"0lkdgevlocYAO03xtXrqY-sBQDS70eGUj3crG31LNup8\x06;\aF:\r@version0:\x10@created_atf\x060:\x10@expires_inf\x161736834265.350129"
-
Check out to master branch, or comment out below line:
# ENV['USE_REDIS_CACHE_STORE_AS_SESSION_STORE'] = 'true' if Rails.env.test? || Rails.env.development?
-
Restart rails
gdk restart rails-web
-
Refresh the browser, you should still be logged in.
-
Redis now contains the old session in hash
redis /Users/gregoriusmarco/gdk/redis/redis.socket[5]> get session:gitlab:2::d64aa9cf9ea0dc0aad17ab8033fe93ca4711f99fd3934989f700632ae4b841eb "\x04\b{\bI\"\x19warden.user.user.key\x06:\x06ET[\a[\x06i\x06I\"\"$2a$10$y7/L42WUBppyY8sKToazCu\x06;\x00TI\" ask_for_usage_stats_consent\x06;\x00FFI\"\x10_csrf_token\x06;\x00FI\"0lkdgevlocYAO03xtXrqY-sBQDS70eGUj3crG31LNup8\x06;\x00F"
-
Or from console:
[35] pry(main)> Marshal.load(Gitlab::Redis::Sessions.with { |c| c.get("session:gitlab:2::d64aa9cf9ea0dc0aad17ab8033fe93ca4711f99fd3934989f700632ae4b841eb") }) => {"warden.user.user.key"=>[[1], "$2a$10$y7/L42WUBppyY8sKToazCu"], "ask_for_usage_stats_consent"=>false, "_csrf_token"=>"lkdgevlocYAO03xtXrqY-sBQDS70eGUj3crG31LNup8"}