journaled 3.1.0 → 4.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 +54 -18
- data/app/jobs/journaled/delivery_job.rb +14 -5
- data/app/models/journaled/change.rb +3 -3
- data/app/models/journaled/change_writer.rb +5 -5
- data/app/models/journaled/event.rb +2 -2
- data/app/models/journaled/writer.rb +2 -2
- data/lib/journaled/version.rb +1 -1
- data/lib/journaled.rb +1 -1
- data/spec/jobs/journaled/delivery_job_spec.rb +65 -10
- data/spec/models/journaled/change_writer_spec.rb +10 -10
- data/spec/models/journaled/event_spec.rb +4 -4
- data/spec/models/journaled/writer_spec.rb +4 -4
- metadata +3 -6
- data/app/models/journaled/delivery.rb +0 -88
- data/spec/models/journaled/delivery_spec.rb +0 -222
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4cefb08a9d7ab294f9084f150f89cda921982fffdb0ec7b0a5bc702ede1d23b8
|
4
|
+
data.tar.gz: 21a484a10c58a3f551e95af064b5f718b9496ba7840ac6c1ac84142b859942db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59d07f836a83dde4e4a24a8a5f326ff5cbc7a751d92fb6df66ee22b5b31ac1aa63326675167d45bcd8c572a0e4afeb831774a468eb45097a71c6940a79ce2681
|
7
|
+
data.tar.gz: 807c603b1085819ed003ed8053f81b6fec59ba895c920bc24ac178b0d3188fac554a2ed9b2dcb80ceceb2a26fc5effe6af5d52819f66d041ad7959cfc30957da
|
data/README.md
CHANGED
@@ -24,16 +24,16 @@ durable, eventually consistent record that discrete events happened.
|
|
24
24
|
[configure ActiveJob](https://guides.rubyonrails.org/active_job_basics.html)
|
25
25
|
to use one of the following queue adapters:
|
26
26
|
|
27
|
-
- `:delayed_job` (via `delayed_job_active_record`)
|
28
|
-
- `:que`
|
29
|
-
- `:good_job`
|
30
|
-
- `:delayed`
|
27
|
+
- `:delayed_job` (via `delayed_job_active_record`)
|
28
|
+
- `:que`
|
29
|
+
- `:good_job`
|
30
|
+
- `:delayed`
|
31
31
|
|
32
|
-
Ensure that your queue adapter is not configured to delete jobs on failure.
|
32
|
+
Ensure that your queue adapter is not configured to delete jobs on failure.
|
33
33
|
|
34
|
-
**If you launch your application in production mode and the gem detects that
|
35
|
-
`ActiveJob::Base.queue_adapter` is not in the above list, it will raise an exception
|
36
|
-
and prevent your application from performing unsafe journaling.**
|
34
|
+
**If you launch your application in production mode and the gem detects that
|
35
|
+
`ActiveJob::Base.queue_adapter` is not in the above list, it will raise an exception
|
36
|
+
and prevent your application from performing unsafe journaling.**
|
37
37
|
|
38
38
|
2. To integrate Journaled into your application, simply include the gem in your
|
39
39
|
app's Gemfile.
|
@@ -51,20 +51,21 @@ app's Gemfile.
|
|
51
51
|
require 'journaled/rspec'
|
52
52
|
```
|
53
53
|
|
54
|
-
3. You will
|
54
|
+
3. You will need to set the following config in an initializer to allow Journaled to publish events to your AWS Kinesis event stream:
|
55
55
|
|
56
|
-
|
56
|
+
```ruby
|
57
|
+
Journaled.default_stream_name = "my_app_#{Rails.env}_events"
|
58
|
+
```
|
57
59
|
|
58
|
-
|
59
|
-
`#journaled_app_name` method to a non-nil value e.g. `my_app`, you will
|
60
|
-
instead need to provide a corresponding
|
61
|
-
`[upcased_app_name]_JOURNALED_STREAM_NAME` variable for each distinct
|
62
|
-
value, e.g. `MY_APP_JOURNALED_STREAM_NAME`. You can provide a default value
|
63
|
-
for all `Journaled::Event`s in an initializer like this:
|
60
|
+
You may also define a `#journaled_stream_name` method on `Journaled::Event` instances:
|
64
61
|
|
65
62
|
```ruby
|
66
|
-
|
67
|
-
|
63
|
+
def journaled_stream_name
|
64
|
+
"my_app_#{Rails.env}_alternate_events"
|
65
|
+
end
|
66
|
+
````
|
67
|
+
|
68
|
+
3. You may also need to define environment variables to allow Journaled to publish events to your AWS Kinesis event stream:
|
68
69
|
|
69
70
|
You may optionally define the following ENV vars to specify AWS
|
70
71
|
credentials outside of the locations that the AWS SDK normally looks:
|
@@ -82,6 +83,41 @@ app's Gemfile.
|
|
82
83
|
|
83
84
|
The AWS principal whose credentials are in the environment will need to be allowed to assume this role.
|
84
85
|
|
86
|
+
### Upgrading from 3.1.0
|
87
|
+
|
88
|
+
Versions of Journaled prior to 4.0 relied directly on environment variables for stream names, but now stream names are configured directly.
|
89
|
+
When upgrading, you can use the following configuration to maintain the previous behavior:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
Journaled.default_stream_name = ENV['JOURNALED_STREAM_NAME']
|
93
|
+
```
|
94
|
+
|
95
|
+
If you previously specified a `Journaled.default_app_name`, you would have required a more precise environment variable name (substitute `{{upcase_app_name}}`):
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
Journaled.default_stream_name = ENV["{{upcase_app_name}}_JOURNALED_STREAM_NAME"]
|
99
|
+
```
|
100
|
+
|
101
|
+
And if you had defined any `journaled_app_name` methods on `Journaled::Event` instances, you can replace them with the following:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
def journaled_stream_name
|
105
|
+
ENV['{{upcase_app_name}}_JOURNALED_STREAM_NAME']
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
When upgrading from 3.1 or below, `Journaled::DeliveryJob` will handle any jobs that remain in the queue by accepting an `app_name` argument. **This behavior will be removed in version 5.0**, so it is recommended to upgrade one major version at a time.
|
110
|
+
|
111
|
+
### Upgrading from 2.5.0
|
112
|
+
|
113
|
+
Versions of Journaled prior to 3.0 relied direclty on `delayed_job` and a "performable" class called `Journaled::Delivery`.
|
114
|
+
In 3.0, this was superceded by an ActiveJob class called `Journaled::DeliveryJob`, but the `Journaled::Delivery` class was not removed until 4.0.
|
115
|
+
|
116
|
+
Therefore, when upgrading from 2.5.0 or below, it is recommended to first upgrade to 3.1.0 (to allow any `Journaled::Delivery` jobs to finish working off) before upgrading to 4.0+.
|
117
|
+
|
118
|
+
The upgrade to 3.1.0 will require a working ActiveJob config. ActiveJob can be configured globally by setting `ActiveJob::Base.queue_adapter`, or just for Journaled jobs by setting `Journaled::DeliveryJob.queue_adapter`.
|
119
|
+
The `:delayed_job` queue adapter will allow you to continue relying on `delayed_job`. You may also consider switching your app(s) to [`delayed`](https://github.com/Betterment/delayed) and using the `:delayed` queue adapter.
|
120
|
+
|
85
121
|
## Usage
|
86
122
|
|
87
123
|
### Configuration
|
@@ -12,15 +12,24 @@ module Journaled
|
|
12
12
|
raise KinesisTemporaryFailure
|
13
13
|
end
|
14
14
|
|
15
|
-
|
15
|
+
UNSPECIFIED = Object.new
|
16
|
+
private_constant :UNSPECIFIED
|
17
|
+
|
18
|
+
def perform(serialized_event:, partition_key:, stream_name: UNSPECIFIED, app_name: UNSPECIFIED)
|
16
19
|
@serialized_event = serialized_event
|
17
20
|
@partition_key = partition_key
|
18
|
-
|
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
|
19
28
|
|
20
29
|
journal!
|
21
30
|
end
|
22
31
|
|
23
|
-
def self.
|
32
|
+
def self.legacy_computed_stream_name(app_name:)
|
24
33
|
env_var_name = [app_name&.upcase, 'JOURNALED_STREAM_NAME'].compact.join('_')
|
25
34
|
ENV.fetch(env_var_name)
|
26
35
|
end
|
@@ -37,7 +46,7 @@ module Journaled
|
|
37
46
|
|
38
47
|
private
|
39
48
|
|
40
|
-
attr_reader :serialized_event, :partition_key, :
|
49
|
+
attr_reader :serialized_event, :partition_key, :stream_name
|
41
50
|
|
42
51
|
def journal!
|
43
52
|
kinesis_client.put_record record if Journaled.enabled?
|
@@ -45,7 +54,7 @@ module Journaled
|
|
45
54
|
|
46
55
|
def record
|
47
56
|
{
|
48
|
-
stream_name:
|
57
|
+
stream_name: stream_name,
|
49
58
|
data: serialized_event,
|
50
59
|
partition_key: partition_key,
|
51
60
|
}
|
@@ -6,7 +6,7 @@ class Journaled::Change
|
|
6
6
|
:database_operation,
|
7
7
|
:logical_operation,
|
8
8
|
:changes,
|
9
|
-
:
|
9
|
+
:journaled_stream_name,
|
10
10
|
:journaled_enqueue_opts,
|
11
11
|
:actor
|
12
12
|
|
@@ -22,7 +22,7 @@ class Journaled::Change
|
|
22
22
|
database_operation:,
|
23
23
|
logical_operation:,
|
24
24
|
changes:,
|
25
|
-
|
25
|
+
journaled_stream_name:,
|
26
26
|
journaled_enqueue_opts:,
|
27
27
|
actor:)
|
28
28
|
@table_name = table_name
|
@@ -30,7 +30,7 @@ class Journaled::Change
|
|
30
30
|
@database_operation = database_operation
|
31
31
|
@logical_operation = logical_operation
|
32
32
|
@changes = changes
|
33
|
-
@
|
33
|
+
@journaled_stream_name = journaled_stream_name
|
34
34
|
@journaled_enqueue_opts = journaled_enqueue_opts
|
35
35
|
@actor = actor
|
36
36
|
end
|
@@ -27,7 +27,7 @@ class Journaled::ChangeWriter
|
|
27
27
|
database_operation: database_operation,
|
28
28
|
logical_operation: logical_operation,
|
29
29
|
changes: JSON.dump(changes),
|
30
|
-
|
30
|
+
journaled_stream_name: journaled_stream_name,
|
31
31
|
journaled_enqueue_opts: model.journaled_enqueue_opts,
|
32
32
|
actor: actor_uri,
|
33
33
|
)
|
@@ -57,11 +57,11 @@ class Journaled::ChangeWriter
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
-
def
|
61
|
-
if model.class.respond_to?(:
|
62
|
-
model.class.
|
60
|
+
def journaled_stream_name
|
61
|
+
if model.class.respond_to?(:journaled_stream_name)
|
62
|
+
model.class.journaled_stream_name
|
63
63
|
else
|
64
|
-
Journaled.
|
64
|
+
Journaled.default_stream_name
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
@@ -3,7 +3,7 @@ class Journaled::Writer
|
|
3
3
|
journaled_schema_name
|
4
4
|
journaled_partition_key
|
5
5
|
journaled_attributes
|
6
|
-
|
6
|
+
journaled_stream_name
|
7
7
|
journaled_enqueue_opts
|
8
8
|
).freeze
|
9
9
|
|
@@ -41,7 +41,7 @@ class Journaled::Writer
|
|
41
41
|
{
|
42
42
|
serialized_event: serialized_event,
|
43
43
|
partition_key: journaled_partition_key,
|
44
|
-
|
44
|
+
stream_name: journaled_stream_name,
|
45
45
|
}
|
46
46
|
end
|
47
47
|
|
data/lib/journaled/version.rb
CHANGED
data/lib/journaled.rb
CHANGED
@@ -8,7 +8,7 @@ require "journaled/engine"
|
|
8
8
|
module Journaled
|
9
9
|
SUPPORTED_QUEUE_ADAPTERS = %w(delayed delayed_job good_job que).freeze
|
10
10
|
|
11
|
-
mattr_accessor :
|
11
|
+
mattr_accessor :default_stream_name
|
12
12
|
mattr_accessor(:job_priority) { 20 }
|
13
13
|
mattr_accessor(:http_idle_timeout) { 5 }
|
14
14
|
mattr_accessor(:http_open_timeout) { 2 }
|
@@ -5,11 +5,7 @@ RSpec.describe Journaled::DeliveryJob do
|
|
5
5
|
let(:partition_key) { 'fake_partition_key' }
|
6
6
|
let(:serialized_event) { '{"foo":"bar"}' }
|
7
7
|
let(:kinesis_client) { Aws::Kinesis::Client.new(stub_responses: true) }
|
8
|
-
let(:args) { { serialized_event: serialized_event, partition_key: partition_key,
|
9
|
-
|
10
|
-
around do |example|
|
11
|
-
with_env(JOURNALED_STREAM_NAME: stream_name) { example.run }
|
12
|
-
end
|
8
|
+
let(:args) { { serialized_event: serialized_event, partition_key: partition_key, stream_name: stream_name } }
|
13
9
|
|
14
10
|
describe '#perform' do
|
15
11
|
let(:return_status_body) { { shard_id: '101', sequence_number: '101123' } }
|
@@ -19,6 +15,7 @@ RSpec.describe Journaled::DeliveryJob do
|
|
19
15
|
allow(Aws::AssumeRoleCredentials).to receive(:new).and_call_original
|
20
16
|
allow(Aws::Kinesis::Client).to receive(:new).and_return kinesis_client
|
21
17
|
kinesis_client.stub_responses(:put_record, return_status_body)
|
18
|
+
allow(kinesis_client).to receive(:put_record).and_call_original
|
22
19
|
|
23
20
|
allow(Journaled).to receive(:enabled?).and_return(true)
|
24
21
|
end
|
@@ -28,6 +25,11 @@ RSpec.describe Journaled::DeliveryJob do
|
|
28
25
|
|
29
26
|
expect(event.shard_id).to eq '101'
|
30
27
|
expect(event.sequence_number).to eq '101123'
|
28
|
+
expect(kinesis_client).to have_received(:put_record).with(
|
29
|
+
stream_name: 'test_events',
|
30
|
+
data: '{"foo":"bar"}',
|
31
|
+
partition_key: 'fake_partition_key',
|
32
|
+
)
|
31
33
|
end
|
32
34
|
|
33
35
|
context 'when JOURNALED_IAM_ROLE_ARN is defined' do
|
@@ -68,11 +70,64 @@ RSpec.describe Journaled::DeliveryJob do
|
|
68
70
|
end
|
69
71
|
end
|
70
72
|
|
71
|
-
context 'when the stream name
|
73
|
+
context 'when the stream name is not set' do
|
72
74
|
let(:stream_name) { nil }
|
73
75
|
|
74
76
|
it 'raises an KeyError error' do
|
75
|
-
expect { described_class.perform_now(args) }.to raise_error
|
77
|
+
expect { described_class.perform_now(args) }.to raise_error ArgumentError, 'missing keyword: stream_name'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
unless Gem::Version.new(Journaled::VERSION) < Gem::Version.new('5.0.0')
|
82
|
+
raise <<~MSG
|
83
|
+
Hey! I see that you're bumping the version to 5.0!
|
84
|
+
|
85
|
+
This is a reminder to:
|
86
|
+
- remove the `app_name` argument (and related logic) from `Journaled::DeliveryJob`,
|
87
|
+
- remove the following app_name test contexts, and
|
88
|
+
- make `stream_name` a required kwarg
|
89
|
+
|
90
|
+
Thanks!
|
91
|
+
MSG
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'when the legacy app_name argument is present but nil' do
|
95
|
+
let(:args) { { serialized_event: serialized_event, partition_key: partition_key, app_name: nil } }
|
96
|
+
|
97
|
+
around do |example|
|
98
|
+
with_env(JOURNALED_STREAM_NAME: 'legacy_stream_name') { example.run }
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'makes requests to AWS to put the event on the Kinesis with the correct body' do
|
102
|
+
event = described_class.perform_now(args)
|
103
|
+
|
104
|
+
expect(event.shard_id).to eq '101'
|
105
|
+
expect(event.sequence_number).to eq '101123'
|
106
|
+
expect(kinesis_client).to have_received(:put_record).with(
|
107
|
+
stream_name: 'legacy_stream_name',
|
108
|
+
data: '{"foo":"bar"}',
|
109
|
+
partition_key: 'fake_partition_key',
|
110
|
+
)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'when the legacy app_name argument is present and has a value' do
|
115
|
+
let(:args) { { serialized_event: serialized_event, partition_key: partition_key, app_name: 'pied_piper' } }
|
116
|
+
|
117
|
+
around do |example|
|
118
|
+
with_env(PIED_PIPER_JOURNALED_STREAM_NAME: 'pied_piper_events') { example.run }
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'makes requests to AWS to put the event on the Kinesis with the correct body' do
|
122
|
+
event = described_class.perform_now(args)
|
123
|
+
|
124
|
+
expect(event.shard_id).to eq '101'
|
125
|
+
expect(event.sequence_number).to eq '101123'
|
126
|
+
expect(kinesis_client).to have_received(:put_record).with(
|
127
|
+
stream_name: 'pied_piper_events',
|
128
|
+
data: '{"foo":"bar"}',
|
129
|
+
partition_key: 'fake_partition_key',
|
130
|
+
)
|
76
131
|
end
|
77
132
|
end
|
78
133
|
|
@@ -137,11 +192,11 @@ RSpec.describe Journaled::DeliveryJob do
|
|
137
192
|
end
|
138
193
|
end
|
139
194
|
|
140
|
-
describe ".
|
195
|
+
describe ".legacy_computed_stream_name" do
|
141
196
|
context "when app_name is unspecified" do
|
142
197
|
it "is fetched from a prefixed ENV var if specified" do
|
143
198
|
allow(ENV).to receive(:fetch).and_return("expected_stream_name")
|
144
|
-
expect(described_class.
|
199
|
+
expect(described_class.legacy_computed_stream_name(app_name: nil)).to eq("expected_stream_name")
|
145
200
|
expect(ENV).to have_received(:fetch).with("JOURNALED_STREAM_NAME")
|
146
201
|
end
|
147
202
|
end
|
@@ -149,7 +204,7 @@ RSpec.describe Journaled::DeliveryJob do
|
|
149
204
|
context "when app_name is specified" do
|
150
205
|
it "is fetched from a prefixed ENV var if specified" do
|
151
206
|
allow(ENV).to receive(:fetch).and_return("expected_stream_name")
|
152
|
-
expect(described_class.
|
207
|
+
expect(described_class.legacy_computed_stream_name(app_name: "my_funky_app_name")).to eq("expected_stream_name")
|
153
208
|
expect(ENV).to have_received(:fetch).with("MY_FUNKY_APP_NAME_JOURNALED_STREAM_NAME")
|
154
209
|
end
|
155
210
|
end
|
@@ -138,30 +138,30 @@ RSpec.describe Journaled::ChangeWriter do
|
|
138
138
|
expect(subject.journaled_change_for("update", {}).logical_operation).to eq("identity_change")
|
139
139
|
end
|
140
140
|
|
141
|
-
it "doesn't set
|
142
|
-
expect(subject.journaled_change_for("update", {}).
|
141
|
+
it "doesn't set journaled_stream_name if model class doesn't respond to it" do
|
142
|
+
expect(subject.journaled_change_for("update", {}).journaled_stream_name).to eq(nil)
|
143
143
|
end
|
144
144
|
|
145
145
|
context "with journaled default app name set" do
|
146
146
|
around do |example|
|
147
|
-
orig_app_name = Journaled.
|
148
|
-
Journaled.
|
147
|
+
orig_app_name = Journaled.default_stream_name
|
148
|
+
Journaled.default_stream_name = "foo"
|
149
149
|
example.run
|
150
|
-
Journaled.
|
150
|
+
Journaled.default_stream_name = orig_app_name
|
151
151
|
end
|
152
152
|
|
153
153
|
it "passes through default" do
|
154
|
-
expect(subject.journaled_change_for("update", {}).
|
154
|
+
expect(subject.journaled_change_for("update", {}).journaled_stream_name).to eq("foo")
|
155
155
|
end
|
156
156
|
end
|
157
157
|
|
158
|
-
context "when model class defines
|
158
|
+
context "when model class defines journaled_stream_name" do
|
159
159
|
before do
|
160
|
-
allow(model_class).to receive(:
|
160
|
+
allow(model_class).to receive(:journaled_stream_name).and_return("my_app_events")
|
161
161
|
end
|
162
162
|
|
163
|
-
it "sets
|
164
|
-
expect(subject.journaled_change_for("update", {}).
|
163
|
+
it "sets journaled_stream_name if model_class responds to it" do
|
164
|
+
expect(subject.journaled_change_for("update", {}).journaled_stream_name).to eq("my_app_events")
|
165
165
|
end
|
166
166
|
end
|
167
167
|
end
|
@@ -65,14 +65,14 @@ RSpec.describe Journaled::Event do
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
describe '#
|
68
|
+
describe '#journaled_stream_name' do
|
69
69
|
it 'returns nil in the base class so it can be set explicitly in apps spanning multiple app domains' do
|
70
|
-
expect(sample_journaled_event.
|
70
|
+
expect(sample_journaled_event.journaled_stream_name).to be_nil
|
71
71
|
end
|
72
72
|
|
73
73
|
it 'returns the journaled default if set' do
|
74
|
-
allow(Journaled).to receive(:
|
75
|
-
expect(sample_journaled_event.
|
74
|
+
allow(Journaled).to receive(:default_stream_name).and_return("my_app_events")
|
75
|
+
expect(sample_journaled_event.journaled_stream_name).to eq("my_app_events")
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
@@ -18,7 +18,7 @@ RSpec.describe Journaled::Writer do
|
|
18
18
|
journaled_schema_name: nil,
|
19
19
|
journaled_attributes: {},
|
20
20
|
journaled_partition_key: '',
|
21
|
-
|
21
|
+
journaled_stream_name: nil,
|
22
22
|
journaled_enqueue_opts: {},
|
23
23
|
)
|
24
24
|
end
|
@@ -34,7 +34,7 @@ RSpec.describe Journaled::Writer do
|
|
34
34
|
journaled_schema_name: :fake_schema_name,
|
35
35
|
journaled_attributes: { foo: :bar },
|
36
36
|
journaled_partition_key: 'fake_partition_key',
|
37
|
-
|
37
|
+
journaled_stream_name: nil,
|
38
38
|
journaled_enqueue_opts: {},
|
39
39
|
)
|
40
40
|
end
|
@@ -75,7 +75,7 @@ RSpec.describe Journaled::Writer do
|
|
75
75
|
journaled_schema_name: :fake_schema_name,
|
76
76
|
journaled_attributes: journaled_event_attributes,
|
77
77
|
journaled_partition_key: 'fake_partition_key',
|
78
|
-
|
78
|
+
journaled_stream_name: 'my_app_events',
|
79
79
|
journaled_enqueue_opts: journaled_enqueue_opts,
|
80
80
|
)
|
81
81
|
end
|
@@ -104,7 +104,7 @@ RSpec.describe Journaled::Writer do
|
|
104
104
|
|
105
105
|
it 'creates a delivery with the app name passed through' do
|
106
106
|
expect { subject.journal! }.to change { enqueued_jobs.count }.from(0).to(1)
|
107
|
-
expect(enqueued_jobs.first[:args].first).to include('
|
107
|
+
expect(enqueued_jobs.first[:args].first).to include('stream_name' => 'my_app_events')
|
108
108
|
end
|
109
109
|
|
110
110
|
context 'when there is no job priority specified in the enqueue opts' do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: journaled
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jake Lipson
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2021-
|
14
|
+
date: 2021-10-27 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activejob
|
@@ -245,7 +245,6 @@ files:
|
|
245
245
|
- app/models/journaled/change.rb
|
246
246
|
- app/models/journaled/change_definition.rb
|
247
247
|
- app/models/journaled/change_writer.rb
|
248
|
-
- app/models/journaled/delivery.rb
|
249
248
|
- app/models/journaled/event.rb
|
250
249
|
- app/models/journaled/json_schema_model/validator.rb
|
251
250
|
- app/models/journaled/not_truly_exceptional_error.rb
|
@@ -293,7 +292,6 @@ files:
|
|
293
292
|
- spec/models/database_change_protection_spec.rb
|
294
293
|
- spec/models/journaled/actor_uri_provider_spec.rb
|
295
294
|
- spec/models/journaled/change_writer_spec.rb
|
296
|
-
- spec/models/journaled/delivery_spec.rb
|
297
295
|
- spec/models/journaled/event_spec.rb
|
298
296
|
- spec/models/journaled/json_schema_model/validator_spec.rb
|
299
297
|
- spec/models/journaled/writer_spec.rb
|
@@ -319,7 +317,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
319
317
|
- !ruby/object:Gem::Version
|
320
318
|
version: '0'
|
321
319
|
requirements: []
|
322
|
-
rubygems_version: 3.1
|
320
|
+
rubygems_version: 3.0.1
|
323
321
|
signing_key:
|
324
322
|
specification_version: 4
|
325
323
|
summary: Journaling for Betterment apps.
|
@@ -354,7 +352,6 @@ test_files:
|
|
354
352
|
- spec/dummy/README.rdoc
|
355
353
|
- spec/models/journaled/json_schema_model/validator_spec.rb
|
356
354
|
- spec/models/journaled/actor_uri_provider_spec.rb
|
357
|
-
- spec/models/journaled/delivery_spec.rb
|
358
355
|
- spec/models/journaled/event_spec.rb
|
359
356
|
- spec/models/journaled/change_writer_spec.rb
|
360
357
|
- spec/models/journaled/writer_spec.rb
|
@@ -1,88 +0,0 @@
|
|
1
|
-
class Journaled::Delivery # rubocop:disable Betterment/ActiveJobPerformable
|
2
|
-
DEFAULT_REGION = 'us-east-1'.freeze
|
3
|
-
|
4
|
-
def initialize(serialized_event:, partition_key:, app_name:)
|
5
|
-
@serialized_event = serialized_event
|
6
|
-
@partition_key = partition_key
|
7
|
-
@app_name = app_name
|
8
|
-
end
|
9
|
-
|
10
|
-
def perform
|
11
|
-
kinesis_client.put_record record if Journaled.enabled?
|
12
|
-
rescue Aws::Kinesis::Errors::InternalFailure, Aws::Kinesis::Errors::ServiceUnavailable, Aws::Kinesis::Errors::Http503Error => e
|
13
|
-
Rails.logger.error "Kinesis Error - Server Error occurred - #{e.class}"
|
14
|
-
raise KinesisTemporaryFailure
|
15
|
-
rescue Seahorse::Client::NetworkingError => e
|
16
|
-
Rails.logger.error "Kinesis Error - Networking Error occurred - #{e.class}"
|
17
|
-
raise KinesisTemporaryFailure
|
18
|
-
end
|
19
|
-
|
20
|
-
def stream_name
|
21
|
-
env_var_name = [app_name&.upcase, 'JOURNALED_STREAM_NAME'].compact.join('_')
|
22
|
-
ENV.fetch(env_var_name)
|
23
|
-
end
|
24
|
-
|
25
|
-
def kinesis_client_config
|
26
|
-
{
|
27
|
-
region: ENV.fetch('AWS_DEFAULT_REGION', DEFAULT_REGION),
|
28
|
-
retry_limit: 0,
|
29
|
-
http_idle_timeout: Journaled.http_idle_timeout,
|
30
|
-
http_open_timeout: Journaled.http_open_timeout,
|
31
|
-
http_read_timeout: Journaled.http_read_timeout,
|
32
|
-
}.merge(credentials)
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
attr_reader :serialized_event, :partition_key, :app_name
|
38
|
-
|
39
|
-
def record
|
40
|
-
{
|
41
|
-
stream_name: stream_name,
|
42
|
-
data: serialized_event,
|
43
|
-
partition_key: partition_key,
|
44
|
-
}
|
45
|
-
end
|
46
|
-
|
47
|
-
def kinesis_client
|
48
|
-
Aws::Kinesis::Client.new(kinesis_client_config)
|
49
|
-
end
|
50
|
-
|
51
|
-
def credentials
|
52
|
-
if ENV.key?('JOURNALED_IAM_ROLE_ARN')
|
53
|
-
{
|
54
|
-
credentials: iam_assume_role_credentials,
|
55
|
-
}
|
56
|
-
else
|
57
|
-
legacy_credentials_hash_if_present
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def legacy_credentials_hash_if_present
|
62
|
-
if ENV.key?('RUBY_AWS_ACCESS_KEY_ID')
|
63
|
-
{
|
64
|
-
access_key_id: ENV.fetch('RUBY_AWS_ACCESS_KEY_ID'),
|
65
|
-
secret_access_key: ENV.fetch('RUBY_AWS_SECRET_ACCESS_KEY'),
|
66
|
-
}
|
67
|
-
else
|
68
|
-
{}
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def sts_client
|
73
|
-
Aws::STS::Client.new({
|
74
|
-
region: ENV.fetch('AWS_DEFAULT_REGION', DEFAULT_REGION),
|
75
|
-
}.merge(legacy_credentials_hash_if_present))
|
76
|
-
end
|
77
|
-
|
78
|
-
def iam_assume_role_credentials
|
79
|
-
@iam_assume_role_credentials ||= Aws::AssumeRoleCredentials.new(
|
80
|
-
client: sts_client,
|
81
|
-
role_arn: ENV.fetch('JOURNALED_IAM_ROLE_ARN'),
|
82
|
-
role_session_name: "JournaledAssumeRoleAccess",
|
83
|
-
)
|
84
|
-
end
|
85
|
-
|
86
|
-
class KinesisTemporaryFailure < ::Journaled::NotTrulyExceptionalError
|
87
|
-
end
|
88
|
-
end
|
@@ -1,222 +0,0 @@
|
|
1
|
-
require 'rails_helper'
|
2
|
-
|
3
|
-
RSpec.describe Journaled::Delivery do
|
4
|
-
let(:stream_name) { 'test_events' }
|
5
|
-
let(:partition_key) { 'fake_partition_key' }
|
6
|
-
let(:serialized_event) { '{"foo":"bar"}' }
|
7
|
-
let(:kinesis_client) { Aws::Kinesis::Client.new(stub_responses: true) }
|
8
|
-
|
9
|
-
around do |example|
|
10
|
-
with_env(JOURNALED_STREAM_NAME: stream_name) { example.run }
|
11
|
-
end
|
12
|
-
|
13
|
-
subject { described_class.new serialized_event: serialized_event, partition_key: partition_key, app_name: nil }
|
14
|
-
|
15
|
-
describe '#perform' do
|
16
|
-
let(:return_status_body) { { shard_id: '101', sequence_number: '101123' } }
|
17
|
-
let(:return_object) { instance_double Aws::Kinesis::Types::PutRecordOutput, return_status_body }
|
18
|
-
|
19
|
-
before do
|
20
|
-
allow(Aws::AssumeRoleCredentials).to receive(:new).and_call_original
|
21
|
-
allow(Aws::Kinesis::Client).to receive(:new).and_return kinesis_client
|
22
|
-
kinesis_client.stub_responses(:put_record, return_status_body)
|
23
|
-
|
24
|
-
allow(Journaled).to receive(:enabled?).and_return(true)
|
25
|
-
end
|
26
|
-
|
27
|
-
it 'makes requests to AWS to put the event on the Kinesis with the correct body' do
|
28
|
-
event = subject.perform
|
29
|
-
|
30
|
-
expect(event.shard_id).to eq '101'
|
31
|
-
expect(event.sequence_number).to eq '101123'
|
32
|
-
end
|
33
|
-
|
34
|
-
context 'when JOURNALED_IAM_ROLE_ARN is defined' do
|
35
|
-
let(:aws_sts_client) { Aws::STS::Client.new(stub_responses: true) }
|
36
|
-
|
37
|
-
around do |example|
|
38
|
-
with_env(JOURNALED_IAM_ROLE_ARN: 'iam-role-arn-for-assuming-kinesis-access') { example.run }
|
39
|
-
end
|
40
|
-
|
41
|
-
before do
|
42
|
-
allow(Aws::STS::Client).to receive(:new).and_return aws_sts_client
|
43
|
-
aws_sts_client.stub_responses(:assume_role, assume_role_response)
|
44
|
-
end
|
45
|
-
|
46
|
-
let(:assume_role_response) do
|
47
|
-
{
|
48
|
-
assumed_role_user: {
|
49
|
-
arn: 'iam-role-arn-for-assuming-kinesis-access',
|
50
|
-
assumed_role_id: "ARO123EXAMPLE123:Bob",
|
51
|
-
},
|
52
|
-
credentials: {
|
53
|
-
access_key_id: "AKIAIOSFODNN7EXAMPLE",
|
54
|
-
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY",
|
55
|
-
session_token: "EXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI",
|
56
|
-
expiration: Time.zone.parse("2011-07-15T23:28:33.359Z"),
|
57
|
-
},
|
58
|
-
}
|
59
|
-
end
|
60
|
-
|
61
|
-
it 'initializes a Kinesis client with assume role credentials' do
|
62
|
-
subject.perform
|
63
|
-
|
64
|
-
expect(Aws::AssumeRoleCredentials).to have_received(:new).with(
|
65
|
-
client: aws_sts_client,
|
66
|
-
role_arn: "iam-role-arn-for-assuming-kinesis-access",
|
67
|
-
role_session_name: "JournaledAssumeRoleAccess",
|
68
|
-
)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
context 'when the stream name env var is NOT set' do
|
73
|
-
let(:stream_name) { nil }
|
74
|
-
|
75
|
-
it 'raises an KeyError error' do
|
76
|
-
expect { subject.perform }.to raise_error KeyError
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
context 'when Amazon responds with an InternalFailure' do
|
81
|
-
before do
|
82
|
-
kinesis_client.stub_responses(:put_record, 'InternalFailure')
|
83
|
-
end
|
84
|
-
|
85
|
-
it 'catches the error and re-raises a subclass of NotTrulyExceptionalError and logs about the failure' do
|
86
|
-
expect(Rails.logger).to receive(:error).with("Kinesis Error - Server Error occurred - Aws::Kinesis::Errors::InternalFailure").once
|
87
|
-
expect { subject.perform }.to raise_error described_class::KinesisTemporaryFailure
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
context 'when Amazon responds with a ServiceUnavailable' do
|
92
|
-
before do
|
93
|
-
kinesis_client.stub_responses(:put_record, 'ServiceUnavailable')
|
94
|
-
end
|
95
|
-
|
96
|
-
it 'catches the error and re-raises a subclass of NotTrulyExceptionalError and logs about the failure' do
|
97
|
-
allow(Rails.logger).to receive(:error)
|
98
|
-
expect { subject.perform }.to raise_error described_class::KinesisTemporaryFailure
|
99
|
-
expect(Rails.logger).to have_received(:error).with(/\AKinesis Error/).once
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
context 'when we receive a 504 Gateway timeout' do
|
104
|
-
before do
|
105
|
-
kinesis_client.stub_responses(:put_record, 'Aws::Kinesis::Errors::ServiceError')
|
106
|
-
end
|
107
|
-
|
108
|
-
it 'raises an error that subclasses Aws::Kinesis::Errors::ServiceError' do
|
109
|
-
expect { subject.perform }.to raise_error Aws::Kinesis::Errors::ServiceError
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
context 'when the IAM user does not have permission to put_record to the specified stream' do
|
114
|
-
before do
|
115
|
-
kinesis_client.stub_responses(:put_record, 'AccessDeniedException')
|
116
|
-
end
|
117
|
-
|
118
|
-
it 'raises an AccessDeniedException error' do
|
119
|
-
expect { subject.perform }.to raise_error Aws::Kinesis::Errors::AccessDeniedException
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
context 'when the request timesout' do
|
124
|
-
before do
|
125
|
-
kinesis_client.stub_responses(:put_record, Seahorse::Client::NetworkingError.new(Timeout::Error.new))
|
126
|
-
end
|
127
|
-
|
128
|
-
it 'catches the error and re-raises a subclass of NotTrulyExceptionalError and logs about the failure' do
|
129
|
-
expect(Rails.logger).to receive(:error).with(
|
130
|
-
"Kinesis Error - Networking Error occurred - Seahorse::Client::NetworkingError",
|
131
|
-
).once
|
132
|
-
expect { subject.perform }.to raise_error described_class::KinesisTemporaryFailure
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
describe "#stream_name" do
|
138
|
-
context "when app_name is unspecified" do
|
139
|
-
subject { described_class.new serialized_event: serialized_event, partition_key: partition_key, app_name: nil }
|
140
|
-
|
141
|
-
it "is fetched from a prefixed ENV var if specified" do
|
142
|
-
allow(ENV).to receive(:fetch).and_return("expected_stream_name")
|
143
|
-
expect(subject.stream_name).to eq("expected_stream_name")
|
144
|
-
expect(ENV).to have_received(:fetch).with("JOURNALED_STREAM_NAME")
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
context "when app_name is specified" do
|
149
|
-
subject { described_class.new serialized_event: serialized_event, partition_key: partition_key, app_name: "my_funky_app_name" }
|
150
|
-
|
151
|
-
it "is fetched from a prefixed ENV var if specified" do
|
152
|
-
allow(ENV).to receive(:fetch).and_return("expected_stream_name")
|
153
|
-
expect(subject.stream_name).to eq("expected_stream_name")
|
154
|
-
expect(ENV).to have_received(:fetch).with("MY_FUNKY_APP_NAME_JOURNALED_STREAM_NAME")
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
describe "#kinesis_client_config" do
|
160
|
-
it "is in us-east-1 by default" do
|
161
|
-
with_env(AWS_DEFAULT_REGION: nil) do
|
162
|
-
expect(subject.kinesis_client_config).to include(region: 'us-east-1')
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
it "respects AWS_DEFAULT_REGION env var" do
|
167
|
-
with_env(AWS_DEFAULT_REGION: 'us-west-2') do
|
168
|
-
expect(subject.kinesis_client_config).to include(region: 'us-west-2')
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
it "doesn't limit retry" do
|
173
|
-
expect(subject.kinesis_client_config).to include(retry_limit: 0)
|
174
|
-
end
|
175
|
-
|
176
|
-
it "provides no AWS credentials by default" do
|
177
|
-
with_env(RUBY_AWS_ACCESS_KEY_ID: nil, RUBY_AWS_SECRET_ACCESS_KEY: nil) do
|
178
|
-
expect(subject.kinesis_client_config).not_to have_key(:access_key_id)
|
179
|
-
expect(subject.kinesis_client_config).not_to have_key(:secret_access_key)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
it "will use legacy credentials if specified" do
|
184
|
-
with_env(RUBY_AWS_ACCESS_KEY_ID: 'key_id', RUBY_AWS_SECRET_ACCESS_KEY: 'secret') do
|
185
|
-
expect(subject.kinesis_client_config).to include(access_key_id: 'key_id', secret_access_key: 'secret')
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
it "will set http_idle_timeout by default" do
|
190
|
-
expect(subject.kinesis_client_config).to include(http_idle_timeout: 5)
|
191
|
-
end
|
192
|
-
|
193
|
-
it "will set http_open_timeout by default" do
|
194
|
-
expect(subject.kinesis_client_config).to include(http_open_timeout: 2)
|
195
|
-
end
|
196
|
-
|
197
|
-
it "will set http_read_timeout by default" do
|
198
|
-
expect(subject.kinesis_client_config).to include(http_read_timeout: 60)
|
199
|
-
end
|
200
|
-
|
201
|
-
context "when Journaled.http_idle_timeout is specified" do
|
202
|
-
it "will set http_idle_timeout by specified value" do
|
203
|
-
allow(Journaled).to receive(:http_idle_timeout).and_return(2)
|
204
|
-
expect(subject.kinesis_client_config).to include(http_idle_timeout: 2)
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
context "when Journaled.http_open_timeout is specified" do
|
209
|
-
it "will set http_open_timeout by specified value" do
|
210
|
-
allow(Journaled).to receive(:http_open_timeout).and_return(1)
|
211
|
-
expect(subject.kinesis_client_config).to include(http_open_timeout: 1)
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
context "when Journaled.http_read_timeout is specified" do
|
216
|
-
it "will set http_read_timeout by specified value" do
|
217
|
-
allow(Journaled).to receive(:http_read_timeout).and_return(2)
|
218
|
-
expect(subject.kinesis_client_config).to include(http_read_timeout: 2)
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|