Summary
2.3.0's #166 optimization makes write_session skip the save when record.changed? is false — but Session#data= can't detect
in-place mutation, so nested session writes are silently dropped. 2.2.0 always saved, so this worked.
Cause
#active_record_store.rb (2.3.0)
record.data = session_data
return sid unless record.changed? || record.new_record? # new
#session.rb
def data=(data)
attribute_will_change!(@@data_column_name) if data != self.data
@data = data
end
get_session hands Rack the record's own @data object. When an app mutates a nested value in place (session[:foo][k] << v), the
change bleeds into @data, so at write time session_data deep-equals self.data, changed? is false, and the save is skipped.
Repro
record = ActiveRecord::SessionStore::Session.find_by_session_id("abc")
data = record.data # {"key" => ["a"]}
data["key"] << "b" # in-place
record.data = data
record.changed? # => false on 2.3.0; save is skipped
Suggested fix
Snapshot the deserialized data (deep copy) at read time and diff against that at write, instead of comparing against the
already-mutated @data.
Env: 2.3.0 (regression vs 2.2.0), Rails 8.1, :active_record_store
Summary
2.3.0's #166 optimization makes write_session skip the save when record.changed? is false — but Session#data= can't detect
in-place mutation, so nested session writes are silently dropped. 2.2.0 always saved, so this worked.
Cause
#active_record_store.rb (2.3.0)
#session.rb
get_session hands Rack the record's own
@dataobject. When an app mutates a nested value in place (session[:foo][k] << v), thechange bleeds into
@data, so at write time session_data deep-equals self.data, changed? is false, and the save is skipped.Repro
Suggested fix
Snapshot the deserialized data (deep copy) at read time and diff against that at write, instead of comparing against the
already-mutated
@data.Env: 2.3.0 (regression vs 2.2.0), Rails 8.1, :active_record_store