table_sync 2.3.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/CHANGELOG.md +57 -0
  4. data/Gemfile.lock +85 -80
  5. data/README.md +4 -2
  6. data/docs/message_protocol.md +24 -0
  7. data/docs/notifications.md +45 -0
  8. data/docs/publishing.md +147 -0
  9. data/docs/receiving.md +341 -0
  10. data/lib/table_sync.rb +16 -31
  11. data/lib/table_sync/errors.rb +39 -23
  12. data/lib/table_sync/publishing.rb +11 -0
  13. data/lib/table_sync/{base_publisher.rb → publishing/base_publisher.rb} +1 -1
  14. data/lib/table_sync/{batch_publisher.rb → publishing/batch_publisher.rb} +4 -4
  15. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/active_record.rb +3 -7
  16. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/sequel.rb +2 -6
  17. data/lib/table_sync/{publisher.rb → publishing/publisher.rb} +4 -4
  18. data/lib/table_sync/receiving.rb +14 -0
  19. data/lib/table_sync/receiving/config.rb +218 -0
  20. data/lib/table_sync/receiving/config_decorator.rb +27 -0
  21. data/lib/table_sync/receiving/dsl.rb +28 -0
  22. data/lib/table_sync/receiving/handler.rb +136 -0
  23. data/lib/table_sync/{model → receiving/model}/active_record.rb +43 -36
  24. data/lib/table_sync/receiving/model/sequel.rb +83 -0
  25. data/lib/table_sync/utils.rb +9 -0
  26. data/lib/table_sync/utils/interface_checker.rb +103 -0
  27. data/lib/table_sync/utils/proc_array.rb +17 -0
  28. data/lib/table_sync/utils/proc_keywords_resolver.rb +46 -0
  29. data/lib/table_sync/version.rb +1 -1
  30. data/table_sync.gemspec +2 -1
  31. metadata +45 -33
  32. data/docs/development.md +0 -43
  33. data/docs/synopsis.md +0 -336
  34. data/lib/table_sync/config.rb +0 -105
  35. data/lib/table_sync/config/callback_registry.rb +0 -53
  36. data/lib/table_sync/config_decorator.rb +0 -38
  37. data/lib/table_sync/dsl.rb +0 -25
  38. data/lib/table_sync/event_actions.rb +0 -96
  39. data/lib/table_sync/event_actions/data_wrapper.rb +0 -7
  40. data/lib/table_sync/event_actions/data_wrapper/base.rb +0 -23
  41. data/lib/table_sync/event_actions/data_wrapper/destroy.rb +0 -19
  42. data/lib/table_sync/event_actions/data_wrapper/update.rb +0 -21
  43. data/lib/table_sync/model/sequel.rb +0 -88
  44. data/lib/table_sync/plugins.rb +0 -72
  45. data/lib/table_sync/plugins/abstract.rb +0 -55
  46. data/lib/table_sync/plugins/access_mixin.rb +0 -49
  47. data/lib/table_sync/plugins/registry.rb +0 -153
  48. data/lib/table_sync/receiving_handler.rb +0 -76
@@ -0,0 +1,147 @@
1
+ # Publishing changes
2
+
3
+ Include `TableSync.sync(self)` into a Sequel or ActiveRecord model. `:if` and `:unless` are
4
+ supported for Sequel and ActiveRecord
5
+
6
+ Functioning `Rails.cache` is required
7
+
8
+ Example:
9
+
10
+ ```ruby
11
+ class SomeModel < Sequel::Model
12
+ TableSync.sync(self, { if: -> (*) { some_code } })
13
+ end
14
+ ```
15
+
16
+ #### #attributes_for_sync
17
+
18
+ Models can implement `#attributes_for_sync` to override which attributes are published. If not
19
+ present, all attributes are published
20
+
21
+ #### #attrs_for_routing_key
22
+
23
+ Models can implement `#attrs_for_routing_key` to override which attributes are given to routing_key_callable. If not present, default attributes are given
24
+
25
+ #### #attrs_for_metadata
26
+
27
+ Models can implement `#attrs_for_metadata` to override which attributes are given to metadata_callable. If not present, default attributes are given
28
+
29
+ #### .table_sync_model_name
30
+
31
+ Models can implement `.table_sync_model_name` class method to override the model name used for
32
+ publishing events. Default is model class name
33
+
34
+ #### .table_sync_destroy_attributes(original_attributes)
35
+
36
+ Models can implement `.table_sync_destroy_attributes` class method to override the attributes
37
+ used for publishing destroy events. Default is object's primary key
38
+
39
+ ## Configuration
40
+
41
+ - `TableSync.publishing_job_class_callable` is a callable which should resolve to a ActiveJob
42
+ subclass that calls TableSync back to actually publish changes (required)
43
+
44
+ Example:
45
+
46
+ ```ruby
47
+ class TableSync::Job < ActiveJob::Base
48
+ def perform(*args)
49
+ TableSync::Publisher.new(*args).publish_now
50
+ end
51
+ end
52
+ ```
53
+
54
+ - `TableSync.batch_publishing_job_class_callable` is a callable which should resolve to a ActiveJob
55
+ subclass that calls TableSync batch publisher back to actually publish changes (required for batch publisher)
56
+
57
+ - `TableSync.routing_key_callable` is a callable which resolves which routing key to use when
58
+ publishing changes. It receives object class and attributes (required)
59
+
60
+ Example:
61
+
62
+ ```ruby
63
+ TableSync.routing_key_callable = -> (klass, attributes) { klass.gsub('::', '_').tableize }
64
+ ```
65
+
66
+ - `TableSync.routing_metadata_callable` is a callable that adds RabbitMQ headers which can be
67
+ used in routing (optional). One possible way of using it is defining a headers exchange and
68
+ routing rules based on key-value pairs (which correspond to sent headers)
69
+
70
+ Example:
71
+
72
+ ```ruby
73
+ TableSync.routing_metadata_callable = -> (klass, attributes) { attributes.slice("project_id") }
74
+ ```
75
+
76
+ - `TableSync.exchange_name` defines the exchange name used for publishing (optional, falls back
77
+ to default Rabbit gem configuration).
78
+
79
+ - `TableSync.notifier` is a module that provides publish and recieve notifications.
80
+
81
+ # Manual publishing
82
+
83
+ `TableSync::Publisher.new(object_class, original_attributes, confirm: true, state: :updated, debounce_time: 45)`
84
+ where state is one of `:created / :updated / :destroyed` and `confirm` is Rabbit's confirm delivery flag and optional param `debounce_time` determines debounce time in seconds, 1 minute by default.
85
+
86
+ # Manual publishing with batches
87
+
88
+ You can use `TableSync::BatchPublisher` to publish changes in batches (array of hashes in `attributes`).
89
+
90
+ When using `TableSync::BatchPublisher`,` TableSync.routing_key_callable` is called as follows:
91
+ `TableSync.routing_key_callable.call(klass, {})`, i.e. empty hash is passed instead of attributes.
92
+ And `TableSync.routing_metadata_callable` is not called at all: metadata is set to empty hash.
93
+
94
+ `TableSync::BatchPublisher.new(object_class, original_attributes_array, **options)`, where `original_attributes_array` is an array with hash of attributes of published objects and `options` is a hash of options.
95
+
96
+ `options` consists of:
97
+ - `confirm`, which is a flag for RabbitMQ, `true` by default
98
+ - `routing_key`, which is a custom key used (if given) to override one from `TableSync.routing_key_callable`, `nil` by default
99
+ - `push_original_attributes` (default value is `false`), if this option is set to `true`,
100
+ original_attributes_array will be pushed to Rabbit instead of fetching records from database and sending their mapped attributes.
101
+ - `headers`, which is an option for custom headers (can be used for headers exchanges routes), `nil` by default
102
+ - `event`, which is an option for event specification (`:destroy` or `:update`), `:update` by default
103
+
104
+ Example:
105
+
106
+ ```ruby
107
+ TableSync::BatchPublisher.new(
108
+ "SomeClass",
109
+ [{ id: 1 }, { id: 2 }],
110
+ confirm: false,
111
+ routing_key: "custom_routing_key",
112
+ push_original_attributes: true,
113
+ headers: { key: :value },
114
+ event: :destroy,
115
+ )
116
+ ```
117
+
118
+ # Manual publishing with batches (Russian)
119
+
120
+ С помощью класса `TableSync::BatchPublisher` вы можете опубликовать изменения батчами (массивом в `attributes`).
121
+
122
+ При использовании `TableSync::BatchPublisher`, `TableSync.routing_key_callable` вызывается следующим образом:
123
+ `TableSync.routing_key_callable.call(klass, {})`, то есть вместо аттрибутов передается пустой хэш.
124
+ А `TableSync.routing_metadata_callable` не вызывается вовсе: в метадате устанавливается пустой хэш.
125
+
126
+ `TableSync::BatchPublisher.new(object_class, original_attributes_array, **options)`, где `original_attributes_array` - массив с аттрибутами публикуемых объектов и `options`- это хэш с дополнительными опциями.
127
+
128
+ `options` состоит из:
129
+ - `confirm`, флаг для RabbitMQ, по умолчанию - `true`
130
+ - `routing_key`, ключ, который (если указан) замещает ключ, получаемый из `TableSync.routing_key_callable`, по умолчанию - `nil`
131
+ - `push_original_attributes` (значение по умолчанию `false`), если для этой опции задано значение true, в Rabbit будут отправлены original_attributes_array, вместо получения значений записей из базы непосредственно перед отправкой.
132
+ - `headers`, опция для задания headers (можно использовать для задания маршрутов в headers exchange'ах), `nil` по умолчанию
133
+ - `event`, опция для указания типа события (`:destroy` или `:update`), `:update` по умолчанию
134
+
135
+ Example:
136
+
137
+ ```ruby
138
+ TableSync::BatchPublisher.new(
139
+ "SomeClass",
140
+ [{ id: 1 }, { id: 2 }],
141
+ confirm: false,
142
+ routing_key: "custom_routing_key",
143
+ push_original_attributes: true,
144
+ headers: { key: :value },
145
+ event: :destroy,
146
+ )
147
+ ```
@@ -0,0 +1,341 @@
1
+ # Receiving changes
2
+
3
+ Naming convention for receiving handlers is `Rabbit::Handler::GROUP_ID::TableSync`,
4
+ where `GROUP_ID` represents first part of source exchange name.
5
+ Define handler class inherited from `TableSync::ReceivingHandler`
6
+ and named according to described convention.
7
+ You should use DSL inside the class.
8
+ Suppose we will synchronize models {Project, News, User} project {MainProject}, then:
9
+
10
+ ```ruby
11
+ class Rabbit::Handler::MainProject::TableSync < TableSync::ReceivingHandler
12
+ queue_as :custom_queue
13
+
14
+ receive "Project", to_table: :projects
15
+
16
+ receive "News", to_table: :news, events: :update do
17
+ after_commit_on_update do
18
+ NewsCache.reload
19
+ end
20
+ end
21
+
22
+ receive "User", to_table: :clients, events: %i[update destroy] do
23
+ mapping_overrides email: :project_user_email, id: :project_user_id
24
+
25
+ only :project_user_email, :project_user_id, :project_id
26
+ target_keys :project_id, :project_user_id
27
+ rest_key :project_user_rest
28
+ version_key :project_user_version
29
+
30
+ additional_data do |project_id:|
31
+ { project_id: project_id }
32
+ end
33
+
34
+ default_values do
35
+ { created_at: Time.current }
36
+ end
37
+ end
38
+
39
+ receive "User", to_model: CustomModel.new(:users) do
40
+ rest_key false
41
+ end
42
+ end
43
+ ```
44
+
45
+ ### Handler class (`Rabbit::Handler::MainProject::TableSync`)
46
+
47
+ In this case:
48
+ - `TableSync` - RabbitMQ event type.
49
+ - `MainProject` - event source.
50
+ - `Rabbit::Handler` - module for our handlers of events from RabbitMQ (there might be others)
51
+
52
+ Method `queue_as` allows you to set custom queue.
53
+
54
+ ### Recieving handler batch processing
55
+
56
+ Receiving handler supports array of attributes in a single update or destroy event. Corresponding
57
+ upsert-style logic in ActiveRecord and Sequel orm handlers are provided.
58
+
59
+ ### Config
60
+ ```ruby
61
+ receive source, [to_table:, to_model:, events:, &block]
62
+ ```
63
+
64
+ The method receives following arguments
65
+ - `source` - string, name of source model (required)
66
+ - `to_table` - destination table name (required if not set to_model)
67
+ - `to_model` - destination model (required if not set to_table)
68
+ - `events` - array of supported events (optional)
69
+ - `block` - configuration block with options (optional)
70
+
71
+ This method implements logic of mapping `source` to `to_table` (or to `to_model`) and allows customizing
72
+ the event handling logic with provided block.
73
+ You can use one `source` for a lot of `to_table` or `to_moel`.
74
+
75
+ ### Options:
76
+
77
+ Most of the options can be set as computed value or as a process.
78
+
79
+ ```ruby
80
+ option(value)
81
+ ```
82
+
83
+ ```ruby
84
+ option do |key params|
85
+ value
86
+ end
87
+ ```
88
+
89
+ Each of options can receive static value or code block which will be called for each event with the following arguments:
90
+ - `event` - type of event (`:update` or `:destroy`)
91
+ - `model` - source model (`Project`, `News`, `User` in example)
92
+ - `version` - version of the data
93
+ - `project_id` - id of project which is used in RabbitMQ
94
+ - `raw_data` - raw data from event (before applying `mapping_overrides`, `only`, etc.)
95
+
96
+ Blocks can receive any number of parameters from the list.
97
+
98
+ All specific key params will be explained in examples for each option.
99
+
100
+ #### only
101
+ Whitelist for receiving attributes.
102
+
103
+ ```ruby
104
+ only(instance of Array)
105
+ ```
106
+
107
+ ```ruby
108
+ only do |row:|
109
+ return instance of Array
110
+ end
111
+ ```
112
+
113
+ default value is taken through the call `model.columns`
114
+
115
+ #### target_keys
116
+ Primary keys or unique keys.
117
+
118
+ ```ruby
119
+ target_keys(instance of Array)
120
+ ```
121
+
122
+ ```ruby
123
+ target_keys do |data:|
124
+ return instance of Array
125
+ end
126
+ ```
127
+
128
+ default value is taken through the call `model.primary_keys`
129
+
130
+ #### rest_key
131
+ Name of jsonb column for attributes which are not included in the whitelist.
132
+ You can set the `rest_key(false)` if you won't need the rest data.
133
+
134
+ ```ruby
135
+ rest_key(instance of Symbol)
136
+ ```
137
+
138
+ ```ruby
139
+ rest_key do |row:, rest:|
140
+ return instance of Symbol
141
+ end
142
+ ```
143
+ default value is `:rest`
144
+
145
+ #### version_key
146
+ Name of version column.
147
+
148
+ ```ruby
149
+ version_key(instance of Symbol)
150
+ ```
151
+
152
+ ```ruby
153
+ version_key do |data:|
154
+ return instance of Symbol
155
+ end
156
+ ```
157
+ default value is `:version`
158
+
159
+ #### except
160
+ Blacklist for receiving attributes.
161
+
162
+ ```ruby
163
+ except(instance of Array)
164
+ ```
165
+
166
+ ```ruby
167
+ except do |row:|
168
+ return instance of Array
169
+ end
170
+ ```
171
+
172
+ default value is `[]`
173
+
174
+ #### mapping_overrides
175
+ Map for overriding receiving columns.
176
+
177
+ ```ruby
178
+ mapping_overrides(instance of Hash)
179
+ ```
180
+
181
+ ```ruby
182
+ mapping_overrides do |row:|
183
+ return instance of Hash
184
+ end
185
+ ```
186
+
187
+ default value is `{}`
188
+
189
+ #### additional_data
190
+ Additional data for insert or update (e.g. `project_id`).
191
+
192
+ ```ruby
193
+ additional_data(instance of Hash)
194
+ ```
195
+
196
+ ```ruby
197
+ additional_data do |row:|
198
+ return instance of Hash
199
+ end
200
+ ```
201
+
202
+ default value is `{}`
203
+
204
+ #### default_values
205
+ Values for insert if a row is not found.
206
+
207
+ ```ruby
208
+ default_values(instance of Hash)
209
+ ```
210
+
211
+ ```ruby
212
+ default_values do |data:|
213
+ return instance of Hash
214
+ end
215
+ ```
216
+
217
+ default value is `{}`
218
+
219
+ #### skip
220
+ Return truthy value to skip the row.
221
+
222
+ ```ruby
223
+ skip(instance of TrueClass or FalseClass)
224
+ ```
225
+
226
+ ```ruby
227
+ skip do |data:|
228
+ return instance of TrueClass or FalseClass
229
+ end
230
+ ```
231
+
232
+ default value is `false`
233
+
234
+ #### wrap_receiving
235
+ Proc that is used to wrap the receiving logic by custom block of code.
236
+
237
+ ```ruby
238
+ wrap_receiving do |data:, target_keys:, version_key:, default_values: {}, &receiving_logic|
239
+ receiving_logic.call
240
+ return makes no sense
241
+ end
242
+ ```
243
+
244
+ default value is `proc { |&block| block.call }`
245
+
246
+ #### before_update
247
+ Perform code before updating data in the database.
248
+
249
+ ```ruby
250
+ before_update do |data:, target_keys:, version_key:, default_values:|
251
+ return makes no sense
252
+ end
253
+
254
+ before_update do |data:, target_keys:, version_key:, default_values:|
255
+ return makes no sense
256
+ end
257
+ ```
258
+
259
+ Сan be defined several times. Execution order guaranteed.
260
+
261
+ #### after_commit_on_update
262
+ Perform code after updated data was committed.
263
+
264
+ ```ruby
265
+ after_commit_on_update do |data:, target_keys:, version_key:, default_values:|
266
+ return makes no sense
267
+ end
268
+
269
+ after_commit_on_update do |data:, target_keys:, version_key:, default_values:|
270
+ return makes no sense
271
+ end
272
+ ```
273
+
274
+ Сan be defined several times. Execution order guaranteed.
275
+
276
+ #### before_destroy
277
+ Perform code before destroying data in database.
278
+
279
+ ```ruby
280
+ before_destroy do |data:, target_keys:, version_key:|
281
+ return makes no sense
282
+ end
283
+
284
+ before_destroy do |data:, target_keys:, version_key:|
285
+ return makes no sense
286
+ end
287
+ ```
288
+
289
+ Сan be defined several times. Execution order guaranteed.
290
+
291
+ #### after_commit_on_destroy
292
+ Perform code after destroyed data was committed.
293
+
294
+ ```ruby
295
+ after_commit_on_destroy do |data:, target_keys:, version_key:|
296
+ return makes no sense
297
+ end
298
+
299
+ after_commit_on_destroy do |data:, target_keys:, version_key:|
300
+ return makes no sense
301
+ end
302
+ ```
303
+
304
+ Сan be defined several times. Execution order guaranteed.
305
+
306
+ ### Custom model
307
+ You can use custom model for receiving.
308
+ ```
309
+ class Rabbit::Handler::MainProject::TableSync < TableSync::ReceivingHandler
310
+ receive "Project", to_model: CustomModel.new
311
+ end
312
+ ```
313
+
314
+ This model has to implement next interface:
315
+ ```
316
+ def columns
317
+ return all columns from table
318
+ end
319
+
320
+ def primary_keys
321
+ return primary keys from table
322
+ end
323
+
324
+ def upsert(data: Array, target_keys: Array, version_key: Symbol, default_values: Hash)
325
+ return array with updated rows
326
+ end
327
+
328
+ def destroy(data: Array, target_keys: Array, version_key: Symbol)
329
+ return array with delited rows
330
+ end
331
+
332
+ def transaction(&block)
333
+ block.call
334
+ return makes no sense
335
+ end
336
+
337
+ def after_commit(&block)
338
+ block.call
339
+ return makes no sense
340
+ end
341
+ ```