3

Let's say I have a method that provides access to an API client in the scope of a user and the API client will automatically update the users OAuth tokens when they expire.

class User < ActiveRecord::Base

  def api
    ApiClient.new access_token: oauth_access_token,
                  refresh_token: oauth_refresh_token,
                  on_oauth_refresh: -> (tokens) {
                    # This proc will be called by the API client when an 
                    # OAuth refresh occurs
                    update_attributes({
                      oauth_access_token: tokens[:access_token],
                      oauth_refresh_token: tokens[:refresh_token]
                     })
                   }
  end

end

If I consume this API within a Rails transaction and a refresh occurs and then an error occurs - I can't persist the new OAuth tokens (because the proc above is also treated as part of the transaction):

u = User.first

User.transaction { 
  local_info = Info.create!

  # My tokens are expired so the client automatically
  # refreshes them and calls the proc that updates them locally.
  external_info = u.api.get_external_info(local_info.id)

  # Now when I try to locally save the info returned by the API an exception
  # occurs (for example due to validation). This rolls back the entire 
  # transaction (including the update of the user's new tokens.)
  local_info.info = external_info 
  local_info.save!
}

I'm simplifying the example but basically the consuming of the API and the persistence of data returned by the API need to happen within a transaction. How can I ensure the update to the user's tokens gets committed even if the parent transaction fails.

5
  • Just put it outside the transaction, no? Commented Jun 16, 2017 at 20:13
  • Unfortunately, no. The "nested transaction" is called by an API client that I don't have control over. Imagine that inside my transaction, I need to consume an API and my OAuth token expires. The API client refreshes my tokens but can't persist them because the transaction fails downstream. Commented Jun 16, 2017 at 20:16
  • 1
    Perhaps you need Autonomous Transactions. This article may help you grasp the concept and apply it to your ruby scenario. Commented Jun 16, 2017 at 20:33
  • Thanks for the link. Do you know of any Ruby/Rails abstraction layers that can help with this? I'd rather avoid writing stored procedures. Commented Jun 17, 2017 at 17:17
  • No. Ruby is not in my tool set. Commented Jun 22, 2017 at 18:41

3 Answers 3

4
+50

Have you tried opening a new db connection inside new thread, and in this thread execute the update

    u = User.first

    User.transaction { 
       local_info = Info.create!
   
       # My tokens are expired so the client automatically
       # refreshes them and calls the proc that updates them locally.
       external_info = u.api.get_external_info(local_info.id)

       # Now when I try to locally save the info returned by the API an exception
       # occurs (for example due to validation). This rolls back the entire 
       # transaction (including the update of the user's new tokens.)
       local_info.info = external_info 
       local_info.save!

       # Now open new thread
       # In the new thread open new db connection, separate from the one already opened
       # In the new connection execute update only for the tokens
       # Close new connection and new thread
       Thread.new do
          ActiveRecord::Base.connection_pool.with_connection do |connection|
             connection.execute("Your SQL statement that will update the user tokens")        
          end
       end.join
    }

I hope this helps

Sign up to request clarification or add additional context in comments.

1 Comment

Works great - thanks a lot! In fact, you can just wrap the transaction in the new Thread and it works.
2

Nermin's (the accepted) answer is correct. Here's an update for Rails >= 5.0

Thread.new do
  Rails.application.executor.wrap do
    record.save
  end
  # Note: record probably won't be updated here yet since it's async
end

Documented here: Rails guides threading and concurrency

Comments

0

This discussion from a previous question might help you. It looks like you can set a requires_new: true flag and essentially mark the child transaction as a sub transaction.

User.transaction { 
  User.transaction(requires_new: true) { 
    u.update_attribute(:name, 'test') 
  }; 

  u.update_attribute(:name, 'test2'); 

  raise 'boom' 
}

1 Comment

Thanks for your comment. I did try this but it looks like it offers the opposite of what I need. From what I understand, this will commit the second update if the nested transaction experiences an exception not the other way around.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.