I ran into something today I'm having a really hard time figuring out:

I've got a model instance, `user`, and after fetching that user instance 
I'd like to make it available in a "read only" state, which is to say I 
don't want any 'downstream' code to be able to make changes to it. #freeze 
accomplishes this. It also makes sense to be to pass a frozen copy of the 
object off to another process, while retaining an unfrozen copy. Testing 
this led to some unexpected results:

The relevant portion of my tests looks like this:
DB.transaction(:rollback => :always, :auto_savepoint=>true) do
  user = FinishSignup.call(...)
  assert_equal u, User.order(:id).last
  assert_equal u.properties.first, Property.order(:id).last
end

The `FinishSignup` call creates a user and property instance. It dups the 
user, freezes the dup, and passes this frozen dup on to some observers. The 
function then returns the original user instance.

The test fails on the last assertion, with a stack trace like this:

RuntimeError: can't modify frozen Hash
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/model/associations.rb:2240:in
>  
> `load_associated_objects'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/plugins/association_proxies.rb:83:in
>  
> `method_missing'
> ~/project/test/services/finish_signup_test.rb:18:in `block (3 levels) in 
> <top (required)>'
> ~/project/test/test_helper.rb:31:in `block in transaction'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:119:in
>  
> `_transaction'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:100:in
>  
> `block in transaction'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>  
> `block in synchronize'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/connection_pool/threaded.rb:98:in
>  
> `hold'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>  
> `synchronize'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:89:in
>  
> `transaction'
> ~/project/test/test_helper.rb:30:in `transaction'
> ~/project/test/services/finish_signup_test.rb:9:in `block (2 levels) in 
> <top (required)>'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/model/associations.rb:2240:in
>  
> `load_associated_objects'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/plugins/association_proxies.rb:83:in
>  
> `method_missing'
> ~/project/test/services/finish_signup_test.rb:18:in `block (3 levels) in 
> <top (required)>'
> ~/project/test/test_helper.rb:31:in `block in transaction'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:119:in
>  
> `_transaction'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:100:in
>  
> `block in transaction'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>  
> `block in synchronize'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/connection_pool/threaded.rb:98:in
>  
> `hold'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>  
> `synchronize'
> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:89:in
>  
> `transaction'
> ~/project/test/test_helper.rb:30:in `transaction'
> ~/project/test/services/finish_signup_test.rb:9:in `block (2 levels) in 
> <top (required)>'



More background, and research I did so far:

The relevant portion of the model looks like this:
class User < Sequel::Model
  one_to_many :properties
end

Inside the FinishSignup call looks basically like this:

# Create User and Property
user = User.create(...)
property = Property.create(user: user, ...)
trigger_event :user_created, user: user

The trigger event method is a generic method that works like this:
def trigger_event(name,**args)
  frozen_args = args.each.with_object({}){|(k,v),o| o[k] = v.dup.freeze}
  send(name,**frozen_args)
end

^ where send actually calls the event trigger.

The reason this is most confusing is I can't duplicate it in the console. 
I've tried every variation of this procedure I can think of, creating a 
user, creating a property, duping the user, freezing the dup, etc., and in 
every case I'm able to call `original_user.properties.first` with no 
runtime error. I've tried this in the console inside and outside of 
transactions etc.

So, I think something else must be going on in the Sequel Internals that 
I'm not aware of which is the reason this specific case won't work, but I'm 
having a terrible time figuring out what it is.

Any insight you can share?

Thanks!
Andrew

-- 
You received this message because you are subscribed to the Google Groups 
"sequel-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/sequel-talk.
For more options, visit https://groups.google.com/d/optout.

Reply via email to