materialist 3.0.0 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -2
- data/lib/materialist/event_handler.rb +20 -12
- data/lib/materialist/event_worker.rb +5 -35
- data/lib/materialist/materializer/internals/class_methods.rb +18 -0
- data/lib/materialist/materializer/internals/dsl.rb +56 -0
- data/lib/materialist/materializer/internals/field_mapping.rb +14 -0
- data/lib/materialist/materializer/internals/link_href_mapping.rb +14 -0
- data/lib/materialist/materializer/internals/link_mapping.rb +14 -0
- data/lib/materialist/materializer/internals/materializer.rb +166 -0
- data/lib/materialist/materializer/internals.rb +13 -0
- data/lib/materialist/materializer.rb +1 -248
- data/lib/materialist/materializer_factory.rb +9 -0
- data/lib/materialist/workers/event.rb +43 -0
- data/lib/materialist.rb +1 -1
- data/spec/materialist/event_handler_spec.rb +30 -32
- data/spec/materialist/materializer_factory_spec.rb +24 -0
- data/spec/materialist/materializer_spec.rb +49 -19
- data/spec/materialist/{event_worker_spec.rb → workers/event_spec.rb} +2 -2
- metadata +16 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99e85711c6de342809785430291c11d6a11b3379
|
4
|
+
data.tar.gz: d6b3f3bfecd9ce4b20ee699c3a75028294f7ab5f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 498ac330b0331c78ab2ce69aa71dbc0c3ddb0e5277711c0f9f1b4c836f4c810c9ae1f9df4e46c2a83ba1690817fe07cc97d694f10f3718728619c26719fd4ffe
|
7
|
+
data.tar.gz: d5e784ae7827bdc7defff614a3d47601efc4035f965e679c9ea4ad639ebc558440c1740556fead09f6788f66f3045b9ab9b037c822101ed95a41b6147263d986
|
data/README.md
CHANGED
@@ -118,7 +118,7 @@ map '/events' do
|
|
118
118
|
end
|
119
119
|
```
|
120
120
|
|
121
|
-
|
121
|
+
#### DSL
|
122
122
|
|
123
123
|
Next you would need to define a materializer for each of the topic. The name of
|
124
124
|
the materializer class should match the topic name (in singular)
|
@@ -131,6 +131,8 @@ require 'materialist/materializer'
|
|
131
131
|
class ZoneMaterializer
|
132
132
|
include Materialist::Materializer
|
133
133
|
|
134
|
+
sidekiq_options queue: :orderweb_service, retry: false
|
135
|
+
|
134
136
|
persist_to :zone
|
135
137
|
|
136
138
|
source_key :source_id do |url|
|
@@ -156,12 +158,17 @@ end
|
|
156
158
|
|
157
159
|
Here is what each part of the DSL mean:
|
158
160
|
|
161
|
+
#### `sidekiq_options <options>`
|
162
|
+
allows to override options for the Sidekiq job which does the materialization.
|
163
|
+
Typically it will specify which queue to put the job on or how many times
|
164
|
+
should the job try to retry. These options override the options specified in
|
165
|
+
`Materialist.configuration.sidekiq_options`.
|
166
|
+
|
159
167
|
#### `persist_to <model_name>`
|
160
168
|
describes the name of the active record model to be used.
|
161
169
|
If missing, materialist skips materialising the resource itself, but will continue
|
162
170
|
with any other functionality -- such as `materialize_link`.
|
163
171
|
|
164
|
-
|
165
172
|
#### `source_key <column> <url_parser_block> (default: url)`
|
166
173
|
describes the column used to persist the unique identifier parsed from the url_parser_block.
|
167
174
|
By default the column used is `:source_url` and the original `url` is used as the identifier.
|
@@ -1,36 +1,44 @@
|
|
1
1
|
require 'active_support/inflector'
|
2
|
-
require_relative './
|
2
|
+
require_relative './workers/event'
|
3
|
+
require_relative './materializer_factory'
|
3
4
|
|
4
5
|
module Materialist
|
5
6
|
class EventHandler
|
6
7
|
|
7
8
|
DEFAULT_SIDEKIQ_OPTIONS = { retry: 10 }.freeze
|
8
9
|
|
9
|
-
def initialize
|
10
|
-
end
|
11
|
-
|
12
10
|
def on_events_received(batch)
|
13
|
-
batch.each { |event| call(event) if should_materialize?(event
|
11
|
+
batch.each { |event| call(event) if should_materialize?(topic(event)) }
|
14
12
|
end
|
15
13
|
|
16
14
|
def call(event)
|
17
|
-
worker.perform_async(event)
|
15
|
+
worker(topic(event)).perform_async(event)
|
18
16
|
end
|
19
17
|
|
20
18
|
private
|
21
19
|
|
22
|
-
|
20
|
+
def topic(event)
|
21
|
+
event['topic'].to_s
|
22
|
+
end
|
23
23
|
|
24
24
|
def should_materialize?(topic)
|
25
|
-
Materialist.configuration.topics.include?(topic
|
25
|
+
Materialist.configuration.topics.include?(topic)
|
26
|
+
end
|
27
|
+
|
28
|
+
def sidekiq_options(topic)
|
29
|
+
[
|
30
|
+
DEFAULT_SIDEKIQ_OPTIONS,
|
31
|
+
Materialist.configuration.sidekiq_options,
|
32
|
+
materializer_sidekiq_options(topic)
|
33
|
+
].inject(:merge)
|
26
34
|
end
|
27
35
|
|
28
|
-
def
|
29
|
-
|
36
|
+
def worker(topic)
|
37
|
+
Materialist::Workers::Event.set(sidekiq_options(topic))
|
30
38
|
end
|
31
39
|
|
32
|
-
def
|
33
|
-
Materialist::
|
40
|
+
def materializer_sidekiq_options(topic)
|
41
|
+
Materialist::MaterializerFactory.class_from_topic(topic)._sidekiq_options
|
34
42
|
end
|
35
43
|
end
|
36
44
|
end
|
@@ -1,41 +1,11 @@
|
|
1
|
-
|
2
|
-
require 'active_support/inflector'
|
1
|
+
require_relative 'workers/event'
|
3
2
|
|
3
|
+
# This class is here for backwards compatibility with pre 3.1 versions. It can be removed with the
|
4
|
+
# next major version (4.0)
|
4
5
|
module Materialist
|
5
|
-
class EventWorker
|
6
|
-
include Sidekiq::Worker
|
7
|
-
|
6
|
+
class EventWorker < Workers::Event
|
8
7
|
def perform(event)
|
9
|
-
|
10
|
-
action = event['type'].to_sym
|
11
|
-
timestamp = event['t']
|
12
|
-
|
13
|
-
materializer = "#{topic.to_s.singularize.classify}Materializer".constantize
|
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
|
-
)
|
8
|
+
super(event)
|
39
9
|
end
|
40
10
|
end
|
41
11
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Materialist
|
2
|
+
module Materializer
|
3
|
+
module Internals
|
4
|
+
module ClassMethods
|
5
|
+
attr_reader :__materialist_options, :__materialist_dsl_mapping_stack
|
6
|
+
|
7
|
+
def perform(url, action)
|
8
|
+
materializer = Materializer.new(url, self)
|
9
|
+
action == :delete ? materializer.destroy : materializer.upsert
|
10
|
+
end
|
11
|
+
|
12
|
+
def _sidekiq_options
|
13
|
+
__materialist_options[:sidekiq_options] || {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Materialist
|
2
|
+
module Materializer
|
3
|
+
module Internals
|
4
|
+
module DSL
|
5
|
+
def materialize_link(key, topic: key)
|
6
|
+
__materialist_options[:links_to_materialize][key] = { topic: topic }
|
7
|
+
end
|
8
|
+
|
9
|
+
def capture(key, as: key)
|
10
|
+
__materialist_dsl_mapping_stack.last << FieldMapping.new(key: key, as: as)
|
11
|
+
end
|
12
|
+
|
13
|
+
def capture_link_href(key, as:)
|
14
|
+
__materialist_dsl_mapping_stack.last << LinkHrefMapping.new(key: key, as: as)
|
15
|
+
end
|
16
|
+
|
17
|
+
def link(key)
|
18
|
+
link_mapping = LinkMapping.new(key: key)
|
19
|
+
__materialist_dsl_mapping_stack.last << link_mapping
|
20
|
+
__materialist_dsl_mapping_stack << link_mapping.mapping
|
21
|
+
yield
|
22
|
+
__materialist_dsl_mapping_stack.pop
|
23
|
+
end
|
24
|
+
|
25
|
+
def persist_to(klass)
|
26
|
+
__materialist_options[:model_class] = klass
|
27
|
+
end
|
28
|
+
|
29
|
+
def sidekiq_options(options)
|
30
|
+
__materialist_options[:sidekiq_options] = options
|
31
|
+
end
|
32
|
+
|
33
|
+
def source_key(key, &url_parser_block)
|
34
|
+
__materialist_options[:source_key] = key
|
35
|
+
__materialist_options[:url_parser] = url_parser_block
|
36
|
+
end
|
37
|
+
|
38
|
+
def before_upsert(*method_array)
|
39
|
+
__materialist_options[:before_upsert] = method_array
|
40
|
+
end
|
41
|
+
|
42
|
+
def after_upsert(*method_array)
|
43
|
+
__materialist_options[:after_upsert] = method_array
|
44
|
+
end
|
45
|
+
|
46
|
+
def after_destroy(*method_array)
|
47
|
+
__materialist_options[:after_destroy] = method_array
|
48
|
+
end
|
49
|
+
|
50
|
+
def before_destroy(*method_array)
|
51
|
+
__materialist_options[:before_destroy] = method_array
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'routemaster/api_client'
|
2
|
+
require_relative '../../workers/event'
|
3
|
+
|
4
|
+
module Materialist
|
5
|
+
module Materializer
|
6
|
+
module Internals
|
7
|
+
class Materializer
|
8
|
+
def initialize(url, klass)
|
9
|
+
@url = url
|
10
|
+
@instance = klass.new
|
11
|
+
@options = klass.__materialist_options
|
12
|
+
end
|
13
|
+
|
14
|
+
def upsert(retry_on_race_condition: true)
|
15
|
+
return unless root_resource
|
16
|
+
|
17
|
+
if materialize_self?
|
18
|
+
upsert_record.tap do |entity|
|
19
|
+
send_messages(after_upsert, entity) unless after_upsert.nil?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
materialize_links
|
24
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
|
25
|
+
# when there is a race condition and uniqueness of :source_url
|
26
|
+
# is enforced by database index, this error is raised
|
27
|
+
# so we simply try upsert again
|
28
|
+
# if error is due to another type of uniqueness constraint
|
29
|
+
# second call will also fail and error would bubble up
|
30
|
+
retry_on_race_condition ?
|
31
|
+
upsert(retry_on_race_condition: false) :
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
|
35
|
+
def destroy
|
36
|
+
return unless materialize_self?
|
37
|
+
model_class.find_by(source_lookup(url)).tap do |entity|
|
38
|
+
send_messages(before_destroy, entity) unless before_destroy.nil?
|
39
|
+
entity.destroy!.tap do |entity|
|
40
|
+
send_messages(after_destroy, entity) unless after_destroy.nil?
|
41
|
+
end if entity
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :url, :instance, :options
|
48
|
+
|
49
|
+
def materialize_self?
|
50
|
+
options.include? :model_class
|
51
|
+
end
|
52
|
+
|
53
|
+
def upsert_record
|
54
|
+
model_class.find_or_initialize_by(source_lookup(url)).tap do |entity|
|
55
|
+
send_messages(before_upsert, entity) unless before_upsert.nil?
|
56
|
+
entity.update_attributes! attributes
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def materialize_links
|
61
|
+
(options[:links_to_materialize] || [])
|
62
|
+
.each { |key, opts| materialize_link(key, opts) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def materialize_link(key, opts)
|
66
|
+
return unless root_resource.body._links.include?(key)
|
67
|
+
|
68
|
+
# this can't happen asynchronously
|
69
|
+
# because the handler options are unavailable in this context
|
70
|
+
# :(
|
71
|
+
::Materialist::Workers::Event.new.perform({
|
72
|
+
'topic' => opts[:topic],
|
73
|
+
'url' => root_resource.body._links[key].href,
|
74
|
+
'type' => 'noop'
|
75
|
+
})
|
76
|
+
end
|
77
|
+
|
78
|
+
def mapping
|
79
|
+
options.fetch :mapping
|
80
|
+
end
|
81
|
+
|
82
|
+
def before_upsert
|
83
|
+
options[:before_upsert]
|
84
|
+
end
|
85
|
+
|
86
|
+
def after_upsert
|
87
|
+
options[:after_upsert]
|
88
|
+
end
|
89
|
+
|
90
|
+
def before_destroy
|
91
|
+
options[:before_destroy]
|
92
|
+
end
|
93
|
+
|
94
|
+
def after_destroy
|
95
|
+
options[:after_destroy]
|
96
|
+
end
|
97
|
+
|
98
|
+
def model_class
|
99
|
+
options.fetch(:model_class).to_s.camelize.constantize
|
100
|
+
end
|
101
|
+
|
102
|
+
def source_key
|
103
|
+
options.fetch(:source_key, :source_url)
|
104
|
+
end
|
105
|
+
|
106
|
+
def url_parser
|
107
|
+
options[:url_parser] || ->url { url }
|
108
|
+
end
|
109
|
+
|
110
|
+
def source_lookup(url)
|
111
|
+
@_source_lookup ||= { source_key => url_parser.call(url) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def attributes
|
115
|
+
build_attributes root_resource, mapping
|
116
|
+
end
|
117
|
+
|
118
|
+
def root_resource
|
119
|
+
@_root_resource ||= resource_at(url)
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_attributes(resource, mapping)
|
123
|
+
return {} unless resource
|
124
|
+
|
125
|
+
mapping.inject({}) do |result, m|
|
126
|
+
case m
|
127
|
+
when FieldMapping
|
128
|
+
result.tap { |r| r[m.as] = resource.body[m.key] }
|
129
|
+
when LinkHrefMapping
|
130
|
+
result.tap do |r|
|
131
|
+
if resource.body._links.include?(m.key)
|
132
|
+
r[m.as] = resource.body._links[m.key].href
|
133
|
+
end
|
134
|
+
end
|
135
|
+
when LinkMapping
|
136
|
+
resource.body._links.include?(m.key) ?
|
137
|
+
result.merge(build_attributes(resource_at(resource.send(m.key).url), m.mapping || [])) :
|
138
|
+
result
|
139
|
+
else
|
140
|
+
result
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def resource_at(url)
|
146
|
+
api_client.get(url, options: { enable_caching: false })
|
147
|
+
rescue Routemaster::Errors::ResourceNotFound
|
148
|
+
# this is due to a race condition between an upsert event
|
149
|
+
# and a :delete event
|
150
|
+
# when this happens we should silently ignore the case
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
|
154
|
+
def api_client
|
155
|
+
@_api_client ||= Routemaster::APIClient.new(
|
156
|
+
response_class: Routemaster::Responses::HateoasResponse
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
160
|
+
def send_messages(messages, arguments)
|
161
|
+
messages.each { |message| instance.send(message, arguments) }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative './internals/field_mapping'
|
2
|
+
require_relative './internals/link_mapping'
|
3
|
+
require_relative './internals/link_href_mapping'
|
4
|
+
require_relative './internals/class_methods'
|
5
|
+
require_relative './internals/dsl'
|
6
|
+
require_relative './internals/materializer'
|
7
|
+
|
8
|
+
module Materialist
|
9
|
+
module Materializer
|
10
|
+
module Internals
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,6 +1,4 @@
|
|
1
|
-
|
2
|
-
require 'routemaster/api_client'
|
3
|
-
require_relative './event_worker'
|
1
|
+
require_relative './materializer/internals'
|
4
2
|
|
5
3
|
module Materialist
|
6
4
|
module Materializer
|
@@ -16,250 +14,5 @@ module Materialist
|
|
16
14
|
})
|
17
15
|
base.instance_variable_set(:@__materialist_dsl_mapping_stack, [root_mapping])
|
18
16
|
end
|
19
|
-
|
20
|
-
module Internals
|
21
|
-
class FieldMapping
|
22
|
-
def initialize(key:, as:)
|
23
|
-
@key = key
|
24
|
-
@as = as
|
25
|
-
end
|
26
|
-
|
27
|
-
attr_reader :key, :as
|
28
|
-
end
|
29
|
-
|
30
|
-
class LinkMapping
|
31
|
-
def initialize(key:)
|
32
|
-
@key = key
|
33
|
-
@mapping = []
|
34
|
-
end
|
35
|
-
|
36
|
-
attr_reader :key, :mapping
|
37
|
-
end
|
38
|
-
|
39
|
-
class LinkHrefMapping
|
40
|
-
def initialize(key:, as:)
|
41
|
-
@key = key
|
42
|
-
@as = as
|
43
|
-
end
|
44
|
-
|
45
|
-
attr_reader :key, :as
|
46
|
-
end
|
47
|
-
|
48
|
-
module ClassMethods
|
49
|
-
attr_reader :__materialist_options, :__materialist_dsl_mapping_stack
|
50
|
-
|
51
|
-
def perform(url, action)
|
52
|
-
materializer = Materializer.new(url, self)
|
53
|
-
action == :delete ? materializer.destroy : materializer.upsert
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
module DSL
|
58
|
-
|
59
|
-
def materialize_link(key, topic: key)
|
60
|
-
__materialist_options[:links_to_materialize][key] = { topic: topic }
|
61
|
-
end
|
62
|
-
|
63
|
-
def capture(key, as: key)
|
64
|
-
__materialist_dsl_mapping_stack.last << FieldMapping.new(key: key, as: as)
|
65
|
-
end
|
66
|
-
|
67
|
-
def capture_link_href(key, as:)
|
68
|
-
__materialist_dsl_mapping_stack.last << LinkHrefMapping.new(key: key, as: as)
|
69
|
-
end
|
70
|
-
|
71
|
-
def link(key)
|
72
|
-
link_mapping = LinkMapping.new(key: key)
|
73
|
-
__materialist_dsl_mapping_stack.last << link_mapping
|
74
|
-
__materialist_dsl_mapping_stack << link_mapping.mapping
|
75
|
-
yield
|
76
|
-
__materialist_dsl_mapping_stack.pop
|
77
|
-
end
|
78
|
-
|
79
|
-
def persist_to(klass)
|
80
|
-
__materialist_options[:model_class] = klass
|
81
|
-
end
|
82
|
-
|
83
|
-
def source_key(key, &url_parser_block)
|
84
|
-
__materialist_options[:source_key] = key
|
85
|
-
__materialist_options[:url_parser] = url_parser_block
|
86
|
-
end
|
87
|
-
|
88
|
-
def before_upsert(*method_array)
|
89
|
-
__materialist_options[:before_upsert] = method_array
|
90
|
-
end
|
91
|
-
|
92
|
-
def after_upsert(*method_array)
|
93
|
-
__materialist_options[:after_upsert] = method_array
|
94
|
-
end
|
95
|
-
|
96
|
-
def after_destroy(*method_array)
|
97
|
-
__materialist_options[:after_destroy] = method_array
|
98
|
-
end
|
99
|
-
|
100
|
-
def before_destroy(*method_array)
|
101
|
-
__materialist_options[:before_destroy] = method_array
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
class Materializer
|
106
|
-
|
107
|
-
def initialize(url, klass)
|
108
|
-
@url = url
|
109
|
-
@instance = klass.new
|
110
|
-
@options = klass.__materialist_options
|
111
|
-
end
|
112
|
-
|
113
|
-
def upsert(retry_on_race_condition: true)
|
114
|
-
return unless root_resource
|
115
|
-
|
116
|
-
if materialize_self?
|
117
|
-
upsert_record.tap do |entity|
|
118
|
-
send_messages(after_upsert, entity) unless after_upsert.nil?
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
materialize_links
|
123
|
-
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
|
124
|
-
# when there is a race condition and uniqueness of :source_url
|
125
|
-
# is enforced by database index, this error is raised
|
126
|
-
# so we simply try upsert again
|
127
|
-
# if error is due to another type of uniqueness constraint
|
128
|
-
# second call will also fail and error would bubble up
|
129
|
-
retry_on_race_condition ?
|
130
|
-
upsert(retry_on_race_condition: false) :
|
131
|
-
raise
|
132
|
-
end
|
133
|
-
|
134
|
-
def destroy
|
135
|
-
return unless materialize_self?
|
136
|
-
model_class.find_by(source_lookup(url)).tap do |entity|
|
137
|
-
send_messages(before_destroy, entity) unless before_destroy.nil?
|
138
|
-
entity.destroy!.tap do |entity|
|
139
|
-
send_messages(after_destroy, entity) unless after_destroy.nil?
|
140
|
-
end if entity
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
private
|
145
|
-
|
146
|
-
attr_reader :url, :instance, :options
|
147
|
-
|
148
|
-
def materialize_self?
|
149
|
-
options.include? :model_class
|
150
|
-
end
|
151
|
-
|
152
|
-
def upsert_record
|
153
|
-
model_class.find_or_initialize_by(source_lookup(url)).tap do |entity|
|
154
|
-
send_messages(before_upsert, entity) unless before_upsert.nil?
|
155
|
-
entity.update_attributes! attributes
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def materialize_links
|
160
|
-
(options[:links_to_materialize] || [])
|
161
|
-
.each { |key, opts| materialize_link(key, opts) }
|
162
|
-
end
|
163
|
-
|
164
|
-
def materialize_link(key, opts)
|
165
|
-
return unless root_resource.body._links.include?(key)
|
166
|
-
|
167
|
-
# this can't happen asynchronously
|
168
|
-
# because the handler options are unavailable in this context
|
169
|
-
# :(
|
170
|
-
::Materialist::EventWorker.new.perform({
|
171
|
-
'topic' => opts[:topic],
|
172
|
-
'url' => root_resource.body._links[key].href,
|
173
|
-
'type' => 'noop'
|
174
|
-
})
|
175
|
-
end
|
176
|
-
|
177
|
-
def mapping
|
178
|
-
options.fetch :mapping
|
179
|
-
end
|
180
|
-
|
181
|
-
def before_upsert
|
182
|
-
options[:before_upsert]
|
183
|
-
end
|
184
|
-
|
185
|
-
def after_upsert
|
186
|
-
options[:after_upsert]
|
187
|
-
end
|
188
|
-
|
189
|
-
def before_destroy
|
190
|
-
options[:before_destroy]
|
191
|
-
end
|
192
|
-
|
193
|
-
def after_destroy
|
194
|
-
options[:after_destroy]
|
195
|
-
end
|
196
|
-
|
197
|
-
def model_class
|
198
|
-
options.fetch(:model_class).to_s.camelize.constantize
|
199
|
-
end
|
200
|
-
|
201
|
-
def source_key
|
202
|
-
options.fetch(:source_key, :source_url)
|
203
|
-
end
|
204
|
-
|
205
|
-
def url_parser
|
206
|
-
options[:url_parser] || ->url { url }
|
207
|
-
end
|
208
|
-
|
209
|
-
def source_lookup(url)
|
210
|
-
@_source_lookup ||= { source_key => url_parser.call(url) }
|
211
|
-
end
|
212
|
-
|
213
|
-
def attributes
|
214
|
-
build_attributes root_resource, mapping
|
215
|
-
end
|
216
|
-
|
217
|
-
def root_resource
|
218
|
-
@_root_resource ||= resource_at(url)
|
219
|
-
end
|
220
|
-
|
221
|
-
def build_attributes(resource, mapping)
|
222
|
-
return {} unless resource
|
223
|
-
|
224
|
-
mapping.inject({}) do |result, m|
|
225
|
-
case m
|
226
|
-
when FieldMapping
|
227
|
-
result.tap { |r| r[m.as] = resource.body[m.key] }
|
228
|
-
when LinkHrefMapping
|
229
|
-
result.tap do |r|
|
230
|
-
if resource.body._links.include?(m.key)
|
231
|
-
r[m.as] = resource.body._links[m.key].href
|
232
|
-
end
|
233
|
-
end
|
234
|
-
when LinkMapping
|
235
|
-
resource.body._links.include?(m.key) ?
|
236
|
-
result.merge(build_attributes(resource_at(resource.send(m.key).url), m.mapping || [])) :
|
237
|
-
result
|
238
|
-
else
|
239
|
-
result
|
240
|
-
end
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
def resource_at(url)
|
245
|
-
api_client.get(url, options: { enable_caching: false })
|
246
|
-
rescue Routemaster::Errors::ResourceNotFound
|
247
|
-
# this is due to a race condition between an upsert event
|
248
|
-
# and a :delete event
|
249
|
-
# when this happens we should silently ignore the case
|
250
|
-
nil
|
251
|
-
end
|
252
|
-
|
253
|
-
def api_client
|
254
|
-
@_api_client ||= Routemaster::APIClient.new(
|
255
|
-
response_class: Routemaster::Responses::HateoasResponse
|
256
|
-
)
|
257
|
-
end
|
258
|
-
|
259
|
-
def send_messages(messages, arguments)
|
260
|
-
messages.each { |message| instance.send(message, arguments) }
|
261
|
-
end
|
262
|
-
end
|
263
|
-
end
|
264
17
|
end
|
265
18
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require_relative '../materializer_factory'
|
3
|
+
|
4
|
+
module Materialist
|
5
|
+
module Workers
|
6
|
+
class Event
|
7
|
+
include Sidekiq::Worker
|
8
|
+
|
9
|
+
def perform(event)
|
10
|
+
topic = event['topic']
|
11
|
+
action = event['type'].to_sym
|
12
|
+
timestamp = event['t']
|
13
|
+
|
14
|
+
materializer = Materialist::MaterializerFactory.class_from_topic(topic)
|
15
|
+
materializer.perform(event['url'], action)
|
16
|
+
|
17
|
+
report_latency(topic, timestamp) if timestamp
|
18
|
+
report_stats(topic, action, :success)
|
19
|
+
rescue
|
20
|
+
report_stats(topic, action, :failure)
|
21
|
+
raise
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def report_latency(topic, timestamp)
|
27
|
+
t = (Time.now.to_f - (timestamp.to_i / 1e3)).round(1)
|
28
|
+
Materialist.configuration.metrics_client.histogram(
|
29
|
+
"materialist.event_latency",
|
30
|
+
t,
|
31
|
+
tags: ["topic:#{topic}"]
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def report_stats(topic, action, kind)
|
36
|
+
Materialist.configuration.metrics_client.increment(
|
37
|
+
"materialist.event_worker.#{kind}",
|
38
|
+
tags: ["action:#{action}", "topic:#{topic}"]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/materialist.rb
CHANGED
@@ -1,20 +1,23 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'materialist/event_handler'
|
3
|
-
require 'materialist/
|
3
|
+
require 'materialist/workers/event'
|
4
4
|
|
5
5
|
RSpec.describe Materialist::EventHandler do
|
6
|
+
let(:configuration) do
|
7
|
+
OpenStruct.new({
|
8
|
+
sidekiq_options: sidekiq_options,
|
9
|
+
topics: topics
|
10
|
+
})
|
11
|
+
end
|
6
12
|
let(:topics) {[]}
|
7
13
|
let(:sidekiq_options) {{}}
|
14
|
+
let(:materializer_class) { double(_sidekiq_options: {}) }
|
8
15
|
let(:worker_double) { double() }
|
9
16
|
|
10
17
|
before do
|
11
|
-
Materialist.
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
allow(Materialist::EventWorker).to receive(:set)
|
17
|
-
.and_return worker_double
|
18
|
+
allow(Materialist).to receive(:configuration).and_return(configuration)
|
19
|
+
allow(Materialist::MaterializerFactory).to receive(:class_from_topic).and_return(materializer_class)
|
20
|
+
allow(Materialist::Workers::Event).to receive(:set).and_return(worker_double)
|
18
21
|
end
|
19
22
|
|
20
23
|
describe "#on_events_received" do
|
@@ -50,35 +53,30 @@ RSpec.describe Materialist::EventHandler do
|
|
50
53
|
end
|
51
54
|
|
52
55
|
describe "#call" do
|
53
|
-
|
54
|
-
let(:perform) { subject.call event }
|
56
|
+
subject { described_class.new.call(event) }
|
55
57
|
|
56
|
-
|
57
|
-
|
58
|
-
|
58
|
+
let(:configuration) do
|
59
|
+
OpenStruct.new({
|
60
|
+
sidekiq_options: { unique: false, retry: 5 }
|
61
|
+
})
|
59
62
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
63
|
+
let(:event) { { "topic" => topic, "data" => "data" } }
|
64
|
+
let(:topic) { "foobar" }
|
65
|
+
let(:materializer_class) { double(_sidekiq_options: materializer_sidekiq_options) }
|
66
|
+
let(:materializer_sidekiq_options) { { queue: :dedicated, retry: 2 } }
|
67
|
+
let(:expected_event_options) { { queue: :dedicated, retry: 2, unique: false } }
|
68
|
+
let(:worker_class) { double(perform_async: nil) }
|
69
|
+
|
70
|
+
before do
|
71
|
+
allow(Materialist).to receive(:configuration).and_return(configuration)
|
72
|
+
allow(Materialist::MaterializerFactory).to receive(:class_from_topic).with(topic).and_return(materializer_class)
|
73
|
+
allow(Materialist::Workers::Event).to receive(:set).with(expected_event_options).and_return(worker_class)
|
71
74
|
end
|
72
75
|
|
73
|
-
|
74
|
-
|
76
|
+
it "enqueues the event worker with sidekiq options merged from configuration, default and the materializer" do
|
77
|
+
subject
|
75
78
|
|
76
|
-
|
77
|
-
expect(Materialist::EventWorker).to receive(:set)
|
78
|
-
.with(retry: false)
|
79
|
-
expect(worker_double).to receive(:perform_async).with(event)
|
80
|
-
perform
|
81
|
-
end
|
79
|
+
expect(worker_class).to have_received(:perform_async).with(event)
|
82
80
|
end
|
83
81
|
end
|
84
82
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'materialist/materializer_factory'
|
3
|
+
|
4
|
+
RSpec.describe Materialist::MaterializerFactory do
|
5
|
+
class BuffaloMozarellaMaterializer; end
|
6
|
+
|
7
|
+
describe '.class_from_topic' do
|
8
|
+
subject { described_class.class_from_topic(topic) }
|
9
|
+
|
10
|
+
context 'when the materializer class exists' do
|
11
|
+
let(:topic) { 'buffalo_mozarella'}
|
12
|
+
|
13
|
+
it 'returns the class' do
|
14
|
+
is_expected.to eql(BuffaloMozarellaMaterializer)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when the materializer class does not exist' do
|
19
|
+
let(:topic) { 'cheddar' }
|
20
|
+
|
21
|
+
it { is_expected.to be nil }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -5,7 +5,7 @@ require 'materialist/materializer'
|
|
5
5
|
RSpec.describe Materialist::Materializer do
|
6
6
|
uses_redis
|
7
7
|
|
8
|
-
describe "
|
8
|
+
describe ".perform" do
|
9
9
|
let!(:materializer_class) do
|
10
10
|
FoobarMaterializer = Class.new do
|
11
11
|
include Materialist::Materializer
|
@@ -71,6 +71,7 @@ RSpec.describe Materialist::Materializer do
|
|
71
71
|
|
72
72
|
let(:action) { :create }
|
73
73
|
let(:perform) { materializer_class.perform(source_url, action) }
|
74
|
+
let(:actions_called) { materializer_class.class_variable_get(:@@actions_called) }
|
74
75
|
|
75
76
|
it "materializes record in db" do
|
76
77
|
expect{perform}.to change{Foobar.count}.by 1
|
@@ -168,18 +169,17 @@ RSpec.describe Materialist::Materializer do
|
|
168
169
|
|
169
170
|
context "when {after, before}_upsert is configured" do
|
170
171
|
let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
|
171
|
-
let(:actions_called) { materializer_class.class_variable_get(:@@actions_called) }
|
172
172
|
let!(:materializer_class) do
|
173
173
|
FoobarMaterializer = Class.new do
|
174
174
|
include Materialist::Materializer
|
175
|
-
|
175
|
+
cattr_accessor(:actions_called) { {} }
|
176
176
|
|
177
177
|
persist_to :foobar
|
178
178
|
before_upsert :before_hook
|
179
179
|
after_upsert :after_hook
|
180
180
|
|
181
|
-
def before_hook(entity);
|
182
|
-
def after_hook(entity);
|
181
|
+
def before_hook(entity); self.actions_called[:before_hook] = true; end
|
182
|
+
def after_hook(entity); self.actions_called[:after_hook] = true; end
|
183
183
|
end
|
184
184
|
end
|
185
185
|
|
@@ -198,16 +198,16 @@ RSpec.describe Materialist::Materializer do
|
|
198
198
|
let(:materializer_class) do
|
199
199
|
FoobarMaterializer = Class.new do
|
200
200
|
include Materialist::Materializer
|
201
|
-
|
201
|
+
cattr_accessor(:actions_called) { {} }
|
202
202
|
|
203
203
|
persist_to :foobar
|
204
204
|
before_upsert :before_hook, :before_hook2
|
205
205
|
after_upsert :after_hook, :after_hook2
|
206
206
|
|
207
|
-
def before_hook(entity);
|
208
|
-
def before_hook2(entity);
|
209
|
-
def after_hook(entity);
|
210
|
-
def after_hook2(entity);
|
207
|
+
def before_hook(entity); self.actions_called[:before_hook] = true; end
|
208
|
+
def before_hook2(entity); self.actions_called[:before_hook2] = true; end
|
209
|
+
def after_hook(entity); self.actions_called[:after_hook] = true; end
|
210
|
+
def after_hook2(entity); self.actions_called[:after_hook2] = true; end
|
211
211
|
end
|
212
212
|
end
|
213
213
|
|
@@ -237,18 +237,17 @@ RSpec.describe Materialist::Materializer do
|
|
237
237
|
|
238
238
|
context "when {before, after}_destroy is configured" do
|
239
239
|
let!(:record) { Foobar.create!(source_url: source_url, name: 'mo') }
|
240
|
-
let(:actions_called) { materializer_class.class_variable_get(:@@actions_called) }
|
241
240
|
let!(:materializer_class) do
|
242
241
|
FoobarMaterializer = Class.new do
|
243
242
|
include Materialist::Materializer
|
244
|
-
|
243
|
+
cattr_accessor(:actions_called) { {} }
|
245
244
|
|
246
245
|
persist_to :foobar
|
247
246
|
before_destroy :before_hook
|
248
247
|
after_destroy :after_hook
|
249
248
|
|
250
|
-
def before_hook(entity);
|
251
|
-
def after_hook(entity);
|
249
|
+
def before_hook(entity); self.actions_called[:before_hook] = true; end
|
250
|
+
def after_hook(entity); self.actions_called[:after_hook] = true; end
|
252
251
|
end
|
253
252
|
end
|
254
253
|
|
@@ -280,16 +279,16 @@ RSpec.describe Materialist::Materializer do
|
|
280
279
|
let(:materializer_class) do
|
281
280
|
FoobarMaterializer = Class.new do
|
282
281
|
include Materialist::Materializer
|
283
|
-
|
282
|
+
cattr_accessor(:actions_called) { {} }
|
284
283
|
|
285
284
|
persist_to :foobar
|
286
285
|
before_destroy :before_hook, :before_hook2
|
287
286
|
after_destroy :after_hook, :after_hook2
|
288
287
|
|
289
|
-
def before_hook(entity);
|
290
|
-
def before_hook2(entity);
|
291
|
-
def after_hook(entity);
|
292
|
-
def after_hook2(entity);
|
288
|
+
def before_hook(entity); self.actions_called[:before_hook] = true; end
|
289
|
+
def before_hook2(entity); self.actions_called[:before_hook2] = true; end
|
290
|
+
def after_hook(entity); self.actions_called[:after_hook] = true; end
|
291
|
+
def after_hook2(entity); self.actions_called[:after_hook2] = true; end
|
293
292
|
end
|
294
293
|
end
|
295
294
|
|
@@ -397,4 +396,35 @@ RSpec.describe Materialist::Materializer do
|
|
397
396
|
end
|
398
397
|
end
|
399
398
|
end
|
399
|
+
|
400
|
+
describe "._sidekiq_options" do
|
401
|
+
subject { materializer_class._sidekiq_options }
|
402
|
+
|
403
|
+
context "when sidekiq options have been set" do
|
404
|
+
let(:materializer_class) do
|
405
|
+
Class.new do
|
406
|
+
include Materialist::Materializer
|
407
|
+
|
408
|
+
sidekiq_options queue: :dedicated, option: 'value'
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
it "returns the options" do
|
414
|
+
is_expected.to eql(queue: :dedicated, option: 'value')
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
context "when sidekiq options have not been set" do
|
419
|
+
let(:materializer_class) do
|
420
|
+
Class.new do
|
421
|
+
include Materialist::Materializer
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
it "returns empty hash" do
|
426
|
+
is_expected.to eql({})
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
400
430
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'materialist/
|
2
|
+
require 'materialist/workers/event'
|
3
3
|
|
4
|
-
RSpec.describe Materialist::
|
4
|
+
RSpec.describe Materialist::Workers::Event 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' }}
|
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: 3.
|
4
|
+
version: 3.1.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-04-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sidekiq
|
@@ -205,11 +205,21 @@ files:
|
|
205
205
|
- lib/materialist/event_worker.rb
|
206
206
|
- lib/materialist/materialized_record.rb
|
207
207
|
- lib/materialist/materializer.rb
|
208
|
+
- lib/materialist/materializer/internals.rb
|
209
|
+
- lib/materialist/materializer/internals/class_methods.rb
|
210
|
+
- lib/materialist/materializer/internals/dsl.rb
|
211
|
+
- lib/materialist/materializer/internals/field_mapping.rb
|
212
|
+
- lib/materialist/materializer/internals/link_href_mapping.rb
|
213
|
+
- lib/materialist/materializer/internals/link_mapping.rb
|
214
|
+
- lib/materialist/materializer/internals/materializer.rb
|
215
|
+
- lib/materialist/materializer_factory.rb
|
216
|
+
- lib/materialist/workers/event.rb
|
208
217
|
- materialist.gemspec
|
209
218
|
- spec/materialist/event_handler_spec.rb
|
210
|
-
- spec/materialist/event_worker_spec.rb
|
211
219
|
- spec/materialist/materialized_record_spec.rb
|
220
|
+
- spec/materialist/materializer_factory_spec.rb
|
212
221
|
- spec/materialist/materializer_spec.rb
|
222
|
+
- spec/materialist/workers/event_spec.rb
|
213
223
|
- spec/models.rb
|
214
224
|
- spec/schema.rb
|
215
225
|
- spec/spec_helper.rb
|
@@ -234,15 +244,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
234
244
|
version: '0'
|
235
245
|
requirements: []
|
236
246
|
rubyforge_project:
|
237
|
-
rubygems_version: 2.6.
|
247
|
+
rubygems_version: 2.6.13
|
238
248
|
signing_key:
|
239
249
|
specification_version: 4
|
240
250
|
summary: Utilities to materialize routemaster topics
|
241
251
|
test_files:
|
242
252
|
- spec/materialist/event_handler_spec.rb
|
243
|
-
- spec/materialist/event_worker_spec.rb
|
244
253
|
- spec/materialist/materialized_record_spec.rb
|
254
|
+
- spec/materialist/materializer_factory_spec.rb
|
245
255
|
- spec/materialist/materializer_spec.rb
|
256
|
+
- spec/materialist/workers/event_spec.rb
|
246
257
|
- spec/models.rb
|
247
258
|
- spec/schema.rb
|
248
259
|
- spec/spec_helper.rb
|