To expand on my comment with some reference, yes you can assume that all database transactions have finished before your #after_commit callback runs. Here is a comment in active_record/transaction.rb (github):
#after_commit callbacks are called on every record saved or destroyed within a
transaction immediately after the transaction is committed. #after_rollback callbacks
are called on every record saved or destroyed within a transaction immediately after the
transaction or savepoint is rolled back.
These callbacks are useful for interacting with other systems since you will be guaranteed
that the callback is only executed when the database is in a permanent state.
The transaction code is a bit hairy, since it handles nested transactions, savepoints, etc. Ultimately, though, in TransactionManager.commit_transaction (active_record/connect_adapters/abstract/transaction.rb), the manager does indeed execute the database-specific commit action before running #after_commit callbacks on all unique record IDs association with a parent transaction.
def commit_transaction
@connection.lock.synchronize do
...
transaction.commit # executes database-specific commit action
transaction.commit_records # executes after_commit callbacks
end
end
All save and destroy operations, including their persistence callbacks, are implicitly wrapped in a transaction block. Per another comment in the Rails active_record/transaction.rb:
#transaction calls can be nested. By default, this makes all database
statements in the nested transaction block become part of the parent
transaction.
So, database statements generated by the #create and #destroy_all in your #after_save callback are committed at the same time with the original parent transaction, and can be depended on in the #after_commit callback. A simplified example to illustrate:
class Foo < ApplicationRecord
after_save :do_stuff
after_commit :check
def do_stuff
Bar.create!(name: 'bar')
end
def check
Rails.logger.info "Bar count: #{Bar.count}"
Rails.logger.info "name: #{Bar.first.name}"
end
end
Output log:
> Foo.create!(name: "Jerry")
(0.0ms) begin transaction
Foo Create (0.3ms) INSERT INTO "foos" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Jerry"], ["created_at", "2020-09-16 01:49:57.480367"], ["updated_at", "2020-09-16 01:49:57.480367"]]
Bar Create (0.2ms) INSERT INTO "bars" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "bar"], ["created_at", "2020-09-16 01:49:57.485953"], ["updated_at", "2020-09-16 01:49:57.485953"]]
(2.3ms) commit transaction
(0.1ms) SELECT COUNT(*) FROM "bars"
Bar count: 1
Bar Load (0.1ms) SELECT "bars".* FROM "bars" ORDER BY "bars"."id" ASC LIMIT ? [["LIMIT", 1]]
bar
Note that of course any #after_commit callbacks will be invoked for any model that is created, updated, or destroyed within the transaction. So, if Bar also defines #after_commit callbacks, those get run too.