journaled 2.2.0 → 3.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +42 -6
  3. data/Rakefile +7 -1
  4. data/app/jobs/journaled/application_job.rb +4 -0
  5. data/app/jobs/journaled/delivery_job.rb +96 -0
  6. data/app/models/concerns/journaled/changes.rb +5 -5
  7. data/app/models/journaled/change.rb +3 -0
  8. data/app/models/journaled/change_writer.rb +1 -0
  9. data/app/models/journaled/delivery.rb +5 -2
  10. data/app/models/journaled/event.rb +5 -2
  11. data/app/models/journaled/writer.rb +10 -8
  12. data/lib/journaled.rb +26 -5
  13. data/lib/journaled/engine.rb +5 -0
  14. data/lib/journaled/relation_change_protection.rb +1 -1
  15. data/lib/journaled/version.rb +1 -1
  16. data/spec/dummy/config/application.rb +1 -2
  17. data/spec/dummy/config/database.yml +4 -19
  18. data/spec/dummy/config/environments/development.rb +0 -13
  19. data/spec/dummy/config/environments/test.rb +3 -5
  20. data/spec/dummy/db/schema.rb +3 -16
  21. data/spec/jobs/journaled/delivery_job_spec.rb +221 -0
  22. data/spec/lib/journaled_spec.rb +39 -0
  23. data/spec/models/concerns/journaled/changes_spec.rb +11 -0
  24. data/spec/models/database_change_protection_spec.rb +19 -25
  25. data/spec/models/journaled/change_writer_spec.rb +5 -0
  26. data/spec/models/journaled/delivery_spec.rb +33 -0
  27. data/spec/models/journaled/event_spec.rb +23 -16
  28. data/spec/models/journaled/writer_spec.rb +34 -18
  29. data/spec/rails_helper.rb +1 -2
  30. data/spec/spec_helper.rb +1 -3
  31. metadata +38 -62
  32. data/config/routes.rb +0 -2
  33. data/lib/journaled/job_priority.rb +0 -5
  34. data/spec/dummy/config/environments/production.rb +0 -78
  35. data/spec/dummy/config/initializers/assets.rb +0 -8
  36. data/spec/dummy/db/migrate/20180606205114_create_delayed_jobs.rb +0 -18
  37. data/spec/dummy/log/development.log +0 -29
  38. data/spec/dummy/log/test.log +0 -2482
  39. data/spec/support/delayed_job_spec_helper.rb +0 -11
@@ -1,10 +1,9 @@
1
1
  require File.expand_path('boot', __dir__)
2
2
 
3
3
  require "active_record/railtie"
4
+ require "active_job/railtie"
4
5
  require "active_model/railtie"
5
6
  require "action_controller/railtie"
6
- require "action_mailer/railtie"
7
- require "sprockets/railtie"
8
7
 
9
8
  Bundler.require(*Rails.groups)
10
9
  require "journaled"
@@ -1,21 +1,6 @@
1
- default: &default
2
- pool: 5
3
- encoding: unicode
4
-
5
- postgresql:
6
- default: &postgres_default
7
- adapter: postgresql
8
- url: <%= ENV['DATABASE_URL'] %>
9
- test: &postgres_test
10
- <<: *postgres_default
11
- url: <%= ENV['DATABASE_URL'] || "postgresql://localhost:#{ENV.fetch('PGPORT', 5432)}/journaled_test" %>
12
- database: journaled_test
13
- development: &postgres_development
14
- <<: *postgres_default
15
- url: <%= ENV['DATABASE_URL'] || "postgresql://localhost:#{ENV.fetch('PGPORT', 5432)}/journaled_development" %>
16
- database: journaled_development
17
-
18
1
  development:
19
- <<: *postgres_development
2
+ adapter: sqlite3
3
+ database: ":memory:"
20
4
  test:
21
- <<: *postgres_test
5
+ adapter: sqlite3
6
+ database: ":memory:"
@@ -13,25 +13,12 @@ Rails.application.configure do
13
13
  config.consider_all_requests_local = true
14
14
  config.action_controller.perform_caching = false
15
15
 
16
- # Don't care if the mailer can't send.
17
- config.action_mailer.raise_delivery_errors = false
18
-
19
16
  # Print deprecation notices to the Rails logger.
20
17
  config.active_support.deprecation = :log
21
18
 
22
19
  # Raise an error on page load if there are pending migrations.
23
20
  config.active_record.migration_error = :page_load
24
21
 
25
- # Debug mode disables concatenation and preprocessing of assets.
26
- # This option may cause significant delays in view rendering with a large
27
- # number of complex assets.
28
- config.assets.debug = true
29
-
30
- # Adds additional error checking when serving assets at runtime.
31
- # Checks for improperly declared sprockets dependencies.
32
- # Raises helpful error messages.
33
- config.assets.raise_runtime_errors = true
34
-
35
22
  # Raises error for missing translations
36
23
  # config.action_view.raise_on_missing_translations = true
37
24
  end
@@ -26,14 +26,12 @@ Rails.application.configure do
26
26
  # Disable request forgery protection in test environment.
27
27
  config.action_controller.allow_forgery_protection = false
28
28
 
29
- # Tell Action Mailer not to deliver emails to the real world.
30
- # The :test delivery method accumulates sent emails in the
31
- # ActionMailer::Base.deliveries array.
32
- config.action_mailer.delivery_method = :test
33
-
34
29
  # Print deprecation notices to the stderr.
35
30
  config.active_support.deprecation = :stderr
36
31
 
37
32
  # Raises error for missing translations
38
33
  # config.action_view.raise_on_missing_translations = true
34
+
35
+ # Use ActiveJob test adapter
36
+ config.active_job.queue_adapter = :test
39
37
  end
@@ -11,21 +11,8 @@
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
13
  ActiveRecord::Schema.define(version: 20180606205114) do
14
- # These are extensions that must be enabled in order to support this database
15
- enable_extension "plpgsql"
16
-
17
- create_table "delayed_jobs", force: :cascade do |t|
18
- t.integer "priority", default: 0, null: false
19
- t.integer "attempts", default: 0, null: false
20
- t.text "handler", null: false
21
- t.text "last_error"
22
- t.datetime "run_at"
23
- t.datetime "locked_at"
24
- t.datetime "failed_at"
25
- t.string "locked_by"
26
- t.string "queue"
27
- t.datetime "created_at"
28
- t.datetime "updated_at"
29
- t.index ["priority", "run_at"], name: "delayed_jobs_priority"
14
+ create_table "widgets", force: :cascade do |t|
15
+ t.string "name"
16
+ t.string "other_column"
30
17
  end
31
18
  end
@@ -0,0 +1,221 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe Journaled::DeliveryJob 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
+ let(:args) { { serialized_event: serialized_event, partition_key: partition_key, app_name: nil } }
9
+
10
+ around do |example|
11
+ with_env(JOURNALED_STREAM_NAME: stream_name) { example.run }
12
+ end
13
+
14
+ describe '#perform' do
15
+ let(:return_status_body) { { shard_id: '101', sequence_number: '101123' } }
16
+ let(:return_object) { instance_double Aws::Kinesis::Types::PutRecordOutput, return_status_body }
17
+
18
+ before do
19
+ allow(Aws::AssumeRoleCredentials).to receive(:new).and_call_original
20
+ allow(Aws::Kinesis::Client).to receive(:new).and_return kinesis_client
21
+ kinesis_client.stub_responses(:put_record, return_status_body)
22
+
23
+ allow(Journaled).to receive(:enabled?).and_return(true)
24
+ end
25
+
26
+ it 'makes requests to AWS to put the event on the Kinesis with the correct body' do
27
+ event = described_class.perform_now(args)
28
+
29
+ expect(event.shard_id).to eq '101'
30
+ expect(event.sequence_number).to eq '101123'
31
+ end
32
+
33
+ context 'when JOURNALED_IAM_ROLE_ARN is defined' do
34
+ let(:aws_sts_client) { Aws::STS::Client.new(stub_responses: true) }
35
+
36
+ around do |example|
37
+ with_env(JOURNALED_IAM_ROLE_ARN: 'iam-role-arn-for-assuming-kinesis-access') { example.run }
38
+ end
39
+
40
+ before do
41
+ allow(Aws::STS::Client).to receive(:new).and_return aws_sts_client
42
+ aws_sts_client.stub_responses(:assume_role, assume_role_response)
43
+ end
44
+
45
+ let(:assume_role_response) do
46
+ {
47
+ assumed_role_user: {
48
+ arn: 'iam-role-arn-for-assuming-kinesis-access',
49
+ assumed_role_id: "ARO123EXAMPLE123:Bob",
50
+ },
51
+ credentials: {
52
+ access_key_id: "AKIAIOSFODNN7EXAMPLE",
53
+ secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY",
54
+ session_token: "EXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI",
55
+ expiration: Time.zone.parse("2011-07-15T23:28:33.359Z"),
56
+ },
57
+ }
58
+ end
59
+
60
+ it 'initializes a Kinesis client with assume role credentials' do
61
+ described_class.perform_now(args)
62
+
63
+ expect(Aws::AssumeRoleCredentials).to have_received(:new).with(
64
+ client: aws_sts_client,
65
+ role_arn: "iam-role-arn-for-assuming-kinesis-access",
66
+ role_session_name: "JournaledAssumeRoleAccess",
67
+ )
68
+ end
69
+ end
70
+
71
+ context 'when the stream name env var is NOT set' do
72
+ let(:stream_name) { nil }
73
+
74
+ it 'raises an KeyError error' do
75
+ expect { described_class.perform_now(args) }.to raise_error KeyError
76
+ end
77
+ end
78
+
79
+ context 'when Amazon responds with an InternalFailure' do
80
+ before do
81
+ kinesis_client.stub_responses(:put_record, 'InternalFailure')
82
+ end
83
+
84
+ it 'catches the error and re-raises a subclass of NotTrulyExceptionalError and logs about the failure' do
85
+ allow(Rails.logger).to receive(:error)
86
+ expect { described_class.perform_now(args) }.to raise_error described_class::KinesisTemporaryFailure
87
+ expect(Rails.logger).to have_received(:error).with(
88
+ "Kinesis Error - Server Error occurred - Aws::Kinesis::Errors::InternalFailure",
89
+ ).once
90
+ end
91
+ end
92
+
93
+ context 'when Amazon responds with a ServiceUnavailable' do
94
+ before do
95
+ kinesis_client.stub_responses(:put_record, 'ServiceUnavailable')
96
+ end
97
+
98
+ it 'catches the error and re-raises a subclass of NotTrulyExceptionalError and logs about the failure' do
99
+ allow(Rails.logger).to receive(:error)
100
+ expect { described_class.perform_now(args) }.to raise_error described_class::KinesisTemporaryFailure
101
+ expect(Rails.logger).to have_received(:error).with(/\AKinesis Error/).once
102
+ end
103
+ end
104
+
105
+ context 'when we receive a 504 Gateway timeout' do
106
+ before do
107
+ kinesis_client.stub_responses(:put_record, 'Aws::Kinesis::Errors::ServiceError')
108
+ end
109
+
110
+ it 'raises an error that subclasses Aws::Kinesis::Errors::ServiceError' do
111
+ expect { described_class.perform_now(args) }.to raise_error Aws::Kinesis::Errors::ServiceError
112
+ end
113
+ end
114
+
115
+ context 'when the IAM user does not have permission to put_record to the specified stream' do
116
+ before do
117
+ kinesis_client.stub_responses(:put_record, 'AccessDeniedException')
118
+ end
119
+
120
+ it 'raises an AccessDeniedException error' do
121
+ expect { described_class.perform_now(args) }.to raise_error Aws::Kinesis::Errors::AccessDeniedException
122
+ end
123
+ end
124
+
125
+ context 'when the request timesout' do
126
+ before do
127
+ kinesis_client.stub_responses(:put_record, Seahorse::Client::NetworkingError.new(Timeout::Error.new))
128
+ end
129
+
130
+ it 'catches the error and re-raises a subclass of NotTrulyExceptionalError and logs about the failure' do
131
+ allow(Rails.logger).to receive(:error)
132
+ expect { described_class.perform_now(args) }.to raise_error described_class::KinesisTemporaryFailure
133
+ expect(Rails.logger).to have_received(:error).with(
134
+ "Kinesis Error - Networking Error occurred - Seahorse::Client::NetworkingError",
135
+ ).once
136
+ end
137
+ end
138
+ end
139
+
140
+ describe ".stream_name" do
141
+ context "when app_name is unspecified" do
142
+ it "is fetched from a prefixed ENV var if specified" do
143
+ allow(ENV).to receive(:fetch).and_return("expected_stream_name")
144
+ expect(described_class.stream_name(app_name: nil)).to eq("expected_stream_name")
145
+ expect(ENV).to have_received(:fetch).with("JOURNALED_STREAM_NAME")
146
+ end
147
+ end
148
+
149
+ context "when app_name is specified" do
150
+ it "is fetched from a prefixed ENV var if specified" do
151
+ allow(ENV).to receive(:fetch).and_return("expected_stream_name")
152
+ expect(described_class.stream_name(app_name: "my_funky_app_name")).to eq("expected_stream_name")
153
+ expect(ENV).to have_received(:fetch).with("MY_FUNKY_APP_NAME_JOURNALED_STREAM_NAME")
154
+ end
155
+ end
156
+ end
157
+
158
+ describe "#kinesis_client_config" do
159
+ it "is in us-east-1 by default" do
160
+ with_env(AWS_DEFAULT_REGION: nil) do
161
+ expect(subject.kinesis_client_config).to include(region: 'us-east-1')
162
+ end
163
+ end
164
+
165
+ it "respects AWS_DEFAULT_REGION env var" do
166
+ with_env(AWS_DEFAULT_REGION: 'us-west-2') do
167
+ expect(subject.kinesis_client_config).to include(region: 'us-west-2')
168
+ end
169
+ end
170
+
171
+ it "doesn't limit retry" do
172
+ expect(subject.kinesis_client_config).to include(retry_limit: 0)
173
+ end
174
+
175
+ it "provides no AWS credentials by default" do
176
+ with_env(RUBY_AWS_ACCESS_KEY_ID: nil, RUBY_AWS_SECRET_ACCESS_KEY: nil) do
177
+ expect(subject.kinesis_client_config).not_to have_key(:access_key_id)
178
+ expect(subject.kinesis_client_config).not_to have_key(:secret_access_key)
179
+ end
180
+ end
181
+
182
+ it "will use legacy credentials if specified" do
183
+ with_env(RUBY_AWS_ACCESS_KEY_ID: 'key_id', RUBY_AWS_SECRET_ACCESS_KEY: 'secret') do
184
+ expect(subject.kinesis_client_config).to include(access_key_id: 'key_id', secret_access_key: 'secret')
185
+ end
186
+ end
187
+
188
+ it "will set http_idle_timeout by default" do
189
+ expect(subject.kinesis_client_config).to include(http_idle_timeout: 5)
190
+ end
191
+
192
+ it "will set http_open_timeout by default" do
193
+ expect(subject.kinesis_client_config).to include(http_open_timeout: 2)
194
+ end
195
+
196
+ it "will set http_read_timeout by default" do
197
+ expect(subject.kinesis_client_config).to include(http_read_timeout: 60)
198
+ end
199
+
200
+ context "when Journaled.http_idle_timeout is specified" do
201
+ it "will set http_idle_timeout by specified value" do
202
+ allow(Journaled).to receive(:http_idle_timeout).and_return(2)
203
+ expect(subject.kinesis_client_config).to include(http_idle_timeout: 2)
204
+ end
205
+ end
206
+
207
+ context "when Journaled.http_open_timeout is specified" do
208
+ it "will set http_open_timeout by specified value" do
209
+ allow(Journaled).to receive(:http_open_timeout).and_return(1)
210
+ expect(subject.kinesis_client_config).to include(http_open_timeout: 1)
211
+ end
212
+ end
213
+
214
+ context "when Journaled.http_read_timeout is specified" do
215
+ it "will set http_read_timeout by specified value" do
216
+ allow(Journaled).to receive(:http_read_timeout).and_return(2)
217
+ expect(subject.kinesis_client_config).to include(http_read_timeout: 2)
218
+ end
219
+ end
220
+ end
221
+ end
@@ -49,4 +49,43 @@ RSpec.describe Journaled do
49
49
  expect(described_class.actor_uri).to eq "my actor uri"
50
50
  end
51
51
  end
52
+
53
+ describe '.detect_queue_adapter!' do
54
+ it 'raises an error unless the queue adapter is DB-backed' do
55
+ expect { described_class.detect_queue_adapter! }.to raise_error <<~MSG
56
+ Journaled has detected an unsupported ActiveJob queue adapter: `:test`
57
+
58
+ Journaled jobs must be enqueued transactionally to your primary database.
59
+
60
+ Please install the appropriate gems and set `queue_adapter` to one of the following:
61
+ - `:delayed`
62
+ - `:delayed_job`
63
+ - `:good_job`
64
+ - `:que`
65
+
66
+ Read more at https://github.com/Betterment/journaled
67
+ MSG
68
+ end
69
+
70
+ context 'when the queue adapter is supported' do
71
+ before do
72
+ stub_const("ActiveJob::QueueAdapters::DelayedAdapter", Class.new)
73
+ ActiveJob::Base.disable_test_adapter
74
+ ActiveJob::Base.queue_adapter = :delayed
75
+ end
76
+
77
+ around do |example|
78
+ begin
79
+ example.run
80
+ ensure
81
+ ActiveJob::Base.queue_adapter = :test
82
+ ActiveJob::Base.enable_test_adapter(ActiveJob::QueueAdapters::TestAdapter.new)
83
+ end
84
+ end
85
+
86
+ it 'does not raise an error' do
87
+ expect { described_class.detect_queue_adapter! }.not_to raise_error
88
+ end
89
+ end
90
+ end
52
91
  end
@@ -92,4 +92,15 @@ RSpec.describe Journaled::Changes do
92
92
  expect(change_writer).to have_received(:delete)
93
93
  expect(Journaled::ChangeWriter).to have_received(:new)
94
94
  end
95
+
96
+ context 'when DJ opts are provided' do
97
+ before do
98
+ klass.journal_changes_to :thing, as: :other_thing, enqueue_with: { asdf: 1, foo: 'bar' }
99
+ end
100
+
101
+ it 'sets them on the model' do
102
+ expect(klass.journaled_enqueue_opts).to eq(asdf: 1, foo: 'bar')
103
+ expect(klass.new.journaled_enqueue_opts).to eq(asdf: 1, foo: 'bar')
104
+ end
105
+ end
95
106
  end
@@ -4,38 +4,42 @@ if Rails::VERSION::MAJOR > 5 || (Rails::VERSION::MAJOR == 5 && Rails::VERSION::M
4
4
  # rubocop:disable Rails/SkipsModelValidations
5
5
  RSpec.describe "Raw database change protection" do
6
6
  let(:journaled_class) do
7
- Class.new(Delayed::Job) do
7
+ Class.new(ActiveRecord::Base) do
8
8
  include Journaled::Changes
9
9
 
10
- journal_changes_to :locked_at, as: :attempt
10
+ self.table_name = 'widgets'
11
+
12
+ journal_changes_to :name, as: :attempt
11
13
  end
12
14
  end
13
15
 
14
16
  let(:journaled_class_with_no_journaled_columns) do
15
- Class.new(Delayed::Job) do
17
+ Class.new(ActiveRecord::Base) do
16
18
  include Journaled::Changes
19
+
20
+ self.table_name = 'widgets'
17
21
  end
18
22
  end
19
23
 
20
24
  describe "the relation" do
21
25
  describe "#update_all" do
22
26
  it "refuses on journaled columns passed as hash" do
23
- expect { journaled_class.update_all(locked_at: nil) }.to raise_error(/aborted by Journaled/)
27
+ expect { journaled_class.update_all(name: nil) }.to raise_error(/aborted by Journaled/)
24
28
  end
25
29
 
26
30
  it "refuses on journaled columns passed as string" do
27
- expect { journaled_class.update_all("\"locked_at\" = NULL") }.to raise_error(/aborted by Journaled/)
28
- expect { journaled_class.update_all("locked_at = null") }.to raise_error(/aborted by Journaled/)
29
- expect { journaled_class.update_all("delayed_jobs.locked_at = null") }.to raise_error(/aborted by Journaled/)
30
- expect { journaled_class.update_all("last_error = 'locked_at'") }.not_to raise_error
31
+ expect { journaled_class.update_all("\"name\" = NULL") }.to raise_error(/aborted by Journaled/)
32
+ expect { journaled_class.update_all("name = null") }.to raise_error(/aborted by Journaled/)
33
+ expect { journaled_class.update_all("widgets.name = null") }.to raise_error(/aborted by Journaled/)
34
+ expect { journaled_class.update_all("other_column = 'name'") }.not_to raise_error
31
35
  end
32
36
 
33
37
  it "succeeds on unjournaled columns" do
34
- expect { journaled_class.update_all(handler: "") }.not_to raise_error
38
+ expect { journaled_class.update_all(other_column: "") }.not_to raise_error
35
39
  end
36
40
 
37
41
  it "succeeds when forced on journaled columns" do
38
- expect { journaled_class.update_all({ locked_at: nil }, force: true) }.not_to raise_error
42
+ expect { journaled_class.update_all({ name: nil }, force: true) }.not_to raise_error
39
43
  end
40
44
  end
41
45
 
@@ -69,29 +73,19 @@ if Rails::VERSION::MAJOR > 5 || (Rails::VERSION::MAJOR == 5 && Rails::VERSION::M
69
73
  end
70
74
 
71
75
  describe "an instance" do
72
- let(:job) do
73
- module TestJob
74
- def perform
75
- "foo"
76
- end
77
-
78
- module_function :perform
79
- end
80
- end
81
-
82
- subject { journaled_class.enqueue(job) }
76
+ subject { journaled_class.create!(name: 'foo') }
83
77
 
84
78
  describe "#update_columns" do
85
79
  it "refuses on journaled columns" do
86
- expect { subject.update_columns(locked_at: nil) }.to raise_error(/aborted by Journaled/)
80
+ expect { subject.update_columns(name: nil) }.to raise_error(/aborted by Journaled/)
87
81
  end
88
82
 
89
83
  it "succeeds on unjournaled columns" do
90
- expect { subject.update_columns(handler: "") }.not_to raise_error
84
+ expect { subject.update_columns(other_column: "") }.not_to raise_error
91
85
  end
92
86
 
93
87
  it "succeeds when forced on journaled columns" do
94
- expect { subject.update_columns({ locked_at: nil }, force: true) }.not_to raise_error
88
+ expect { subject.update_columns({ name: nil }, force: true) }.not_to raise_error
95
89
  end
96
90
  end
97
91
 
@@ -101,7 +95,7 @@ if Rails::VERSION::MAJOR > 5 || (Rails::VERSION::MAJOR == 5 && Rails::VERSION::M
101
95
  end
102
96
 
103
97
  it "succeeds if no journaled columns exist" do
104
- instance = journaled_class_with_no_journaled_columns.enqueue(job)
98
+ instance = journaled_class_with_no_journaled_columns.create!
105
99
  expect { instance.delete }.not_to raise_error
106
100
  end
107
101