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