ruby/psych

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