table_sync 1.13.1 → 4.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.rubocop.yml +26 -1
  4. data/.travis.yml +6 -6
  5. data/CHANGELOG.md +74 -1
  6. data/Gemfile.lock +262 -0
  7. data/LICENSE.md +1 -1
  8. data/README.md +4 -1
  9. data/docs/message_protocol.md +24 -0
  10. data/docs/notifications.md +45 -0
  11. data/docs/publishing.md +147 -0
  12. data/docs/receiving.md +341 -0
  13. data/lib/table_sync.rb +23 -33
  14. data/lib/table_sync/errors.rb +60 -22
  15. data/lib/table_sync/instrument.rb +2 -2
  16. data/lib/table_sync/publishing.rb +11 -0
  17. data/lib/table_sync/{base_publisher.rb → publishing/base_publisher.rb} +1 -1
  18. data/lib/table_sync/{batch_publisher.rb → publishing/batch_publisher.rb} +12 -13
  19. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/active_record.rb +4 -8
  20. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/sequel.rb +10 -17
  21. data/lib/table_sync/{publisher.rb → publishing/publisher.rb} +4 -4
  22. data/lib/table_sync/receiving.rb +14 -0
  23. data/lib/table_sync/receiving/config.rb +218 -0
  24. data/lib/table_sync/receiving/config_decorator.rb +27 -0
  25. data/lib/table_sync/receiving/dsl.rb +28 -0
  26. data/lib/table_sync/receiving/handler.rb +131 -0
  27. data/lib/table_sync/{model → receiving/model}/active_record.rb +37 -23
  28. data/lib/table_sync/{model → receiving/model}/sequel.rb +13 -8
  29. data/lib/table_sync/utils.rb +9 -0
  30. data/lib/table_sync/utils/interface_checker.rb +97 -0
  31. data/lib/table_sync/utils/proc_array.rb +17 -0
  32. data/lib/table_sync/utils/proc_keywords_resolver.rb +46 -0
  33. data/lib/table_sync/version.rb +1 -1
  34. data/table_sync.gemspec +5 -4
  35. metadata +48 -30
  36. data/docs/synopsis.md +0 -318
  37. data/lib/table_sync/config.rb +0 -105
  38. data/lib/table_sync/config/callback_registry.rb +0 -53
  39. data/lib/table_sync/config_decorator.rb +0 -38
  40. data/lib/table_sync/dsl.rb +0 -25
  41. data/lib/table_sync/event_actions.rb +0 -96
  42. data/lib/table_sync/event_actions/data_wrapper.rb +0 -7
  43. data/lib/table_sync/event_actions/data_wrapper/base.rb +0 -23
  44. data/lib/table_sync/event_actions/data_wrapper/destroy.rb +0 -19
  45. data/lib/table_sync/event_actions/data_wrapper/update.rb +0 -21
  46. data/lib/table_sync/receiving_handler.rb +0 -76
@@ -0,0 +1,45 @@
1
+ ### Notifications
2
+
3
+ #### ActiveSupport adapter
4
+
5
+ You can use an already existing ActiveSupport adapter:
6
+ ```ruby
7
+ TableSync.notifier = TableSync::InstrumentAdapter::ActiveSupport
8
+ ```
9
+
10
+ This instrumentation API is provided by Active Support. It allows to subscribe to notifications:
11
+
12
+ ```ruby
13
+ ActiveSupport::Notifications.subscribe(/tablesync/) do |name, start, finish, id, payload|
14
+ # do something
15
+ end
16
+ ```
17
+
18
+ Types of events available:
19
+ `"tablesync.receive.update"`, `"tablesync.receive.destroy"`, `"tablesync.publish.update"`
20
+ and `"tablesync.publish.destroy"`.
21
+
22
+ You have access to the payload, which contains `event`, `direction`, `table`, `schema` and `count`.
23
+
24
+ ```
25
+ {
26
+ :event => :update, # one of update / destroy
27
+ :direction => :publish, # one of publish / receive
28
+ :table => "users",
29
+ :schema => "public",
30
+ :count => 1
31
+ }
32
+ ```
33
+
34
+ See more at https://guides.rubyonrails.org/active_support_instrumentation.html
35
+
36
+
37
+ #### Custom adapters
38
+
39
+ You can also create a custom adapter. It is expected to respond to the following method:
40
+
41
+ ```ruby
42
+ def notify(table:, event:, direction:, count:)
43
+ # processes data about table_sync event
44
+ end
45
+ ```
@@ -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
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 nil
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
+ ```