Skip to content

Commit

Permalink
Threadsafe "with_transactional_batching" block method (#29)
Browse files Browse the repository at this point in the history
This adds a `Journaled.with_transactional_batching { }` feature that can be used to conditionally enable batching within the current thread, even if the feature is disabled globally.
  • Loading branch information
smudge authored Dec 1, 2022
1 parent dd4adb8 commit 9081633
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 9 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,14 @@ This feature can be disabled conditionally:
Journaled.transactional_batching_enabled = false
```

And can then be enabled via the following block:

```ruby
Journaled.with_transactional_batching do
# your code
end
```

Backwards compatibility has been included for background jobs enqueued by
version 4.0 and above, but **has been dropped for jobs emitted by versions prior
to 4.0**. (Again, be sure to upgrade only one major version at a time.)
Expand Down
14 changes: 13 additions & 1 deletion lib/journaled.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ module Journaled
mattr_accessor(:http_open_timeout) { 2 }
mattr_accessor(:http_read_timeout) { 60 }
mattr_accessor(:job_base_class_name) { 'ActiveJob::Base' }
mattr_accessor(:transactional_batching_enabled) { true }
mattr_writer(:transactional_batching_enabled) { true }

def self.transactional_batching_enabled?
Thread.current[:journaled_transactional_batching_enabled] || @@transactional_batching_enabled
end

def self.with_transactional_batching
value_was = Thread.current[:journaled_transactional_batching_enabled]
Thread.current[:journaled_transactional_batching_enabled] = true
yield
ensure
Thread.current[:journaled_transactional_batching_enabled] = value_was
end

def self.development_or_test?
%w(development test).include?(Rails.env)
Expand Down
2 changes: 1 addition & 1 deletion lib/journaled/audit_log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def enabled?

def dup
super.tap do |config|
config.ignored_columns = ignored_columns.dup
config.ignored_columns = ignored_columns.dup # rubocop:disable Rails/IgnoredColumnsAssignment
config.enqueue_opts = enqueue_opts.dup
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/journaled/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ module Journaled
module Connection
class << self
def available?
Journaled.transactional_batching_enabled && transaction_open?
Journaled.transactional_batching_enabled? && transaction_open?
end

def stage!(event)
raise TransactionSafetyError, <<~MSG unless transaction_open?
raise TransactionSafetyError, <<~MSG unless available?
Transaction not available! By default, journaled event batching requires an open database transaction.
MSG

Expand Down
2 changes: 1 addition & 1 deletion lib/journaled/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Journaled
VERSION = "5.2.0".freeze
VERSION = "5.3.0".freeze
end
21 changes: 18 additions & 3 deletions spec/lib/journaled/connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
end
end

context 'when transactional batching is disabled' do
context 'when transactional batching is disabled globally' do
around do |example|
Journaled.transactional_batching_enabled = false
example.run
Expand All @@ -36,8 +36,23 @@
end

it 'returns false, and raises an error when events are staged' do
expect(described_class.available?).to be false
expect { described_class.stage!(event) }.to raise_error(Journaled::TransactionSafetyError)
ActiveRecord::Base.transaction do
expect(described_class.available?).to be false
expect { described_class.stage!(event) }.to raise_error(Journaled::TransactionSafetyError)
end
end

context 'but thread-local batching is enabled' do
around do |example|
Journaled.with_transactional_batching { example.run }
end

it 'returns true, and allows for staging events' do
ActiveRecord::Base.transaction do
expect(described_class.available?).to be true
expect { described_class.stage!(event) }.not_to raise_error
end
end
end
end
end
Expand Down
23 changes: 22 additions & 1 deletion spec/models/journaled/writer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ def fake_event(num)
end
end

context 'when transactional batching is disabled' do
context 'when transactional batching is disabled globally' do
around do |example|
Journaled.transactional_batching_enabled = false
example.run
Expand All @@ -415,6 +415,27 @@ def fake_event(num)
.and journal_event_including(id: 'FAKE_UUID_1', event_type: 'fake_event_1')
.and journal_event_including(id: 'FAKE_UUID_5', event_type: 'fake_event_5')
end

context 'but thread-local transactional batching is enabled' do
around do |example|
Journaled.with_transactional_batching { example.run }
end

it 'batches multiple events and does not enqueue until the end of a transaction' do
expect {
ActiveRecord::Base.transaction do
expect { described_class.new(journaled_event: fake_event(1)).journal! }
.to not_change { enqueued_jobs.count }.from(0)
.and not_journal_event_including(id: 'FAKE_UUID_1', event_type: 'fake_event_1')
expect { described_class.new(journaled_event: fake_event(2)).journal! }
.to not_change { enqueued_jobs.count }.from(0)
.and not_journal_event_including(id: 'FAKE_UUID_2', event_type: 'fake_event_2')
end
}.to change { enqueued_jobs.count }.from(0).to(1)
.and journal_event_including(id: 'FAKE_UUID_1', event_type: 'fake_event_1')
.and journal_event_including(id: 'FAKE_UUID_2', event_type: 'fake_event_2')
end
end
end

context 'when an event is enqueued in its own before_commit callback' do
Expand Down

0 comments on commit 9081633

Please sign in to comment.