Unknown alias: 1 (Psych::BadAlias) when referencing an object twice from the object being serialized
dblock opened this issue · 1 comments
Could use some help, please. I'm definitely a noob with Psych, so apologies if I am missing something obvious. This comes from collectiveidea/delayed_job_mongoid#65 (comment). You can run it as is on Ruby 2.3.1 for example.
In short, delayed_job is serializing job objects to yaml and then deserializing then back into ruby objects. When the job contains the same object more than once, it will take advantage of yaml aliases to avoid duplicating that object in the yaml string to make a smaller payload. This works fine for vanilla objects, but not Mongoid::Document objects.
I could use some help fixing this. How do I modify what's below to either not create an alias or maybe I'm missing something else?
The full repro of the problem:
require 'psych'
module Delayed
class PerformableMethod
# serialize to YAML
def encode_with(coder)
coder.map = {
'object' => object,
'method_name' => method_name,
'args' => args
}
end
end
end
module Psych
def self.load_dj(yaml)
result = parse(yaml)
result ? Delayed::PsychExt::ToRuby.create.accept(result) : result
end
end
module Delayed
module PsychExt
class ToRuby < Psych::Visitors::ToRuby
unless respond_to?(:create)
def self.create
new
end
end
def visit_Psych_Nodes_Mapping(object) # rubocop:disable CyclomaticComplexity, MethodName, PerceivedComplexity
return revive(Psych.load_tags[object.tag], object) if Psych.load_tags[object.tag]
case object.tag
when %r{^!ruby/object}
result = super
if defined?(ActiveRecord::Base) && result.is_a?(ActiveRecord::Base)
klass = result.class
id = result[klass.primary_key]
begin
klass.find(id)
rescue ActiveRecord::RecordNotFound => error # rubocop:disable BlockNesting
raise Delayed::DeserializationError, "ActiveRecord::RecordNotFound, class: #{klass}, primary key: #{id} (#{error.message})"
end
else
result
end
when %r{^!ruby/Mongoid:(.+)$}
klass = resolve_class(Regexp.last_match[1])
payload = Hash[*object.children.map { |c| accept c }]
id = payload['attributes']['_id']
begin
klass.find(id)
rescue Mongoid::Errors::DocumentNotFound => error
raise Delayed::DeserializationError, "Mongoid::Errors::DocumentNotFound, class: #{klass}, primary key: #{id} (#{error.message})"
end
else
super
end
end
def resolve_class(klass_name)
return nil if !klass_name || klass_name.empty?
klass_name.constantize
rescue
super
end
end
end
end
class Foo
end
class Bar
def initialize
@baz = Foo.new
@qux = @baz
end
end
obj = Bar.new
yaml = Psych.dump(obj)
# looks like this (note how qux is an "alias" for baz):
#
# --- !ruby/object:Bar
# baz: &1 !ruby/object:Foo {}
# qux: *1
# code from DJ's psych_ext.rb
# this works:
p Psych.load_dj(yaml)
require 'mongoid'
# some mongoid config
Mongoid.connect_to('issue=65')
Mongoid.logger.level = Logger::INFO
Mongo::Logger.logger.level = Logger::INFO
# from delayed_job_mongoid:
Mongoid::Document.class_eval do
def encode_with(coder)
coder['attributes'] = @attributes
coder.tag = ['!ruby/Mongoid', self.class.name].join(':')
end
end
class Dog
include Mongoid::Document
end
class DogJob
def initialize(dog)
@dog = dog
@also_dog = @dog
end
end
dog = Dog.first || Dog.create!
obj = DogJob.new(dog)
yaml = Psych.dump(obj)
# looks like the following (notice the alias is present)
#
# --- !ruby/object:DogJob
# dog: &1 !ruby/object:Dog
# attributes: !ruby/hash:BSON::Document
# _id: !ruby/object:BSON::ObjectId
# raw_data: !binary |-
# WIelGzxHsZcB90Ou
# __selected_fields:
# also_dog: *1
# this will error:
# (but it doesn't if we didn't override encode_with)
p Psych.load_dj(yaml)
The result:
/Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:319:in `block in visit_Psych_Nodes_Alias': Unknown alias: 1 (Psych::BadAlias)
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:319:in `fetch'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:319:in `visit_Psych_Nodes_Alias'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/visitor.rb:16:in `visit'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/visitor.rb:6:in `accept'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:32:in `accept'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:338:in `block in revive_hash'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:336:in `each'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:336:in `each_slice'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:336:in `revive_hash'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:374:in `revive'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:208:in `visit_Psych_Nodes_Mapping'
from t.rb:37:in `visit_Psych_Nodes_Mapping'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/visitor.rb:16:in `visit'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/visitor.rb:6:in `accept'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:32:in `accept'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:311:in `visit_Psych_Nodes_Document'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/visitor.rb:16:in `visit'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/visitor.rb:6:in `accept'
from /Users/dblock/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:32:in `accept'
from t.rb:19:in `load_dj'
from t.rb:139:in `<main>'
Figured it out. It needed a register(document, object)
in there, see collectiveidea/delayed_job_mongoid#72