journaled 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +148 -46
  3. data/Rakefile +10 -24
  4. data/app/jobs/journaled/delivery_job.rb +17 -28
  5. data/app/models/concerns/journaled/changes.rb +5 -5
  6. data/app/models/journaled/change.rb +12 -12
  7. data/app/models/journaled/change_writer.rb +3 -2
  8. data/app/models/journaled/event.rb +1 -1
  9. data/app/models/journaled/writer.rb +32 -15
  10. data/lib/journaled/connection.rb +48 -0
  11. data/lib/journaled/engine.rb +5 -0
  12. data/lib/journaled/errors.rb +3 -0
  13. data/lib/journaled/relation_change_protection.rb +11 -10
  14. data/lib/journaled/rspec.rb +86 -0
  15. data/lib/journaled/transaction_ext.rb +31 -0
  16. data/lib/journaled/version.rb +1 -1
  17. data/lib/journaled.rb +17 -13
  18. metadata +54 -97
  19. data/spec/dummy/README.rdoc +0 -28
  20. data/spec/dummy/Rakefile +0 -6
  21. data/spec/dummy/bin/bundle +0 -3
  22. data/spec/dummy/bin/rails +0 -4
  23. data/spec/dummy/bin/rake +0 -4
  24. data/spec/dummy/config/application.rb +0 -25
  25. data/spec/dummy/config/boot.rb +0 -5
  26. data/spec/dummy/config/database.yml +0 -6
  27. data/spec/dummy/config/environment.rb +0 -5
  28. data/spec/dummy/config/environments/development.rb +0 -24
  29. data/spec/dummy/config/environments/test.rb +0 -37
  30. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
  31. data/spec/dummy/config/initializers/cookies_serializer.rb +0 -3
  32. data/spec/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  33. data/spec/dummy/config/initializers/inflections.rb +0 -16
  34. data/spec/dummy/config/initializers/mime_types.rb +0 -4
  35. data/spec/dummy/config/initializers/session_store.rb +0 -3
  36. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
  37. data/spec/dummy/config/locales/en.yml +0 -23
  38. data/spec/dummy/config/routes.rb +0 -56
  39. data/spec/dummy/config/secrets.yml +0 -22
  40. data/spec/dummy/config.ru +0 -4
  41. data/spec/dummy/db/schema.rb +0 -18
  42. data/spec/dummy/public/404.html +0 -67
  43. data/spec/dummy/public/422.html +0 -67
  44. data/spec/dummy/public/500.html +0 -66
  45. data/spec/dummy/public/favicon.ico +0 -0
  46. data/spec/jobs/journaled/delivery_job_spec.rb +0 -276
  47. data/spec/lib/journaled_spec.rb +0 -91
  48. data/spec/models/concerns/journaled/actor_spec.rb +0 -47
  49. data/spec/models/concerns/journaled/changes_spec.rb +0 -106
  50. data/spec/models/database_change_protection_spec.rb +0 -109
  51. data/spec/models/journaled/actor_uri_provider_spec.rb +0 -42
  52. data/spec/models/journaled/change_writer_spec.rb +0 -281
  53. data/spec/models/journaled/event_spec.rb +0 -236
  54. data/spec/models/journaled/json_schema_model/validator_spec.rb +0 -133
  55. data/spec/models/journaled/writer_spec.rb +0 -174
  56. data/spec/rails_helper.rb +0 -19
  57. data/spec/spec_helper.rb +0 -20
  58. data/spec/support/environment_spec_helper.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88602263644ea51719a18c5ee3e719d2c13a9e69049e1f20a52a44edc38a0835
4
- data.tar.gz: c7cafdb9f305e51c3590d09815d5d6f909db496866c774ab442ffaa334be927c
3
+ metadata.gz: ac63d8988a5cbbd63340c019b82642a039a9dfee81f46d83b15e5a71d83e9cb7
4
+ data.tar.gz: ae809e4b05901d6eb73eb0196119999be2b0fd414c3af3c4bb30d280216098cf
5
5
  SHA512:
6
- metadata.gz: 6bf229efd53882ed50931c964c1564b01603fa22ea2083b8a6039d0a0ed972d2cdc7bfc5913a9e9539d7dd023294c2a226963e2e2a526bf857926afe3e0c76c0
7
- data.tar.gz: cf7b28c58c40c9ee8348df60095a7b544c0a8b0ea448ffa20021aceda61ae2feda61110ef9685756d4bac7d3d887e12a181f1840d435b2d2f85aeba5d8a52e45
6
+ metadata.gz: 72bcbe0ae43717280eb8a0b3383672cc87762e4c2b9b082778f1beac249004ef90a41e32e6fbabd693377a4d0af6cf6fc78df8a2b8ff44067276dd502cece63a
7
+ data.tar.gz: b21401ba2cf6155a25f50f1c58c634e3036754ec69b8d7f287c86408f69819aed5cfac1a43d55636a2be461545096b1510318a6ce665bf47595f30c8b90bceb9
data/README.md CHANGED
@@ -179,52 +179,6 @@ journaling. Note that the less-frequently-used methods `toggle`,
179
179
  `increment*`, `decrement*`, and `update_counters` are not intercepted at
180
180
  this time.
181
181
 
182
- ### Tagged Events
183
-
184
- Events may be optionally marked as "tagged." This will add a `tags` field, intended for tracing and
185
- auditing purposes.
186
-
187
- ```ruby
188
- class MyEvent
189
- include Journaled::Event
190
-
191
- journal_attributes :attr_1, :attr_2, tagged: true
192
- end
193
- ```
194
-
195
- You may then use `Journaled.tag!` and `Journaled.tagged` inside of your
196
- `ApplicationController` and `ApplicationJob` classes (or anywhere else!) to tag
197
- all events with request and job metadata:
198
-
199
- ```ruby
200
- class ApplicationController < ActionController::Base
201
- before_action do
202
- Journaled.tag!(request_id: request.request_id, current_user_id: current_user&.id)
203
- end
204
- end
205
-
206
- class ApplicationJob < ActiveJob::Base
207
- around_perform do |job, perform|
208
- Journaled.tagged(job_id: job.id) { perform.call }
209
- end
210
- end
211
- ```
212
-
213
- This feature relies on `ActiveSupport::CurrentAttributes` under the hood, so these tags are local to
214
- the current thread, and will be cleared at the end of each request request/job.
215
-
216
- #### Testing
217
-
218
- If you use RSpec (and have required `journaled/rspec` in your
219
- `spec/rails_helper.rb`), you can regression-protect important journaling
220
- config with the `journal_changes_to` matcher:
221
-
222
- ```ruby
223
- it "journals exactly these things or there will be heck to pay" do
224
- expect(User).to journal_changes_to(:email, :first_name, :last_name, as: :identity_change)
225
- end
226
- ```
227
-
228
182
  ### Custom Journaling
229
183
 
230
184
  For every custom implementation of journaling in your application, define the JSON schema for the attributes in your event.
@@ -309,6 +263,40 @@ An event like the following will be journaled to kinesis:
309
263
  }
310
264
  ```
311
265
 
266
+ ### Tagged Events
267
+
268
+ Events may be optionally marked as "tagged." This will add a `tags` field, intended for tracing and
269
+ auditing purposes.
270
+
271
+ ```ruby
272
+ class MyEvent
273
+ include Journaled::Event
274
+
275
+ journal_attributes :attr_1, :attr_2, tagged: true
276
+ end
277
+ ```
278
+
279
+ You may then use `Journaled.tag!` and `Journaled.tagged` inside of your
280
+ `ApplicationController` and `ApplicationJob` classes (or anywhere else!) to tag
281
+ all events with request and job metadata:
282
+
283
+ ```ruby
284
+ class ApplicationController < ActionController::Base
285
+ before_action do
286
+ Journaled.tag!(request_id: request.request_id, current_user_id: current_user&.id)
287
+ end
288
+ end
289
+
290
+ class ApplicationJob < ActiveJob::Base
291
+ around_perform do |job, perform|
292
+ Journaled.tagged(job_id: job.id) { perform.call }
293
+ end
294
+ end
295
+ ```
296
+
297
+ This feature relies on `ActiveSupport::CurrentAttributes` under the hood, so these tags are local to
298
+ the current thread, and will be cleared at the end of each request request/job.
299
+
312
300
  ### Helper methods for custom events
313
301
 
314
302
  Journaled provides a couple helper methods that may be useful in your
@@ -352,6 +340,102 @@ Returns one of the following in order of preference:
352
340
  In order for this to be most useful, you must configure your controller
353
341
  as described in [Change Journaling](#change-journaling) above.
354
342
 
343
+ ### Testing
344
+
345
+ If you use RSpec, you can test for journaling behaviors with the
346
+ `journal_event(s)_including` and `journal_changes_to` matchers. First, make
347
+ sure to require `journaled/rspec` in your spec setup (e.g.
348
+ `spec/rails_helper.rb`):
349
+
350
+ ```ruby
351
+ require 'journaled/rspec'
352
+ ```
353
+
354
+ #### Checking for specific events
355
+
356
+ The `journal_event_including` and `journal_events_including` matchers allow you
357
+ to check for one or more matching event being journaled:
358
+
359
+ ```ruby
360
+ expect { my_code }
361
+ .to journal_event_including(name: 'foo')
362
+ expect { my_code }
363
+ .to journal_events_including({ name: 'foo', value: 1 }, { name: 'foo', value: 2 })
364
+ ```
365
+
366
+ This will only perform matches on the specified fields (and will not match one
367
+ way or the other against unspecified fields). These matchers will also ignore
368
+ any extraneous events that are not positively matched (as they may be unrelated
369
+ to behavior under test).
370
+
371
+ When writing tests, pairing every positive assertion with a negative assertion
372
+ is a good practice, and so negative matching is also supported (via both
373
+ `.not_to` and `.to not_`):
374
+
375
+ ```ruby
376
+ expect { my_code }
377
+ .not_to journal_events_including({ name: 'foo' }, { name: 'bar' })
378
+ expect { my_code }
379
+ .to raise_error(SomeError)
380
+ .and not_journal_event_including(name: 'foo') # the `not_` variant can chain off of `.and`
381
+ ```
382
+
383
+ Several chainable modifiers are also available:
384
+
385
+ ```ruby
386
+ expect { my_code }.to journal_event_including(name: 'foo')
387
+ .with_schema_name('my_event_schema')
388
+ .with_partition_key(user.id)
389
+ .with_stream_name('my_stream_name')
390
+ .with_enqueue_opts(run_at: future_time)
391
+ .with_priority(999)
392
+ ```
393
+
394
+ All of this can be chained together to test for multiple sets of events with
395
+ multiple sets of options:
396
+
397
+ ```ruby
398
+ expect { subject.journal! }
399
+ .to journal_events_including({ name: 'event1', value: 300 }, { name: 'event2', value: 200 })
400
+ .with_priority(10)
401
+ .and journal_event_including(name: 'event3', value: 100)
402
+ .with_priority(20)
403
+ .and not_journal_event_including(name: 'other_event')
404
+ ```
405
+
406
+ #### Checking for `Journaled::Changes` declarations
407
+
408
+ The `journal_changes_to` matcher checks against the list of attributes specified
409
+ on the model. It does not actually test that an event is emitted within a given
410
+ codepath, and is instead intended to guard against accidental regressions that
411
+ may impact external consumers of these events:
412
+
413
+ ```ruby
414
+ it "journals exactly these things or there will be heck to pay" do
415
+ expect(User).to journal_changes_to(:email, :first_name, :last_name, as: :identity_change)
416
+ end
417
+ ```
418
+
419
+ ### Instrumentation
420
+
421
+ When an event is enqueued, an `ActiveSupport::Notification` titled
422
+ `journaled.event.enqueue` is emitted. Its payload will include the `:event` and
423
+ its background job `:priority`.
424
+
425
+ This can be forwarded along to your preferred monitoring solution via a Rails
426
+ initializer:
427
+
428
+ ```ruby
429
+ ActiveSupport::Notifications.subscribe('journaled.event.enqueue') do |*args|
430
+ payload = ActiveSupport::Notifications::Event.new(*args).payload
431
+ journaled_event = payload[:event]
432
+
433
+ tags = { priority: payload[:priority], event_type: journaled_event.journaled_attributes[:event_type] }
434
+
435
+ Statsd.increment('journaled.event.enqueue', tags: tags.map { |k,v| "#{k.to_s[0..64]}:#{v.to_s[0..255]}" })
436
+ end
437
+ ```
438
+
355
439
  ## Upgrades
356
440
 
357
441
  Since this gem relies on background jobs (which can remain in the queue across
@@ -360,6 +444,24 @@ gem version.
360
444
 
361
445
  As such, **we always recommend upgrading only one major version at a time.**
362
446
 
447
+ ### Upgrading from 4.3.0
448
+
449
+ Versions of Journaled prior to 5.0 would enqueue events one at a time, but 5.0
450
+ introduces a new transaction-aware feature that will bundle up all events
451
+ emitted within a transaction and enqueue them all in a single "batch" job
452
+ directly before the SQL `COMMIT` statement. This reduces the database impact of
453
+ emitting a large volume of events at once.
454
+
455
+ This feature can be disabled conditionally:
456
+
457
+ ```ruby
458
+ Journaled.transactional_batching_enabled = false
459
+ ```
460
+
461
+ Backwards compatibility has been included for background jobs enqueued by
462
+ version 4.0 and above, but **has been dropped for jobs emitted by versions prior
463
+ to 4.0**. (Again, be sure to upgrade only one major version at a time.)
464
+
363
465
  ### Upgrading from 3.1.0
364
466
 
365
467
  Versions of Journaled prior to 4.0 relied directly on environment variables for stream names, but now stream names are configured directly.
data/Rakefile CHANGED
@@ -4,37 +4,23 @@ rescue LoadError
4
4
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
5
  end
6
6
 
7
- require 'rdoc/task'
8
-
9
- RDoc::Task.new(:rdoc) do |rdoc|
10
- rdoc.rdoc_dir = 'rdoc'
11
- rdoc.title = 'Journaled'
12
- rdoc.options << '--line-numbers'
13
- rdoc.rdoc_files.include('README.rdoc')
14
- rdoc.rdoc_files.include('lib/**/*.rb')
15
- end
16
-
17
- APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
18
- load 'rails/tasks/engine.rake'
19
-
20
7
  Bundler::GemHelper.install_tasks
21
8
 
22
- if %w(development test).include? Rails.env
23
- require 'rspec/core'
24
- require 'rspec/core/rake_task'
25
- RSpec::Core::RakeTask.new
9
+ require 'rubocop/rake_task'
10
+ RuboCop::RakeTask.new
26
11
 
27
- require 'rubocop/rake_task'
28
- RuboCop::RakeTask.new
12
+ require 'rspec/core'
13
+ require 'rspec/core/rake_task'
14
+ RSpec::Core::RakeTask.new(:spec)
29
15
 
30
- task(:default).clear
16
+ def default_task
31
17
  if ENV['APPRAISAL_INITIALIZED'] || ENV['CI']
32
- task default: %i(rubocop spec)
18
+ %i(rubocop spec)
33
19
  else
34
20
  require 'appraisal'
35
21
  Appraisal::Task.new
36
- task default: :appraisal
22
+ %i(appraisal)
37
23
  end
38
-
39
- task 'db:test:prepare' => 'db:setup'
40
24
  end
25
+
26
+ task(:default).clear.enhance(default_task)
@@ -12,26 +12,11 @@ module Journaled
12
12
  raise KinesisTemporaryFailure
13
13
  end
14
14
 
15
- UNSPECIFIED = Object.new
16
- private_constant :UNSPECIFIED
17
-
18
- def perform(serialized_event:, partition_key:, stream_name: UNSPECIFIED, app_name: UNSPECIFIED)
19
- @serialized_event = serialized_event
20
- @partition_key = partition_key
21
- if app_name != UNSPECIFIED
22
- @stream_name = self.class.legacy_computed_stream_name(app_name: app_name)
23
- elsif stream_name != UNSPECIFIED && !stream_name.nil?
24
- @stream_name = stream_name
25
- else
26
- raise(ArgumentError, 'missing keyword: stream_name')
27
- end
28
-
29
- journal!
30
- end
15
+ def perform(*events, **legacy_kwargs)
16
+ events << legacy_kwargs if legacy_kwargs.present?
17
+ @kinesis_records = events.map { |e| KinesisRecord.new(**e.delete_if { |_k, v| v.nil? }) }
31
18
 
32
- def self.legacy_computed_stream_name(app_name:)
33
- env_var_name = [app_name&.upcase, 'JOURNALED_STREAM_NAME'].compact.join('_')
34
- ENV.fetch(env_var_name)
19
+ journal! if Journaled.enabled?
35
20
  end
36
21
 
37
22
  def kinesis_client_config
@@ -46,18 +31,22 @@ module Journaled
46
31
 
47
32
  private
48
33
 
49
- attr_reader :serialized_event, :partition_key, :stream_name
34
+ KinesisRecord = Struct.new(:serialized_event, :partition_key, :stream_name, keyword_init: true) do
35
+ def initialize(serialized_event:, partition_key:, stream_name:)
36
+ super(serialized_event: serialized_event, partition_key: partition_key, stream_name: stream_name)
37
+ end
50
38
 
51
- def journal!
52
- kinesis_client.put_record record if Journaled.enabled?
39
+ def to_h
40
+ { stream_name: stream_name, data: serialized_event, partition_key: partition_key }
41
+ end
53
42
  end
54
43
 
55
- def record
56
- {
57
- stream_name: stream_name,
58
- data: serialized_event,
59
- partition_key: partition_key,
60
- }
44
+ attr_reader :kinesis_records
45
+
46
+ def journal!
47
+ kinesis_records.map do |record|
48
+ kinesis_client.put_record(**record.to_h)
49
+ end
61
50
  end
62
51
 
63
52
  def kinesis_client
@@ -39,8 +39,8 @@ module Journaled::Changes
39
39
  end
40
40
  end
41
41
 
42
- def update_columns(attributes, force: false)
43
- unless force || self.class.journaled_attribute_names.empty?
42
+ def update_columns(attributes, opts = { force: false })
43
+ unless opts[:force] || self.class.journaled_attribute_names.empty?
44
44
  conflicting_journaled_attribute_names = self.class.journaled_attribute_names & attributes.keys.map(&:to_sym)
45
45
  raise(<<~ERROR) if conflicting_journaled_attribute_names.present?
46
46
  #update_columns aborted by Journaled::Changes due to journaled attributes:
@@ -56,7 +56,7 @@ module Journaled::Changes
56
56
  end
57
57
 
58
58
  class_methods do
59
- def journal_changes_to(*attribute_names, as:, enqueue_with: {}) # rubocop:disable Naming/UncommunicativeMethodParamName
59
+ def journal_changes_to(*attribute_names, as:, enqueue_with: {}) # rubocop:disable Naming/MethodParameterName
60
60
  if attribute_names.empty? || attribute_names.any? { |n| !n.is_a?(Symbol) }
61
61
  raise "one or more symbol attribute_name arguments is required"
62
62
  end
@@ -69,8 +69,8 @@ module Journaled::Changes
69
69
  end
70
70
 
71
71
  if Rails::VERSION::MAJOR > 5 || (Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR >= 2)
72
- def delete(id_or_array, force: false)
73
- if force || journaled_attribute_names.empty?
72
+ def delete(id_or_array, opts = { force: false })
73
+ if opts[:force] || journaled_attribute_names.empty?
74
74
  where(primary_key => id_or_array).delete_all(force: true)
75
75
  else
76
76
  raise(<<~ERROR)
@@ -2,20 +2,20 @@ class Journaled::Change
2
2
  include Journaled::Event
3
3
 
4
4
  attr_reader :table_name,
5
- :record_id,
6
- :database_operation,
7
- :logical_operation,
8
- :changes,
9
- :journaled_stream_name,
10
- :journaled_enqueue_opts,
11
- :actor
5
+ :record_id,
6
+ :database_operation,
7
+ :logical_operation,
8
+ :changes,
9
+ :journaled_stream_name,
10
+ :journaled_enqueue_opts,
11
+ :actor
12
12
 
13
13
  journal_attributes :table_name,
14
- :record_id,
15
- :database_operation,
16
- :logical_operation,
17
- :changes,
18
- :actor
14
+ :record_id,
15
+ :database_operation,
16
+ :logical_operation,
17
+ :changes,
18
+ :actor
19
19
 
20
20
  def initialize(table_name:,
21
21
  record_id:,
@@ -1,5 +1,6 @@
1
1
  class Journaled::ChangeWriter
2
2
  attr_reader :model, :change_definition
3
+
3
4
  delegate :attribute_names, :logical_operation, to: :change_definition
4
5
 
5
6
  def initialize(model:, change_definition:)
@@ -52,8 +53,8 @@ class Journaled::ChangeWriter
52
53
  private
53
54
 
54
55
  def pluck_changed_values(change_hash, index:)
55
- change_hash.each_with_object({}) do |(k, v), result|
56
- result[k] = v[index]
56
+ change_hash.transform_values do |v|
57
+ v[index]
57
58
  end
58
59
  end
59
60
 
@@ -63,7 +63,7 @@ module Journaled::Event
63
63
  end
64
64
 
65
65
  included do
66
- cattr_accessor(:journaled_enqueue_opts, instance_writer: false) { {} }
66
+ class_attribute :journaled_enqueue_opts, default: {}
67
67
 
68
68
  journal_attributes :id, :event_type, :created_at
69
69
  end
@@ -26,34 +26,51 @@ class Journaled::Writer
26
26
 
27
27
  def journal!
28
28
  validate!
29
- Journaled::DeliveryJob
30
- .set(journaled_enqueue_opts.reverse_merge(priority: Journaled.job_priority))
31
- .perform_later(delivery_perform_args)
29
+
30
+ ActiveSupport::Notifications.instrument('journaled.event.stage', event: journaled_event, **journaled_enqueue_opts) do
31
+ if Journaled::Connection.available?
32
+ Journaled::Connection.stage!(journaled_event)
33
+ else
34
+ self.class.enqueue!(journaled_event)
35
+ end
36
+ end
37
+ end
38
+
39
+ def self.enqueue!(*events)
40
+ events.group_by(&:journaled_enqueue_opts).each do |enqueue_opts, batch|
41
+ job_opts = enqueue_opts.reverse_merge(priority: Journaled.job_priority)
42
+ ActiveSupport::Notifications.instrument('journaled.batch.enqueue', batch: batch, **job_opts) do
43
+ Journaled::DeliveryJob.set(job_opts).perform_later(*delivery_perform_args(batch))
44
+
45
+ batch.each { |event| ActiveSupport::Notifications.instrument('journaled.event.enqueue', event: event, **job_opts) }
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.delivery_perform_args(events)
51
+ events.map do |event|
52
+ {
53
+ serialized_event: event.journaled_attributes.to_json,
54
+ partition_key: event.journaled_partition_key,
55
+ stream_name: event.journaled_stream_name,
56
+ }
57
+ end
32
58
  end
33
59
 
34
60
  private
35
61
 
36
62
  attr_reader :journaled_event
63
+
37
64
  delegate(*EVENT_METHOD_NAMES, to: :journaled_event)
38
65
 
39
66
  def validate!
67
+ serialized_event = journaled_event.journaled_attributes.to_json
68
+
40
69
  schema_validator('base_event').validate! serialized_event
41
70
  schema_validator('tagged_event').validate! serialized_event if journaled_event.tagged?
42
71
  schema_validator(journaled_schema_name).validate! serialized_event
43
72
  end
44
73
 
45
- def delivery_perform_args
46
- {
47
- serialized_event: serialized_event,
48
- partition_key: journaled_partition_key,
49
- stream_name: journaled_stream_name,
50
- }
51
- end
52
-
53
- def serialized_event
54
- @serialized_event ||= journaled_attributes.to_json
55
- end
56
-
57
74
  def schema_validator(schema_name)
58
75
  Journaled::JsonSchemaModel::Validator.new(schema_name)
59
76
  end
@@ -0,0 +1,48 @@
1
+ module Journaled
2
+ module Connection
3
+ class << self
4
+ def available?
5
+ Journaled.transactional_batching_enabled && transaction_open?
6
+ end
7
+
8
+ def stage!(event)
9
+ raise TransactionSafetyError, <<~MSG unless transaction_open?
10
+ Transaction not available! By default, journaled event batching requires an open database transaction.
11
+ MSG
12
+
13
+ connection.current_transaction._journaled_staged_events << event
14
+ end
15
+
16
+ private
17
+
18
+ def transaction_open?
19
+ connection.transaction_open?
20
+ end
21
+
22
+ def connection
23
+ if Journaled.queue_adapter.in? %w(delayed delayed_job)
24
+ Delayed::Job.connection
25
+ elsif Journaled.queue_adapter == 'good_job'
26
+ GoodJob::BaseRecord.connection
27
+ elsif Journaled.queue_adapter == 'que'
28
+ Que::ActiveRecord::Model.connection
29
+ elsif Journaled.queue_adapter == 'test' && Rails.env.test?
30
+ ActiveRecord::Base.connection
31
+ else
32
+ raise "Unsupported adapter: #{Journaled.queue_adapter}"
33
+ end
34
+ end
35
+ end
36
+
37
+ module TestOnlyBehaviors
38
+ def transaction_open?
39
+ # Transactional fixtures wrap all tests in an outer, non-joinable transaction:
40
+ super && (connection.open_transactions > 1 || connection.current_transaction.joinable?)
41
+ end
42
+ end
43
+
44
+ class << self
45
+ prepend TestOnlyBehaviors if Rails.env.test?
46
+ end
47
+ end
48
+ end
@@ -4,6 +4,11 @@ module Journaled
4
4
  ActiveSupport.on_load(:active_job) do
5
5
  Journaled.detect_queue_adapter! unless Journaled.development_or_test?
6
6
  end
7
+
8
+ ActiveSupport.on_load(:active_record) do
9
+ require 'journaled/transaction_ext'
10
+ ActiveRecord::ConnectionAdapters::Transaction.prepend Journaled::TransactionExt
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -0,0 +1,3 @@
1
+ module Journaled
2
+ class TransactionSafetyError < StandardError; end
3
+ end
@@ -1,14 +1,15 @@
1
1
  module Journaled::RelationChangeProtection
2
- def update_all(updates, force: false) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
3
- unless force || !@klass.respond_to?(:journaled_attribute_names) || @klass.journaled_attribute_names.empty?
4
- conflicting_journaled_attribute_names = if updates.is_a?(Hash)
5
- @klass.journaled_attribute_names & updates.keys.map(&:to_sym)
6
- elsif updates.is_a?(String)
7
- @klass.journaled_attribute_names.select do |a|
8
- updates.match?(/\b(?<!')#{a}(?!')\b/)
9
- end
10
- else
11
- raise "unsupported type '#{updates.class}' for 'updates'"
2
+ def update_all(updates, opts = { force: false }) # rubocop:disable Metrics/AbcSize
3
+ unless opts[:force] || !@klass.respond_to?(:journaled_attribute_names) || @klass.journaled_attribute_names.empty?
4
+ conflicting_journaled_attribute_names = case updates
5
+ when Hash
6
+ @klass.journaled_attribute_names & updates.keys.map(&:to_sym)
7
+ when String
8
+ @klass.journaled_attribute_names.select do |a|
9
+ updates.match?(/\b(?<!')#{a}(?!')\b/)
10
+ end
11
+ else
12
+ raise "unsupported type '#{updates.class}' for 'updates'"
12
13
  end
13
14
  raise(<<~ERROR) if conflicting_journaled_attribute_names.present?
14
15
  .update_all aborted by Journaled::Changes due to journaled attributes: