Search code examples
ruby-on-railspostgresqlactiverecord

Commit a nested transaction


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.


Solution

  • 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