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.
- checksums.yaml +4 -4
- data/README.md +148 -46
- data/Rakefile +10 -24
- data/app/jobs/journaled/delivery_job.rb +17 -28
- data/app/models/concerns/journaled/changes.rb +5 -5
- data/app/models/journaled/change.rb +12 -12
- data/app/models/journaled/change_writer.rb +3 -2
- data/app/models/journaled/event.rb +1 -1
- data/app/models/journaled/writer.rb +32 -15
- data/lib/journaled/connection.rb +48 -0
- data/lib/journaled/engine.rb +5 -0
- data/lib/journaled/errors.rb +3 -0
- data/lib/journaled/relation_change_protection.rb +11 -10
- data/lib/journaled/rspec.rb +86 -0
- data/lib/journaled/transaction_ext.rb +31 -0
- data/lib/journaled/version.rb +1 -1
- data/lib/journaled.rb +17 -13
- metadata +54 -97
- data/spec/dummy/README.rdoc +0 -28
- data/spec/dummy/Rakefile +0 -6
- data/spec/dummy/bin/bundle +0 -3
- data/spec/dummy/bin/rails +0 -4
- data/spec/dummy/bin/rake +0 -4
- data/spec/dummy/config/application.rb +0 -25
- data/spec/dummy/config/boot.rb +0 -5
- data/spec/dummy/config/database.yml +0 -6
- data/spec/dummy/config/environment.rb +0 -5
- data/spec/dummy/config/environments/development.rb +0 -24
- data/spec/dummy/config/environments/test.rb +0 -37
- data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/spec/dummy/config/initializers/cookies_serializer.rb +0 -3
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/spec/dummy/config/initializers/inflections.rb +0 -16
- data/spec/dummy/config/initializers/mime_types.rb +0 -4
- data/spec/dummy/config/initializers/session_store.rb +0 -3
- data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/spec/dummy/config/locales/en.yml +0 -23
- data/spec/dummy/config/routes.rb +0 -56
- data/spec/dummy/config/secrets.yml +0 -22
- data/spec/dummy/config.ru +0 -4
- data/spec/dummy/db/schema.rb +0 -18
- data/spec/dummy/public/404.html +0 -67
- data/spec/dummy/public/422.html +0 -67
- data/spec/dummy/public/500.html +0 -66
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/jobs/journaled/delivery_job_spec.rb +0 -276
- data/spec/lib/journaled_spec.rb +0 -91
- data/spec/models/concerns/journaled/actor_spec.rb +0 -47
- data/spec/models/concerns/journaled/changes_spec.rb +0 -106
- data/spec/models/database_change_protection_spec.rb +0 -109
- data/spec/models/journaled/actor_uri_provider_spec.rb +0 -42
- data/spec/models/journaled/change_writer_spec.rb +0 -281
- data/spec/models/journaled/event_spec.rb +0 -236
- data/spec/models/journaled/json_schema_model/validator_spec.rb +0 -133
- data/spec/models/journaled/writer_spec.rb +0 -174
- data/spec/rails_helper.rb +0 -19
- data/spec/spec_helper.rb +0 -20
- data/spec/support/environment_spec_helper.rb +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac63d8988a5cbbd63340c019b82642a039a9dfee81f46d83b15e5a71d83e9cb7
|
4
|
+
data.tar.gz: ae809e4b05901d6eb73eb0196119999be2b0fd414c3af3c4bb30d280216098cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
23
|
-
|
24
|
-
require 'rspec/core/rake_task'
|
25
|
-
RSpec::Core::RakeTask.new
|
9
|
+
require 'rubocop/rake_task'
|
10
|
+
RuboCop::RakeTask.new
|
26
11
|
|
27
|
-
|
28
|
-
|
12
|
+
require 'rspec/core'
|
13
|
+
require 'rspec/core/rake_task'
|
14
|
+
RSpec::Core::RakeTask.new(:spec)
|
29
15
|
|
30
|
-
|
16
|
+
def default_task
|
31
17
|
if ENV['APPRAISAL_INITIALIZED'] || ENV['CI']
|
32
|
-
|
18
|
+
%i(rubocop spec)
|
33
19
|
else
|
34
20
|
require 'appraisal'
|
35
21
|
Appraisal::Task.new
|
36
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
52
|
-
|
39
|
+
def to_h
|
40
|
+
{ stream_name: stream_name, data: serialized_event, partition_key: partition_key }
|
41
|
+
end
|
53
42
|
end
|
54
43
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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/
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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.
|
56
|
-
|
56
|
+
change_hash.transform_values do |v|
|
57
|
+
v[index]
|
57
58
|
end
|
58
59
|
end
|
59
60
|
|
@@ -26,34 +26,51 @@ class Journaled::Writer
|
|
26
26
|
|
27
27
|
def journal!
|
28
28
|
validate!
|
29
|
-
|
30
|
-
|
31
|
-
.
|
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
|
data/lib/journaled/engine.rb
CHANGED
@@ -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
|
@@ -1,14 +1,15 @@
|
|
1
1
|
module Journaled::RelationChangeProtection
|
2
|
-
def update_all(updates, force: false) # rubocop:disable Metrics/AbcSize
|
3
|
-
unless force || !@klass.respond_to?(:journaled_attribute_names) || @klass.journaled_attribute_names.empty?
|
4
|
-
conflicting_journaled_attribute_names =
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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:
|