materialist 2.3.1 → 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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/Gemfile +1 -1
- data/README.md +28 -14
- data/RELEASE_NOTES.md +8 -0
- data/lib/configuration.rb +30 -0
- data/lib/materialist.rb +3 -1
- data/lib/materialist/event_handler.rb +11 -9
- data/lib/materialist/event_worker.rb +28 -1
- data/materialist.gemspec +2 -0
- data/spec/materialist/event_handler_spec.rb +13 -8
- data/spec/materialist/event_worker_spec.rb +48 -5
- data/spec/materialist/materialized_record_spec.rb +2 -2
- data/spec/materialist/materializer_spec.rb +129 -215
- data/spec/models.rb +8 -0
- data/spec/schema.rb +31 -0
- data/spec/spec_helper.rb +15 -0
- metadata +35 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c126eb6aa8d3e7fd91c9628f1a9a4a16e6476e8
|
4
|
+
data.tar.gz: 3d20acfd6c6ae59358dcfffb0b6270d438d6f7e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39310ca655d95cd3b51b6765da39f5e7ccb6479ca8df8680c5b6f27100cd7fd5479458d36b5baca80d80b1c8b29dec266453b2e2bd26c98dd4b65e01044b5351
|
7
|
+
data.tar.gz: df9c7c8f9c59a6a1b640f011deb7e848ac2cb9e705b288bbb6d8238c1c08e3db5a4cca8014ddfa061236d52a13d60d6a1d239758a414e352adc09e9b1a6d8b64
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -52,27 +52,47 @@ class Zone < ApplicationRecord
|
|
52
52
|
end
|
53
53
|
```
|
54
54
|
|
55
|
+
### Materialist Configuration
|
56
|
+
|
57
|
+
If you need to override any of the materialist configurations,
|
58
|
+
you can do so in an `configure/initializers/materialist.rb` file:
|
59
|
+
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
Materialist.configure do |config|
|
63
|
+
# Configure materialist here. For example:
|
64
|
+
#
|
65
|
+
# config.topics = %w(topic_a topic_b)
|
66
|
+
#
|
67
|
+
# config.sidekiq_options = {
|
68
|
+
# queue: :routemaster_index,
|
69
|
+
# retry: 3
|
70
|
+
# }
|
71
|
+
#
|
72
|
+
# config.metrics_client = STATSD
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
- `topics` (only when using in `.subscribe`): A string array of topics to be used.
|
77
|
+
If not provided nothing would be materialized.
|
78
|
+
- `sidekiq_options` (optional, default: `{ retry: 10 }`) -- See [Sidekiq docs](https://github.com/mperham/sidekiq/wiki/Advanced-Options#workers) for list of optiosn
|
79
|
+
- `metrics_client` (optional) -- You can pass your `STATSD` instance
|
80
|
+
|
55
81
|
### Routemaster Configuration
|
56
82
|
|
57
83
|
First you need an "event handler":
|
58
84
|
|
59
85
|
```ruby
|
60
|
-
handler = Materialist::EventHandler.new
|
86
|
+
handler = Materialist::EventHandler.new
|
61
87
|
```
|
62
88
|
|
63
89
|
Where options could be:
|
64
90
|
|
65
|
-
- `topics` (only when using in `.subscribe`): An array of topics to be used.
|
66
|
-
If not provided nothing would be materialized.
|
67
|
-
- `queue` (optional): name of the queue to be used by sidekiq worker
|
68
|
-
- `retry` (default: `10`): sidekiq retry policy to be used.
|
69
|
-
|
70
91
|
Then there are two ways to configure materialist in routemaster:
|
71
92
|
|
72
93
|
1. **If you DON'T need resources to be cached in redis:** use `handler` as siphon:
|
73
94
|
|
74
95
|
```ruby
|
75
|
-
handler = Materialist::EventHandler.new
|
76
96
|
siphon_events = {
|
77
97
|
zones: handler,
|
78
98
|
rider_domain_riders: handler
|
@@ -86,15 +106,9 @@ map '/events' do
|
|
86
106
|
end
|
87
107
|
```
|
88
108
|
|
89
|
-
2. **You DO need resources cached in redis:** In this case you need to use `handler` to subscribe to
|
109
|
+
2. **You DO need resources cached in redis:** In this case you need to provide `topics` in `Materialist.configure` and use `handler` to subscribe to routemaster caching pipeline:
|
90
110
|
|
91
111
|
```ruby
|
92
|
-
TOPICS = %w(
|
93
|
-
zones
|
94
|
-
rider_domain_riders
|
95
|
-
)
|
96
|
-
|
97
|
-
handler = Materialist::EventHandler.new({ topics: TOPICS })
|
98
112
|
app = Routemaster::Drain::Caching.new # or ::Basic.new
|
99
113
|
app.subscribe(handler, prefix: true)
|
100
114
|
# ...
|
data/RELEASE_NOTES.md
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Materialist
|
2
|
+
class << self
|
3
|
+
def configuration
|
4
|
+
@configuration ||= Configuration.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def reset_configuration!
|
8
|
+
@configuration = Configuration.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def configure
|
12
|
+
yield(self.configuration)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Configuration
|
17
|
+
attr_accessor :topics, :sidekiq_options, :metrics_client
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@topics = []
|
21
|
+
@sidekiq_options = {}
|
22
|
+
@metrics_client = NullMetricsClient
|
23
|
+
end
|
24
|
+
|
25
|
+
class NullMetricsClient
|
26
|
+
def self.increment(_, tags:); end
|
27
|
+
def self.histogram(_, _v, tags:); end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/materialist.rb
CHANGED
@@ -4,15 +4,13 @@ require_relative './event_worker'
|
|
4
4
|
module Materialist
|
5
5
|
class EventHandler
|
6
6
|
|
7
|
-
|
8
|
-
DEFAULT_OPTIONS = { retry: 10 }
|
7
|
+
DEFAULT_SIDEKIQ_OPTIONS = { retry: 10 }.freeze
|
9
8
|
|
10
|
-
def initialize
|
11
|
-
@options = DEFAULT_OPTIONS.merge(options)
|
9
|
+
def initialize
|
12
10
|
end
|
13
11
|
|
14
12
|
def on_events_received(batch)
|
15
|
-
batch.each { |event| call(event) if
|
13
|
+
batch.each { |event| call(event) if should_materialize?(event['topic']) }
|
16
14
|
end
|
17
15
|
|
18
16
|
def call(event)
|
@@ -21,14 +19,18 @@ module Materialist
|
|
21
19
|
|
22
20
|
private
|
23
21
|
|
24
|
-
attr_reader :
|
22
|
+
attr_reader :topics
|
25
23
|
|
26
|
-
def
|
27
|
-
|
24
|
+
def should_materialize?(topic)
|
25
|
+
Materialist.configuration.topics.include?(topic.to_s)
|
26
|
+
end
|
27
|
+
|
28
|
+
def sidekiq_options
|
29
|
+
DEFAULT_SIDEKIQ_OPTIONS.merge(Materialist.configuration.sidekiq_options)
|
28
30
|
end
|
29
31
|
|
30
32
|
def worker
|
31
|
-
Materialist::EventWorker.set(
|
33
|
+
Materialist::EventWorker.set(sidekiq_options)
|
32
34
|
end
|
33
35
|
end
|
34
36
|
end
|
@@ -7,8 +7,35 @@ module Materialist
|
|
7
7
|
|
8
8
|
def perform(event)
|
9
9
|
topic = event['topic']
|
10
|
+
action = event['type'].to_sym
|
11
|
+
timestamp = event['t']
|
12
|
+
|
10
13
|
materializer = "#{topic.to_s.singularize.classify}Materializer".constantize
|
11
|
-
materializer.perform(event['url'],
|
14
|
+
materializer.perform(event['url'], action)
|
15
|
+
|
16
|
+
report_latency(topic, timestamp) if timestamp
|
17
|
+
report_stats(topic, action, :success)
|
18
|
+
rescue
|
19
|
+
report_stats(topic, action, :failure)
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def report_latency(topic, timestamp)
|
26
|
+
t = (Time.now.to_f - (timestamp.to_i / 1e3)).round(1)
|
27
|
+
Materialist.configuration.metrics_client.histogram(
|
28
|
+
"materialist.event_latency",
|
29
|
+
t,
|
30
|
+
tags: ["topic:#{topic}"]
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def report_stats(topic, action, kind)
|
35
|
+
Materialist.configuration.metrics_client.increment(
|
36
|
+
"materialist.event_worker.#{kind}",
|
37
|
+
tags: ["action:#{action}", "topic:#{topic}"]
|
38
|
+
)
|
12
39
|
end
|
13
40
|
end
|
14
41
|
end
|
data/materialist.gemspec
CHANGED
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.add_runtime_dependency 'activesupport'
|
21
21
|
spec.add_runtime_dependency 'routemaster-drain', '>= 3.0'
|
22
22
|
|
23
|
+
spec.add_development_dependency 'activerecord'
|
24
|
+
spec.add_development_dependency 'sqlite3'
|
23
25
|
spec.add_development_dependency 'dotenv'
|
24
26
|
spec.add_development_dependency 'simplecov'
|
25
27
|
spec.add_development_dependency 'codecov'
|
@@ -3,11 +3,16 @@ require 'materialist/event_handler'
|
|
3
3
|
require 'materialist/event_worker'
|
4
4
|
|
5
5
|
RSpec.describe Materialist::EventHandler do
|
6
|
-
let(:
|
7
|
-
|
8
|
-
|
6
|
+
let(:topics) {[]}
|
7
|
+
let(:sidekiq_options) {{}}
|
9
8
|
let(:worker_double) { double() }
|
9
|
+
|
10
10
|
before do
|
11
|
+
Materialist.configure do |config|
|
12
|
+
config.sidekiq_options = sidekiq_options
|
13
|
+
config.topics = topics
|
14
|
+
end
|
15
|
+
|
11
16
|
allow(Materialist::EventWorker).to receive(:set)
|
12
17
|
.and_return worker_double
|
13
18
|
end
|
@@ -17,7 +22,7 @@ RSpec.describe Materialist::EventHandler do
|
|
17
22
|
let(:perform) { subject.on_events_received events.map() }
|
18
23
|
|
19
24
|
context "when no topic is specified" do
|
20
|
-
let(:
|
25
|
+
let(:topics) {[]}
|
21
26
|
|
22
27
|
it "doesn't enqueue any event" do
|
23
28
|
expect(worker_double).to_not receive(:perform_async)
|
@@ -26,7 +31,7 @@ RSpec.describe Materialist::EventHandler do
|
|
26
31
|
end
|
27
32
|
|
28
33
|
context "when a topic is specified" do
|
29
|
-
let(:
|
34
|
+
let(:topics) { %w(topic_a) }
|
30
35
|
|
31
36
|
it "enqueues event of that topic" do
|
32
37
|
expect(worker_double).to receive(:perform_async).with(events[0])
|
@@ -35,7 +40,7 @@ RSpec.describe Materialist::EventHandler do
|
|
35
40
|
end
|
36
41
|
|
37
42
|
context "when both topics are specified" do
|
38
|
-
let(:
|
43
|
+
let(:topics) { %w(topic_a topic_b) }
|
39
44
|
|
40
45
|
it "enqueues event of both topics" do
|
41
46
|
expect(worker_double).to receive(:perform_async).twice
|
@@ -55,7 +60,7 @@ RSpec.describe Materialist::EventHandler do
|
|
55
60
|
|
56
61
|
context "if queue name is privided" do
|
57
62
|
let(:queue_name) { :some_queue_name }
|
58
|
-
let(:
|
63
|
+
let(:sidekiq_options) {{ queue: queue_name }}
|
59
64
|
|
60
65
|
it "enqueues the event in the given queue" do
|
61
66
|
expect(Materialist::EventWorker).to receive(:set)
|
@@ -66,7 +71,7 @@ RSpec.describe Materialist::EventHandler do
|
|
66
71
|
end
|
67
72
|
|
68
73
|
context "when a retry is specified in options" do
|
69
|
-
let(:
|
74
|
+
let(:sidekiq_options) {{ retry: false }}
|
70
75
|
|
71
76
|
it "uses the given retry option for sidekiq" do
|
72
77
|
expect(Materialist::EventWorker).to receive(:set)
|
@@ -5,17 +5,60 @@ RSpec.describe Materialist::EventWorker do
|
|
5
5
|
describe "#perform" do
|
6
6
|
let(:source_url) { 'https://service.dev/foobars/1' }
|
7
7
|
let(:event) {{ 'topic' => :foobar, 'url' => source_url, 'type' => 'noop' }}
|
8
|
-
let!(:materializer_class) {
|
8
|
+
let!(:materializer_class) { FoobarMaterializer = Class.new }
|
9
|
+
let(:metrics_client) { double(increment: true) }
|
9
10
|
|
10
11
|
before do
|
11
12
|
allow(FoobarMaterializer).to receive(:perform)
|
13
|
+
Materialist.configure { |c| c.metrics_client = metrics_client }
|
12
14
|
end
|
13
15
|
|
14
|
-
|
15
|
-
let(:perform) { subject.perform(event) }
|
16
|
+
after { Object.send(:remove_const, :FoobarMaterializer) }
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
let(:perform) { subject.perform(event) }
|
19
|
+
|
20
|
+
it "calls the relevant materializer" do
|
21
|
+
expect(FoobarMaterializer).to receive(:perform).with(source_url, :noop)
|
22
|
+
perform
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'logs success to metrics' do
|
26
|
+
expect(metrics_client).to receive(:increment).with(
|
27
|
+
"materialist.event_worker.success",
|
28
|
+
tags: ["action:noop", "topic:foobar"]
|
29
|
+
)
|
30
|
+
perform
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'does not log latency' do
|
34
|
+
expect(metrics_client).to_not receive(:histogram)
|
35
|
+
perform
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'when there is an error' do
|
39
|
+
let(:error){ StandardError.new }
|
40
|
+
before do
|
41
|
+
expect(FoobarMaterializer).to receive(:perform).and_raise error
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'logs failure to metrics and re-raises the error' do
|
45
|
+
expect(metrics_client).to receive(:increment).with(
|
46
|
+
"materialist.event_worker.failure",
|
47
|
+
tags: ["action:noop", "topic:foobar"]
|
48
|
+
)
|
49
|
+
expect{ perform }.to raise_error error
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when event has a timestamp' do
|
54
|
+
let(:event) {{ 'topic' => :foobar, 'url' => source_url, 'type' => 'noop', 't' => '1519659773842' }}
|
55
|
+
|
56
|
+
it 'logs latency to metrics' do
|
57
|
+
expect(metrics_client).to receive(:histogram).with(
|
58
|
+
"materialist.event_latency",
|
59
|
+
instance_of(Float),
|
60
|
+
tags: ["topic:foobar"]
|
61
|
+
)
|
19
62
|
perform
|
20
63
|
end
|
21
64
|
end
|
@@ -6,7 +6,7 @@ RSpec.describe Materialist::MaterializedRecord do
|
|
6
6
|
uses_redis
|
7
7
|
|
8
8
|
let!(:materialized_type) do
|
9
|
-
|
9
|
+
Class.new do
|
10
10
|
include Materialist::MaterializedRecord
|
11
11
|
|
12
12
|
attr_accessor :source_url
|
@@ -19,7 +19,7 @@ RSpec.describe Materialist::MaterializedRecord do
|
|
19
19
|
end
|
20
20
|
|
21
21
|
let(:record) do
|
22
|
-
|
22
|
+
materialized_type.new.tap { |r| r.source_url = source_url }
|
23
23
|
end
|
24
24
|
|
25
25
|
let(:country_url) { 'https://service.dev/countries/1' }
|
@@ -7,7 +7,7 @@ RSpec.describe Materialist::Materializer do
|
|
7
7
|
|
8
8
|
describe "#perform" do
|
9
9
|
let!(:materializer_class) do
|
10
|
-
|
10
|
+
FoobarMaterializer = Class.new do
|
11
11
|
include Materialist::Materializer
|
12
12
|
|
13
13
|
persist_to :foobar
|
@@ -27,122 +27,16 @@ RSpec.describe Materialist::Materializer do
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
30
|
+
end
|
30
31
|
|
31
|
-
|
32
|
+
let!(:city_materializer) do
|
33
|
+
CityMaterializer = Class.new do
|
32
34
|
include Materialist::Materializer
|
33
35
|
|
34
36
|
persist_to :city
|
35
37
|
source_key :source_url
|
36
38
|
capture :name
|
37
39
|
end
|
38
|
-
|
39
|
-
class DefinedSourceMaterializer
|
40
|
-
include Materialist::Materializer
|
41
|
-
|
42
|
-
persist_to :defined_source
|
43
|
-
|
44
|
-
source_key :source_id do |url|
|
45
|
-
url.split('/').last.to_i
|
46
|
-
end
|
47
|
-
|
48
|
-
capture :name
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# this class mocks active record behaviour
|
53
|
-
class BaseModel
|
54
|
-
def update_attributes!(attrs)
|
55
|
-
attrs.each { |k, v| send("#{k}=", v) }
|
56
|
-
save!
|
57
|
-
end
|
58
|
-
|
59
|
-
def save!
|
60
|
-
self.class.all[source_key_value] = self
|
61
|
-
end
|
62
|
-
|
63
|
-
def destroy!
|
64
|
-
self.class.all.delete source_key_value
|
65
|
-
end
|
66
|
-
|
67
|
-
def reload
|
68
|
-
self.class.all[source_key_value]
|
69
|
-
end
|
70
|
-
|
71
|
-
def actions_called
|
72
|
-
@_actions_called ||= {}
|
73
|
-
end
|
74
|
-
|
75
|
-
private def source_key_value
|
76
|
-
send(self.class.source_key_column)
|
77
|
-
end
|
78
|
-
|
79
|
-
class << self
|
80
|
-
attr_accessor :error_to_throw_once_in_find_or_initialize_by,
|
81
|
-
:source_key_column
|
82
|
-
|
83
|
-
def find_or_initialize_by(kv_hash)
|
84
|
-
if(err = error_to_throw_once_in_find_or_initialize_by)
|
85
|
-
self.error_to_throw_once_in_find_or_initialize_by = nil
|
86
|
-
raise err
|
87
|
-
end
|
88
|
-
|
89
|
-
key_value = kv_hash[source_key_column]
|
90
|
-
|
91
|
-
(all[key_value] || self.new).tap do |record|
|
92
|
-
record.send("#{source_key_column}=", key_value)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
def source_key_column
|
97
|
-
@source_key_column || :source_url
|
98
|
-
end
|
99
|
-
|
100
|
-
def find_by(kv_hash)
|
101
|
-
key_value = kv_hash[source_key_column]
|
102
|
-
all[key_value]
|
103
|
-
end
|
104
|
-
|
105
|
-
def create!(attrs)
|
106
|
-
new.tap do |record|
|
107
|
-
record.update_attributes! attrs
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def all
|
112
|
-
store[self.name] ||= {}
|
113
|
-
end
|
114
|
-
|
115
|
-
def destroy_all
|
116
|
-
store[self.name] = {}
|
117
|
-
end
|
118
|
-
|
119
|
-
def store
|
120
|
-
@@_store ||= {}
|
121
|
-
end
|
122
|
-
|
123
|
-
def count
|
124
|
-
all.keys.size
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
class Foobar < BaseModel
|
130
|
-
attr_accessor :source_url, :name, :how_old, :age, :timezone,
|
131
|
-
:country_tld, :city_url, :account_url
|
132
|
-
end
|
133
|
-
|
134
|
-
class City < BaseModel
|
135
|
-
attr_accessor :source_url, :name
|
136
|
-
end
|
137
|
-
|
138
|
-
class DefinedSource < BaseModel
|
139
|
-
attr_accessor :source_id, :name
|
140
|
-
self.source_key_column = :source_id
|
141
|
-
end
|
142
|
-
|
143
|
-
module ActiveRecord
|
144
|
-
class RecordNotUnique < StandardError; end
|
145
|
-
class RecordInvalid < StandardError; end
|
146
40
|
end
|
147
41
|
|
148
42
|
let(:country_url) { 'https://service.dev/countries/1' }
|
@@ -164,18 +58,19 @@ RSpec.describe Materialist::Materializer do
|
|
164
58
|
end
|
165
59
|
|
166
60
|
before do
|
167
|
-
Foobar.destroy_all
|
168
|
-
City.destroy_all
|
169
|
-
DefinedSource.destroy_all
|
170
|
-
|
171
61
|
stub_resource source_url, source_body
|
172
62
|
stub_resource country_url, country_body
|
173
63
|
stub_resource city_url, city_body
|
174
64
|
stub_resource defined_source_url, defined_source_body
|
175
65
|
end
|
176
66
|
|
67
|
+
after do
|
68
|
+
Object.send(:remove_const, :FoobarMaterializer)
|
69
|
+
Object.send(:remove_const, :CityMaterializer)
|
70
|
+
end
|
71
|
+
|
177
72
|
let(:action) { :create }
|
178
|
-
let(:perform) {
|
73
|
+
let(:perform) { materializer_class.perform(source_url, action) }
|
179
74
|
|
180
75
|
it "materializes record in db" do
|
181
76
|
expect{perform}.to change{Foobar.count}.by 1
|
@@ -212,31 +107,28 @@ RSpec.describe Materialist::Materializer do
|
|
212
107
|
end
|
213
108
|
|
214
109
|
context "when there is a race condition between a create and update" do
|
215
|
-
let(:error) {
|
110
|
+
let(:error) { nil }
|
216
111
|
let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
|
217
112
|
|
218
|
-
|
219
|
-
|
220
|
-
[ ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid ].each do |error_type|
|
113
|
+
[ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid].each do |error_type|
|
221
114
|
context "when error of type #{error_type.name} is thrown" do
|
222
115
|
let(:error) { error_type }
|
223
116
|
|
224
117
|
it "still updates the record" do
|
118
|
+
expect(Foobar).to receive(:find_or_initialize_by).ordered.and_raise(error)
|
119
|
+
expect(Foobar).to receive(:find_or_initialize_by).ordered.and_call_original
|
225
120
|
expect{ perform }.to change { record.reload.name }
|
226
121
|
.from('mo').to('jack')
|
227
122
|
end
|
228
123
|
|
229
124
|
context "if error was thrown second time" do
|
230
|
-
before { allow(Foobar).to receive(:find_or_initialize_by).and_raise error }
|
231
|
-
|
232
125
|
it "bubbles up the error" do
|
126
|
+
expect(Foobar).to receive(:find_or_initialize_by).and_raise(error).twice
|
233
127
|
expect{ perform }.to raise_error error
|
234
128
|
end
|
235
129
|
end
|
236
|
-
|
237
130
|
end
|
238
131
|
end
|
239
|
-
|
240
132
|
end
|
241
133
|
|
242
134
|
%i(create update noop).each do |action_name|
|
@@ -276,21 +168,18 @@ RSpec.describe Materialist::Materializer do
|
|
276
168
|
|
277
169
|
context "when {after, before}_upsert is configured" do
|
278
170
|
let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
|
171
|
+
let(:actions_called) { materializer_class.class_variable_get(:@@actions_called) }
|
279
172
|
let!(:materializer_class) do
|
280
|
-
|
173
|
+
FoobarMaterializer = Class.new do
|
281
174
|
include Materialist::Materializer
|
175
|
+
@@actions_called = {}
|
282
176
|
|
283
177
|
persist_to :foobar
|
284
|
-
before_upsert :
|
285
|
-
after_upsert :
|
286
|
-
|
287
|
-
def my_method(entity)
|
288
|
-
entity.actions_called[:after_upsert] = true
|
289
|
-
end
|
178
|
+
before_upsert :before_hook
|
179
|
+
after_upsert :after_hook
|
290
180
|
|
291
|
-
def
|
292
|
-
|
293
|
-
end
|
181
|
+
def before_hook(entity); @@actions_called[:before_hook] = true; end
|
182
|
+
def after_hook(entity); @@actions_called[:after_hook] = true; end
|
294
183
|
end
|
295
184
|
end
|
296
185
|
|
@@ -298,30 +187,36 @@ RSpec.describe Materialist::Materializer do
|
|
298
187
|
context "when action is :#{action_name}" do
|
299
188
|
let(:action) { action_name }
|
300
189
|
it "calls before_upsert method" do
|
301
|
-
expect{ perform }.to change {
|
190
|
+
expect{ perform }.to change { actions_called[:before_hook] }
|
302
191
|
end
|
303
192
|
|
304
193
|
it "calls after_upsert method" do
|
305
|
-
expect{ perform }.to change {
|
194
|
+
expect{ perform }.to change { actions_called[:after_hook] }
|
306
195
|
end
|
307
196
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
197
|
+
context "when configured with more than one hook" do
|
198
|
+
let(:materializer_class) do
|
199
|
+
FoobarMaterializer = Class.new do
|
200
|
+
include Materialist::Materializer
|
201
|
+
@@actions_called = {}
|
312
202
|
|
313
|
-
|
314
|
-
|
315
|
-
|
203
|
+
persist_to :foobar
|
204
|
+
before_upsert :before_hook, :before_hook2
|
205
|
+
after_upsert :after_hook, :after_hook2
|
316
206
|
|
317
|
-
|
318
|
-
entity
|
207
|
+
def before_hook(entity); @@actions_called[:before_hook] = true; end
|
208
|
+
def before_hook2(entity); @@actions_called[:before_hook2] = true; end
|
209
|
+
def after_hook(entity); @@actions_called[:after_hook] = true; end
|
210
|
+
def after_hook2(entity); @@actions_called[:after_hook2] = true; end
|
319
211
|
end
|
320
212
|
end
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
213
|
+
|
214
|
+
it "calls more than one method" do
|
215
|
+
expect{ perform }.to change { actions_called[:before_hook] }
|
216
|
+
.and change { actions_called[:before_hook2] }
|
217
|
+
.and change { actions_called[:after_hook] }
|
218
|
+
.and change { actions_called[:after_hook2] }
|
219
|
+
end
|
325
220
|
end
|
326
221
|
end
|
327
222
|
end
|
@@ -330,11 +225,11 @@ RSpec.describe Materialist::Materializer do
|
|
330
225
|
let(:action) { :delete }
|
331
226
|
|
332
227
|
it "does not call after_upsert method" do
|
333
|
-
expect{ perform }.to_not change {
|
228
|
+
expect{ perform }.to_not change { actions_called[:after_hook] }
|
334
229
|
end
|
335
230
|
|
336
231
|
it "does call after_upsert method" do
|
337
|
-
expect{ perform }.to_not change {
|
232
|
+
expect{ perform }.to_not change { actions_called[:before_hook] }
|
338
233
|
end
|
339
234
|
end
|
340
235
|
|
@@ -342,21 +237,18 @@ RSpec.describe Materialist::Materializer do
|
|
342
237
|
|
343
238
|
context "when {before, after}_destroy is configured" do
|
344
239
|
let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
|
240
|
+
let(:actions_called) { materializer_class.class_variable_get(:@@actions_called) }
|
345
241
|
let!(:materializer_class) do
|
346
|
-
|
242
|
+
FoobarMaterializer = Class.new do
|
347
243
|
include Materialist::Materializer
|
244
|
+
@@actions_called = {}
|
348
245
|
|
349
246
|
persist_to :foobar
|
350
|
-
before_destroy :
|
351
|
-
after_destroy :
|
352
|
-
|
353
|
-
def my_method(entity)
|
354
|
-
entity.actions_called[:after_destroy] = true
|
355
|
-
end
|
247
|
+
before_destroy :before_hook
|
248
|
+
after_destroy :after_hook
|
356
249
|
|
357
|
-
def
|
358
|
-
|
359
|
-
end
|
250
|
+
def before_hook(entity); @@actions_called[:before_hook] = true; end
|
251
|
+
def after_hook(entity); @@actions_called[:after_hook] = true; end
|
360
252
|
end
|
361
253
|
end
|
362
254
|
|
@@ -364,11 +256,11 @@ RSpec.describe Materialist::Materializer do
|
|
364
256
|
context "when action is :#{action_name}" do
|
365
257
|
let(:action) { action_name }
|
366
258
|
it "does not call after_destroy method" do
|
367
|
-
expect{ perform }.to_not change {
|
259
|
+
expect{ perform }.to_not change { actions_called[:after_hook] }
|
368
260
|
end
|
369
261
|
|
370
262
|
it "does not call before_destroy method" do
|
371
|
-
expect{ perform }.to_not change {
|
263
|
+
expect{ perform }.to_not change { actions_called[:before_hook] }
|
372
264
|
end
|
373
265
|
end
|
374
266
|
end
|
@@ -377,54 +269,60 @@ RSpec.describe Materialist::Materializer do
|
|
377
269
|
let(:action) { :delete }
|
378
270
|
|
379
271
|
it "calls after_destroy method" do
|
380
|
-
expect{ perform }.to change {
|
272
|
+
expect{ perform }.to change { actions_called[:after_hook] }
|
381
273
|
end
|
382
274
|
|
383
275
|
it "calls before_destroy method" do
|
384
|
-
expect{ perform }.to change {
|
276
|
+
expect{ perform }.to change { actions_called[:before_hook] }
|
385
277
|
end
|
386
278
|
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
279
|
+
context "when configured with more than one hook" do
|
280
|
+
let(:materializer_class) do
|
281
|
+
FoobarMaterializer = Class.new do
|
282
|
+
include Materialist::Materializer
|
283
|
+
@@actions_called = {}
|
391
284
|
|
392
|
-
|
393
|
-
|
394
|
-
|
285
|
+
persist_to :foobar
|
286
|
+
before_destroy :before_hook, :before_hook2
|
287
|
+
after_destroy :after_hook, :after_hook2
|
395
288
|
|
396
|
-
|
397
|
-
entity
|
289
|
+
def before_hook(entity); @@actions_called[:before_hook] = true; end
|
290
|
+
def before_hook2(entity); @@actions_called[:before_hook2] = true; end
|
291
|
+
def after_hook(entity); @@actions_called[:after_hook] = true; end
|
292
|
+
def after_hook2(entity); @@actions_called[:after_hook2] = true; end
|
398
293
|
end
|
399
294
|
end
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
295
|
+
|
296
|
+
it "calls more than one method" do
|
297
|
+
expect{ perform }.to change { actions_called[:before_hook] }
|
298
|
+
.and change { actions_called[:before_hook2] }
|
299
|
+
.and change { actions_called[:after_hook] }
|
300
|
+
.and change { actions_called[:after_hook2] }
|
301
|
+
end
|
404
302
|
end
|
405
303
|
|
406
304
|
context "when resource doesn't exist locally" do
|
407
305
|
it "does not raise error" do
|
408
|
-
Foobar.
|
306
|
+
Foobar.delete_all
|
409
307
|
expect{ perform }.to_not raise_error
|
410
308
|
end
|
411
309
|
end
|
412
310
|
end
|
413
|
-
|
414
311
|
end
|
415
312
|
|
416
313
|
context "when not materializing self but materializing linked parent" do
|
417
|
-
|
418
|
-
|
314
|
+
subject do
|
315
|
+
Class.new do
|
316
|
+
include Materialist::Materializer
|
419
317
|
|
420
|
-
|
318
|
+
materialize_link :city
|
319
|
+
end
|
421
320
|
end
|
422
|
-
|
423
321
|
let(:city_settings_url) { 'https://service.dev/city_settings/1' }
|
424
322
|
let(:city_settings_body) {{ _links: { city: { href: city_url }}}}
|
425
323
|
before { stub_resource city_settings_url, city_settings_body }
|
426
324
|
|
427
|
-
let(:perform) {
|
325
|
+
let(:perform) { subject.perform(city_settings_url, action) }
|
428
326
|
|
429
327
|
it "materializes linked parent" do
|
430
328
|
expect{perform}.to change{City.count}.by 1
|
@@ -439,47 +337,63 @@ RSpec.describe Materialist::Materializer do
|
|
439
337
|
end
|
440
338
|
end
|
441
339
|
|
442
|
-
context "
|
443
|
-
|
340
|
+
context "entity based on the source_key column" do
|
341
|
+
subject do
|
342
|
+
Class.new do
|
343
|
+
include Materialist::Materializer
|
444
344
|
|
445
|
-
|
446
|
-
|
447
|
-
|
345
|
+
persist_to :defined_source
|
346
|
+
|
347
|
+
source_key :source_id do |url|
|
348
|
+
url.split('/').last.to_i
|
349
|
+
end
|
448
350
|
|
449
|
-
|
450
|
-
|
451
|
-
inserted = DefinedSource.find_by(source_id: defined_source_id)
|
452
|
-
expect(inserted.source_id).to eq defined_source_id
|
453
|
-
expect(inserted.name).to eq defined_source_body[:name]
|
351
|
+
capture :name
|
352
|
+
end
|
454
353
|
end
|
455
|
-
end
|
456
354
|
|
457
|
-
|
458
|
-
|
459
|
-
let!(:record) { DefinedSource.create!(source_id: defined_source_id, name: 'mo') }
|
460
|
-
let(:perform) { DefinedSourceMaterializer.perform(defined_source_url, action) }
|
355
|
+
context "when creating" do
|
356
|
+
let(:perform) { subject.perform(defined_source_url, action) }
|
461
357
|
|
462
|
-
|
463
|
-
|
464
|
-
|
358
|
+
it "creates based on source_key" do
|
359
|
+
expect{perform}.to change{DefinedSource.count}.by 1
|
360
|
+
end
|
361
|
+
|
362
|
+
it "sets the correct source key" do
|
363
|
+
perform
|
364
|
+
inserted = DefinedSource.find_by(source_id: defined_source_id)
|
365
|
+
expect(inserted.source_id).to eq defined_source_id
|
366
|
+
expect(inserted.name).to eq defined_source_body[:name]
|
367
|
+
end
|
465
368
|
end
|
466
369
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
370
|
+
context "when updating" do
|
371
|
+
let(:action) { :update }
|
372
|
+
let!(:record) { DefinedSource.create!(source_id: defined_source_id, name: 'mo') }
|
373
|
+
let(:perform) { subject.perform(defined_source_url, action) }
|
374
|
+
|
375
|
+
it "updates based on source_key" do
|
376
|
+
perform
|
377
|
+
expect(DefinedSource.count).to eq 1
|
378
|
+
end
|
379
|
+
|
380
|
+
it "updates the existing record" do
|
381
|
+
perform
|
382
|
+
inserted = DefinedSource.find_by(source_id: defined_source_id)
|
383
|
+
expect(inserted.source_id).to eq defined_source_id
|
384
|
+
expect(inserted.name).to eq defined_source_body[:name]
|
385
|
+
end
|
472
386
|
end
|
473
|
-
end
|
474
387
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
388
|
+
context "when deleting" do
|
389
|
+
let(:action) { :delete }
|
390
|
+
let!(:record) { DefinedSource.create!(source_id: defined_source_id, name: 'mo') }
|
391
|
+
let(:perform) { subject.perform(defined_source_url, action) }
|
479
392
|
|
480
|
-
|
481
|
-
|
482
|
-
|
393
|
+
it "deletes based on source_key" do
|
394
|
+
perform
|
395
|
+
expect(DefinedSource.count).to eq 0
|
396
|
+
end
|
483
397
|
end
|
484
398
|
end
|
485
399
|
end
|
data/spec/models.rb
ADDED
data/spec/schema.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
self.verbose = false
|
3
|
+
|
4
|
+
create_table :foobars, :force => true do |t|
|
5
|
+
t.string :source_url, null: false
|
6
|
+
t.string :name
|
7
|
+
t.integer :how_old
|
8
|
+
t.integer :age
|
9
|
+
t.string :timezone
|
10
|
+
t.string :country_tld
|
11
|
+
t.string :city_url
|
12
|
+
t.string :account_url
|
13
|
+
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
create_table :cities, :force => true do |t|
|
18
|
+
t.string :source_url, null: false
|
19
|
+
t.string :name
|
20
|
+
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
|
24
|
+
create_table :defined_sources, :force => true do |t|
|
25
|
+
t.integer :source_id, null: false
|
26
|
+
t.string :name
|
27
|
+
|
28
|
+
t.timestamps
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -19,9 +19,24 @@ RSpec.configure do |config|
|
|
19
19
|
config.run_all_when_everything_filtered = true
|
20
20
|
config.raise_errors_for_deprecations!
|
21
21
|
|
22
|
+
config.before(:each) do
|
23
|
+
Materialist.reset_configuration!
|
24
|
+
|
25
|
+
# clear database
|
26
|
+
ActiveRecord::Base.descendants.each(&:delete_all)
|
27
|
+
end
|
28
|
+
|
22
29
|
# Run specs in random order to surface order dependencies. If you find an
|
23
30
|
# order dependency and want to debug it, you can fix the order by providing
|
24
31
|
# the seed, which is printed after each run.
|
25
32
|
# --seed 1234
|
26
33
|
config.order = 'random'
|
27
34
|
end
|
35
|
+
|
36
|
+
# active record
|
37
|
+
require 'active_record'
|
38
|
+
|
39
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
40
|
+
|
41
|
+
load File.dirname(__FILE__) + '/schema.rb'
|
42
|
+
load File.dirname(__FILE__) + '/models.rb'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: materialist
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mo Valipour
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-02-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sidekiq
|
@@ -52,6 +52,34 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activerecord
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: dotenv
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -170,6 +198,7 @@ files:
|
|
170
198
|
- RELEASE_NOTES.md
|
171
199
|
- Rakefile
|
172
200
|
- appraise
|
201
|
+
- lib/configuration.rb
|
173
202
|
- lib/materialist.rb
|
174
203
|
- lib/materialist/errors.rb
|
175
204
|
- lib/materialist/event_handler.rb
|
@@ -181,6 +210,8 @@ files:
|
|
181
210
|
- spec/materialist/event_worker_spec.rb
|
182
211
|
- spec/materialist/materialized_record_spec.rb
|
183
212
|
- spec/materialist/materializer_spec.rb
|
213
|
+
- spec/models.rb
|
214
|
+
- spec/schema.rb
|
184
215
|
- spec/spec_helper.rb
|
185
216
|
- spec/support/uses_redis.rb
|
186
217
|
homepage: http://github.com/deliveroo/materialist
|
@@ -212,5 +243,7 @@ test_files:
|
|
212
243
|
- spec/materialist/event_worker_spec.rb
|
213
244
|
- spec/materialist/materialized_record_spec.rb
|
214
245
|
- spec/materialist/materializer_spec.rb
|
246
|
+
- spec/models.rb
|
247
|
+
- spec/schema.rb
|
215
248
|
- spec/spec_helper.rb
|
216
249
|
- spec/support/uses_redis.rb
|