When upgrading a high-traffic Rails application from 3.0.x to 3.2.x, existing sessions with Flash data will raise dump format error during the session unmarshalling step of the users next request. For an ostensibly minor patch (see semver), breaking user sessions is an unexpected surprise and a real bummer for your users.
The Rails breaking change to FlashHash
was to stop inheriting from Hash:
# Rails 3.0 - See: http://git.io/X8WqIw
class FlashHash < Hash
# Rails 3.2 - See: http://git.io/jJ_Qhw
class FlashHash
Dealing with serialization errors can be notoriously difficult, even more so because these were occurring deep
in the framework. So, we monkey patched ActiveRecord::SessionStore::Session
to remove the offending Flash data.
The possible loss was a notification displayed on the page, the benefit was maintaining user sessions across the
upgrade deployment.
If we catch the dump format error, assume it's the Flash and delete it, the remainder of the session may be unmarshalled and accessed.
ActiveRecord::SessionStore::Session.class_eval do
def self.unmarshal(data)
return unless data
marshalled = nil
begin
decoded = ::Base64.decode64 data
marshalled = Marshal.load decoded
rescue ArgumentError => ae
if ae.message =~ /dump format error/
with_alternate_flash_klass do
marshalled = Marshal.load decoded
marshalled.delete('flash')
end
else
raise ae
end
end
marshalled
end
...
end
We start by monkey patching the ActiveRecord::SessionStore::Session#unmarshal
method to rescue
ArgumentError
. see original method
If the ArgumentError
message is dump format error, we use a back-ported FlashHash
implementation and attempt to decode again. This
time we delete the 'flash' data and return the unmarshalled session. Going forward, the session should be valid because new Flash data will
be from the current FlashHash
object.
In the monkey patched ActiveRecord::SessionStore::Session#unmarshal
above, you'll notice the with_alternate_flash_klass
block
for the second attempt to unmarshal. This method takes the block to execute and handles the redefining of the FlashHash
constant for you. It
conveniently restores the FlashHash
after the unmarshalling.
First, we create helper method to dynamically create the alternate FlashHash
class:
def self.alternate_flash_klass
if ActionDispatch::Flash::FlashHash.ancestors.include? Hash
Class.new() do #rails3.2 FlashHash
def initialize
@used = Set.new
@closed = false
@flashes = {}
@now = nil
end
end
else
Class.new(Hash) do #rails3.0 FlashHash
def initialize
super
@used = Set.new
end
end
end
end
Using Class.new()
, we have the option of passing in a parent, which we do for the rails 3.0 version.
Next, we need a method to swap the FlashHash
and execute a block:
def self.with_alternate_flash_klass
temporary_flash_hash_klass = alternate_flash_klass
original_flash_hash_klass = ActionDispatch::Flash::FlashHash
ActionDispatch::Flash.send :remove_const, :FlashHash
ActionDispatch::Flash.const_set 'FlashHash', temporary_flash_hash_klass
yield
ActionDispatch::Flash.send :remove_const, :FlashHash
ActionDispatch::Flash.const_set 'FlashHash', original_flash_hash_klass
end
This saves the current FlashHash
in original_flash_hash_klass
, redefines the constant. When the block
is yielded, the unmarshal is attempted with the back-ported FlashHash
. Then we restore the original FlashHash
.
Astute readers, will notice alternate_flash_klass
will provide a back-port or forward-port implementation of FlashHash
.
This allows us to deploy Rails 3.2 and rollback to Rails 3.0. If you deployed Rails 3.2, allowed some sessions to be
persisted, then rolled back, those sessions would be corrupt. This implementation supports going forward and backward
with only a loss of Flash notifications.
You can add this code to an initialization file. You may only need it for 30 days or until your sessions expire. We have a warn so your reminded this monkey patch is still present in your code.
# ActionDispatch::Flash::FlashHash changed between 3.0 and 3.2
# Rails 3.0.x FlashHash inherits from Hash, this was removed in Rails 3.2.x
# if a session was created in 3.0.x, after deploy we would receive
# 'dump format error' when trying to deserialize the session
#
# this monkey patch attempts to unmarshal with an alternate version
# of FlashHash and deletes it if successful
#
warn "[TEMPORARY] Loading session monkey patches for Rails 3.0.x => 3.2.x upgrade"
ActiveRecord::SessionStore::Session.class_eval do
def self.unmarshal(data)
return unless data
marshalled = nil
begin
decoded = ::Base64.decode64 data
marshalled = Marshal.load decoded
rescue ArgumentError => ae
if ae.message =~ /dump format error/
with_alternate_flash_klass do
marshalled = Marshal.load decoded
marshalled.delete('flash')
end
else
raise ae
end
end
marshalled
end
def self.alternate_flash_klass
if ActionDispatch::Flash::FlashHash.ancestors.include? Hash
Class.new() do #rails3.2 FlashHash
def initialize
@used = Set.new
@closed = false
@flashes = {}
@now = nil
end
end
else
Class.new(Hash) do #rails3.0 FlashHash
def initialize
super
@used = Set.new
end
end
end
end
def self.with_alternate_flash_klass
temporary_flash_hash_klass = alternate_flash_klass
original_flash_hash_klass = ActionDispatch::Flash::FlashHash
ActionDispatch::Flash.send :remove_const, :FlashHash
ActionDispatch::Flash.const_set 'FlashHash', temporary_flash_hash_klass
yield
ActionDispatch::Flash.send :remove_const, :FlashHash
ActionDispatch::Flash.const_set 'FlashHash', original_flash_hash_klass
end
end
NOTE you will need to copy session from your application for these tests to pass. The session data below is invalid.
require 'test_helper'
class Rails32SessionUpgradePatchesTest < ActiveSupport::TestCase
test 'remove flash from rails 3.0.x sessions in rails 3.2.x' do
rails30_session = '7CUkiDnByb2R1Y3RpZAY6BkVGSSIKMDExMDQGOwBUSSIMbGFiX3VybAY7
AEYiNGh0dHA6Ly93d3cuY3VzdG6taW5rLmNvbS9sYWI/Y2lkPWNobTAtMDAx
akxVVlYyOWVxVHRPQk9ES1NCNFliVjVWUT0GOwBGSSIKZmxhc2gGOwBGSUM6
JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoewY6CmVycm9yewc6
IGluZm9ybWF0aW9uLgY7AEY6DG1lc3NhZ2VJIiZQbGVhc2UgZmlsbCBpbiBh
IHJlY2lwaWVudCBiZWxvdy4GOwBGBjoKQHVzZWRvOghTZXQGOgpAaGFzaHsG
OwdU'
session = ActiveRecord::SessionStore::Session.unmarshal rails30_session
assert_nil session['flash']
end if Rails.version =~ /^3\.2/
test 'remove flash from rails 3.2.x sessions in rails 3.0.x' do
rails32_session = '7BkkiCmZsYXNoBjoGRUZvOiVBY3Rpb25EaXNwYXRjaDo6Rmxhc2g6OkZs
YXNoSGFzaAk6CkB1c2VkbzoIU2V0BjoKQGhhc2h7BjoJd2FyblQ6DEBjbG9z
ZWRGOg1AZmxhc2hlc3sGOwp7BjoMbWVzc2FnZUkiAcJQbGVhc2Ugbm90ZSB0
dHdvcmsgYmVmb3JlIHByb2NlZWRpbmcgd2l0aCBwcm9kdWN0aW9uLiBQbGVh
bWVudCB3aXRoIGEgbGluayB0byB5b3VyIGZpbmFsIHBpY3R1cmUgcHJvb2Zz
LgY7AEY6CUBub3dvOiRBY3Rpb25EaXNwYXRjaDo6Rmxhc2g6OkZsYXNoTm93
BjoLQGZsYXNoQAc='
session = ActiveRecord::SessionStore::Session.unmarshal rails32_session
assert_nil session['flash']
end if Rails.version =~ /^3\.0/
end
No, it's not pretty. But it will help you overcome an unexpected dump format error when deploying a Rails 3.2 upgrade.