journaled 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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