table_sync 2.3.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/CHANGELOG.md +40 -0
  4. data/Gemfile.lock +82 -77
  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 +131 -0
  23. data/lib/table_sync/{model → receiving/model}/active_record.rb +36 -22
  24. data/lib/table_sync/{model → receiving/model}/sequel.rb +13 -8
  25. data/lib/table_sync/utils.rb +9 -0
  26. data/lib/table_sync/utils/interface_checker.rb +97 -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 +42 -30
  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/plugins.rb +0 -72
  44. data/lib/table_sync/plugins/abstract.rb +0 -55
  45. data/lib/table_sync/plugins/access_mixin.rb +0 -49
  46. data/lib/table_sync/plugins/registry.rb +0 -153
  47. data/lib/table_sync/receiving_handler.rb +0 -76
@@ -1,43 +0,0 @@
1
- # TableSync (development)
2
-
3
- ## Table of Contents
4
-
5
- - [Creation and registration of the new plugin](#creation-and-registration-of-the-new-plugin)
6
-
7
- ### Creation and registration of the new plugin
8
-
9
- * Create a class inherited from `TableSync::Plugins::Abstract` (recommendation: place it in the plugins directory (`lib/plugins/`));
10
- * Implement `.install!` method (`TableSync::Plugins::Abstract.install!`)
11
- * Register the newly created class in plugins ecosystem (`TableSync.register_plugin('plugin_name', PluginClass))`);
12
- * Usage: `TableSync.enable(:plugin_name)` / `TableSync.plugin(:plugin_name)` / `TableSync.load(:plugin_name)` (string name is supported too);
13
-
14
- Example:
15
-
16
- ```ruby
17
- # 1) creation (lib/plugins/global_method.rb)
18
- class TableSync::Plugins::GlobalMethod < TableSync::Plugins::Abstract
19
- class << self
20
- # 2) plugin loader method implementation
21
- def install!
22
- ::TableSync.extend(Module.new do
23
- def global_method
24
- :works!
25
- end
26
- end)
27
- end
28
- end
29
- end
30
-
31
- # 3) plugin registration
32
- TableSync.register('global_method', TableSync::Plugins::GlobalMethod)
33
-
34
- # 4) enable registerd plugin
35
- TableSync.plugin(:global_method) # string is supported too
36
- # --- or ---
37
- TableSync.enable(:global_method) # string is supported too
38
- # --- or ---
39
- TableSync.load(:global_method) # string is supported too
40
-
41
- # Your new functionality
42
- TableSync.global_method # => :works!
43
- ```
@@ -1,336 +0,0 @@
1
- # TableSync
2
-
3
- Table synchronization via RabbitMQ
4
-
5
- # Publishing changes
6
-
7
- Include `TableSync.sync(self)` into a Sequel or ActiveRecord model. `:if` and `:unless` are
8
- supported for Sequel and ActiveRecord
9
-
10
- Functioning `Rails.cache` is required
11
-
12
- Example:
13
-
14
- ```ruby
15
- class SomeModel < Sequel::Model
16
- TableSync.sync(self, { if: -> (*) { some_code } })
17
- end
18
- ```
19
-
20
- #### #attributes_for_sync
21
-
22
- Models can implement `#attributes_for_sync` to override which attributes are published. If not
23
- present, all attributes are published
24
-
25
- #### #attrs_for_routing_key
26
-
27
- Models can implement `#attrs_for_routing_key` to override which attributes are given to routing_key_callable. If not present, default attributes are given
28
-
29
- #### #attrs_for_metadata
30
-
31
- Models can implement `#attrs_for_metadata` to override which attributes are given to metadata_callable. If not present, default attributes are given
32
-
33
- #### .table_sync_model_name
34
-
35
- Models can implement `.table_sync_model_name` class method to override the model name used for
36
- publishing events. Default is model class name
37
-
38
- #### .table_sync_destroy_attributes(original_attributes)
39
-
40
- Models can implement `.table_sync_destroy_attributes` class method to override the attributes
41
- used for publishing destroy events. Default is object's primary key
42
-
43
- ## Configuration
44
-
45
- - `TableSync.publishing_job_class_callable` is a callable which should resolve to a ActiveJob
46
- subclass that calls TableSync back to actually publish changes (required)
47
-
48
- Example:
49
-
50
- ```ruby
51
- class TableSync::Job < ActiveJob::Base
52
- def perform(*args)
53
- TableSync::Publisher.new(*args).publish_now
54
- end
55
- end
56
- ```
57
-
58
- - `TableSync.batch_publishing_job_class_callable` is a callable which should resolve to a ActiveJob
59
- subclass that calls TableSync batch publisher back to actually publish changes (required for batch publisher)
60
-
61
- - `TableSync.routing_key_callable` is a callable which resolves which routing key to use when
62
- publishing changes. It receives object class and attributes (required)
63
-
64
- Example:
65
-
66
- ```ruby
67
- TableSync.routing_key_callable = -> (klass, attributes) { klass.gsub('::', '_').tableize }
68
- ```
69
-
70
- - `TableSync.routing_metadata_callable` is a callable that adds RabbitMQ headers which can be
71
- used in routing (optional). One possible way of using it is defining a headers exchange and
72
- routing rules based on key-value pairs (which correspond to sent headers)
73
-
74
- Example:
75
-
76
- ```ruby
77
- TableSync.routing_metadata_callable = -> (klass, attributes) { attributes.slice("project_id") }
78
- ```
79
-
80
- - `TableSync.exchange_name` defines the exchange name used for publishing (optional, falls back
81
- to default Rabbit gem configuration).
82
-
83
- - `TableSync.notifier` is a module that provides publish and recieve notifications.
84
-
85
- # Manual publishing
86
-
87
- `TableSync::Publisher.new(object_class, original_attributes, confirm: true, state: :updated, debounce_time: 45)`
88
- 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.
89
-
90
- # Manual publishing with batches
91
-
92
- You can use `TableSync::BatchPublisher` to publish changes in batches (array of hashes in `attributes`).
93
-
94
- When using `TableSync::BatchPublisher`,` TableSync.routing_key_callable` is called as follows:
95
- `TableSync.routing_key_callable.call(klass, {})`, i.e. empty hash is passed instead of attributes.
96
- And `TableSync.routing_metadata_callable` is not called at all: metadata is set to empty hash.
97
-
98
- `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.
99
-
100
- `options` consists of:
101
- - `confirm`, which is a flag for RabbitMQ, `true` by default
102
- - `routing_key`, which is a custom key used (if given) to override one from `TableSync.routing_key_callable`, `nil` by default
103
- - `push_original_attributes` (default value is `false`), if this option is set to `true`,
104
- original_attributes_array will be pushed to Rabbit instead of fetching records from database and sending their mapped attributes.
105
- - `headers`, which is an option for custom headers (can be used for headers exchanges routes), `nil` by default
106
- - `event`, which is an option for event specification (`:destroy` or `:update`), `:update` by default
107
-
108
- Example:
109
-
110
- ```ruby
111
- TableSync::BatchPublisher.new(
112
- "SomeClass",
113
- [{ id: 1 }, { id: 2 }],
114
- confirm: false,
115
- routing_key: "custom_routing_key",
116
- push_original_attributes: true,
117
- headers: { key: :value },
118
- event: :destroy,
119
- )
120
- ```
121
-
122
- # Manual publishing with batches (Russian)
123
-
124
- С помощью класса `TableSync::BatchPublisher` вы можете опубликовать изменения батчами (массивом в `attributes`).
125
-
126
- При использовании `TableSync::BatchPublisher`, `TableSync.routing_key_callable` вызывается следующим образом:
127
- `TableSync.routing_key_callable.call(klass, {})`, то есть вместо аттрибутов передается пустой хэш.
128
- А `TableSync.routing_metadata_callable` не вызывается вовсе: в метадате устанавливается пустой хэш.
129
-
130
- `TableSync::BatchPublisher.new(object_class, original_attributes_array, **options)`, где `original_attributes_array` - массив с аттрибутами публикуемых объектов и `options`- это хэш с дополнительными опциями.
131
-
132
- `options` состоит из:
133
- - `confirm`, флаг для RabbitMQ, по умолчанию - `true`
134
- - `routing_key`, ключ, который (если указан) замещает ключ, получаемый из `TableSync.routing_key_callable`, по умолчанию - `nil`
135
- - `push_original_attributes` (значение по умолчанию `false`), если для этой опции задано значение true, в Rabbit будут отправлены original_attributes_array, вместо получения значений записей из базы непосредственно перед отправкой.
136
- - `headers`, опция для задания headers (можно использовать для задания маршрутов в headers exchange'ах), `nil` по умолчанию
137
- - `event`, опция для указания типа события (`:destroy` или `:update`), `:update` по умолчанию
138
-
139
- Example:
140
-
141
- ```ruby
142
- TableSync::BatchPublisher.new(
143
- "SomeClass",
144
- [{ id: 1 }, { id: 2 }],
145
- confirm: false,
146
- routing_key: "custom_routing_key",
147
- push_original_attributes: true,
148
- headers: { key: :value },
149
- event: :destroy,
150
- )
151
- ```
152
-
153
- # Receiving changes
154
-
155
- Naming convention for receiving handlers is `Rabbit::Handler::GROUP_ID::TableSync`,
156
- where `GROUP_ID` represents first part of source exchange name.
157
- Define handler class inherited from `TableSync::ReceivingHandler`
158
- and named according to described convention.
159
- You should use DSL inside the class.
160
- Suppose we will synchronize models {Project, News, User} project {MainProject}, then:
161
-
162
- ```ruby
163
- class Rabbit::Handler::MainProject::TableSync < TableSync::ReceivingHandler
164
- queue_as :custom_queue
165
-
166
- receive "Project", to_table: :projects
167
-
168
- receive "News", to_table: :news, events: :update do
169
- after_commit on: :update do
170
- NewsCache.reload
171
- end
172
- end
173
-
174
- receive "User", to_table: :clients, events: %i[update destroy] do
175
- mapping_overrides email: :project_user_email, id: :project_user_id
176
-
177
- only :project_user_email, :project_user_id
178
- target_keys :project_id, :project_user_id
179
- rest_key :project_user_rest
180
- version_key :project_user_version
181
-
182
- additional_data do |project_id:|
183
- { project_id: project_id }
184
- end
185
-
186
- default_values do
187
- { created_at: Time.current }
188
- end
189
- end
190
-
191
- receive "User", to_table: :users do
192
- rest_key nil
193
- end
194
- end
195
- ```
196
-
197
- ### Handler class (`Rabbit::Handler::MainProject::TableSync`)
198
-
199
- In this case:
200
- - `TableSync` - RabbitMQ event type.
201
- - `MainProject` - event source.
202
- - `Rabbit::Handler` - module for our handlers of events from RabbitMQ (there might be others)
203
-
204
- Method `queue_as` allow you to set custom queue.
205
-
206
- ### Recieving handler batch processing
207
-
208
- Receiving handler supports array of attributes in a single update event. Corresponding
209
- upsert-style logic in ActiveRecord and Sequel orm handlers is provided.
210
-
211
- ### Config DSL
212
- ```ruby
213
- receive source, to_table:, [events:, &block]
214
- ```
215
-
216
- The method receives following arguments
217
- - `source` - string, name of source model (required)
218
- - `to_table` - destination_table hash (required)
219
- - `events` - array of supported events (optional)
220
- - `block` - configuration block (optional)
221
-
222
- This method implements logic of mapping `source` to `to_table` and allows customizing the event handling logic with provided block.
223
- You can use one `source` for a lot of `to_table`.
224
-
225
- The following options are available inside the block:
226
- - `on_destroy` - defines a custom logic and behavior for `destroy` event:
227
- - definition:
228
- ```ruby
229
- on_destroy do |attributes:, target_keys:|
230
- # your code here
231
- end
232
- ```
233
- - `target_keys:` - primary keys or unique keys;
234
- - `attributes:` - received model attributes;
235
- - `only` - whitelist for receiving attributes
236
- - `skip` - return truthy value to skip the row
237
- - `target_keys` - primary keys or unique keys
238
- - `rest_key` - name of jsonb column for attributes which are not included in the whitelist. You can set the `rest_key(false)` or `rest_key(nil)` if you won't need the rest data.
239
- - `version_key` - name of version column
240
- - `first_sync_time_key` - name of the column where the time of first record synchronization should be stored. Disabled by default.
241
- - `mapping_overrides` - map for overriding receiving columns
242
- - `additional_data` - additional data for insert or update (e.g. `project_id`)
243
- - `default_values` - values for insert if a row is not found
244
- - `partitions` - proc that is used to obtain partitioned data to support table partitioning. Must return a hash which
245
- keys are names of partitions of partitioned table and values - arrays of attributes to be inserted into particular
246
- partition `{ measurements_2018_01: [ { attrs }, ... ], measurements_2018_02: [ { attrs }, ... ], ...}`.
247
- While the proc is called inside an upsert transaction it is suitable place for creating partitions for new data.
248
- Note that transaction of proc is a TableSynk.orm transaction.
249
- ```ruby
250
- partitions do |data:|
251
- data.group_by { |d| "measurements_#{d[:time].year}_#{d[:time].month}" }
252
- .tap { |data| data.keys.each { |table| DB.run("CREATE TABLE IF NOT EXISTS #{table} PARTITION OF measurements") } }
253
- end
254
- ```
255
- - `wrap_reciving` - proc that is used to wrap the receiving logic by custom block of code. Receives `data` and `receiving` attributes
256
- (received event data and receiving logic proc respectively). `receiving.call` runs receiving process (you should use it manually).
257
- - example (concurrent receiving):
258
- ```ruby
259
- wrap_receiving do |data, receiving|
260
- Locking.acquire("some-lock-key") { receiving.call }
261
- end
262
- ```
263
- - `data` attribute:
264
- - for `destroy` event - an instance of `TableSync::EventActions::DataWrapper::Destroy`;
265
- - for `update` event - an instance of `TableSync::EventActions::DataWrapper::Update`;
266
- - `#event_data` - raw recevied event data:
267
- - for `destroy` event - simple `Hash`;
268
- - for `update` event - `Hash` with `Hash<ModelKlass, Array<Hash<Symbol, Object>>>` signature;
269
- - `#destroy?` / `#update?` - corresponding predicates;
270
- - `#type` - indicates a type of data (`:destroy` and `:update` respectively);
271
- - `#each` - iterates over `#event_data` elements (acts like an iteration over an array of elements);
272
-
273
- Each of options can receive static value or code block which will be called for each event with the following arguments:
274
- - `event` - type of event (`:update` or `:destroy`)
275
- - `model` - source model (`Project`, `News`, `User` in example)
276
- - `version` - version of the data
277
- - `project_id` - id of project which is used in RabbitMQ
278
- - `data` - raw data from event (before applying `mapping_overrides`, `only`, etc.)
279
-
280
- Also, the `additional_data`, `skip` has a `current_row` field, which gives you a hash of all parameters of the current row (useful when receiving changes in batches).
281
-
282
- Block can receive any number of parameters from the list.
283
-
284
- ### Callbacks
285
- You can set callbacks like this:
286
- ```ruby
287
- before_commit on: event, &block
288
- after_commit on: event, &block
289
- ```
290
- TableSync performs this callbacks after transaction commit as to avoid side effects. Block receives array of record attributes.
291
-
292
- ### Notifications
293
-
294
- #### ActiveSupport adapter
295
-
296
- You can use an already existing ActiveSupport adapter:
297
- ```ruby
298
- TableSync.notifier = TableSync::InstrumentAdapter::ActiveSupport
299
- ```
300
-
301
- This instrumentation API is provided by Active Support. It allows to subscribe to notifications:
302
-
303
- ```ruby
304
- ActiveSupport::Notifications.subscribe(/tablesync/) do |name, start, finish, id, payload|
305
- # do something
306
- end
307
- ```
308
-
309
- Types of events available:
310
- `"tablesync.receive.update"`, `"tablesync.receive.destroy"`, `"tablesync.publish.update"`
311
- and `"tablesync.publish.destroy"`.
312
-
313
- You have access to the payload, which contains `event`, `direction`, `table`, `schema` and `count`.
314
-
315
- ```
316
- {
317
- :event => :update, # one of update / destroy
318
- :direction => :publish, # one of publish / receive
319
- :table => "users",
320
- :schema => "public",
321
- :count => 1
322
- }
323
- ```
324
-
325
- See more at https://guides.rubyonrails.org/active_support_instrumentation.html
326
-
327
-
328
- #### Custom adapters
329
-
330
- You can also create a custom adapter. It is expected to respond to the following method:
331
-
332
- ```ruby
333
- def notify(table:, event:, direction:, count:)
334
- # processes data about table_sync event
335
- end
336
- ```
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TableSync
4
- class Config
5
- attr_reader :model, :events, :callback_registry
6
-
7
- def initialize(model:, events: nil)
8
- @model = model
9
- @events = events.nil? ? nil : [events].flatten.map(&:to_sym)
10
-
11
- @callback_registry = CallbackRegistry.new
12
-
13
- # initialize default options
14
- only(model.columns)
15
- mapping_overrides({})
16
- additional_data({})
17
- default_values({})
18
- @rest_key = :rest
19
- @version_key = :version
20
- @first_sync_time_key = nil
21
- @on_destroy = nil
22
- @wrap_receiving = nil
23
- target_keys(model.primary_keys)
24
- end
25
-
26
- # add_option implements the following logic
27
- # config.option - get value
28
- # config.option(args) - set static value
29
- # config.option { ... } - set proc as value
30
- def self.add_option(name, &option_block)
31
- ivar = "@#{name}".to_sym
32
-
33
- # default option handler (empty handler)
34
- # handles values when setting options
35
- option_block ||= proc { |value| value }
36
-
37
- define_method(name) do |*args, &block|
38
- # logic for each option
39
- if args.empty? && block.nil? # case for config.option, just return ivar
40
- instance_variable_get(ivar)
41
- elsif block # case for config.option { ... }
42
- # get named parameters (parameters with :keyreq) from proc-value
43
- params = block.parameters.map { |param| param[0] == :keyreq ? param[1] : nil }.compact
44
-
45
- # wrapper for proc-value, this wrapper receives all params (model, version, project_id...)
46
- # and filters them for proc-value
47
- unified_block = proc { |hash = {}| block.call(**hash.slice(*params)) }
48
-
49
- # set wrapped proc-value as ivar value
50
- instance_variable_set(ivar, unified_block)
51
- else # case for config.option(args)
52
- # call option_block with args as params and set ivar to result
53
- instance_variable_set(ivar, instance_exec(*args, &option_block))
54
- end
55
- end
56
- end
57
-
58
- def allow_event?(name)
59
- return true if events.nil?
60
- events.include?(name)
61
- end
62
-
63
- def before_commit(on:, &block)
64
- callback_registry.register_callback(block, kind: :before_commit, event: on.to_sym)
65
- end
66
-
67
- def after_commit(on:, &block)
68
- callback_registry.register_callback(block, kind: :after_commit, event: on.to_sym)
69
- end
70
-
71
- def on_destroy(&block)
72
- block_given? ? @on_destroy = block : @on_destroy
73
- end
74
-
75
- def wrap_receiving(&block)
76
- block_given? ? @wrap_receiving = block : @wrap_receiving
77
- end
78
-
79
- check_and_set_column_key = proc do |key|
80
- key = key.to_sym
81
- raise "#{model.inspect} doesn't have key: #{key}" unless model.columns.include?(key)
82
- key
83
- end
84
-
85
- set_column_keys = proc do |*keys|
86
- [keys].flatten.map { |k| instance_exec(k, &check_and_set_column_key) }
87
- end
88
-
89
- add_option(:only, &set_column_keys)
90
- add_option(:target_keys, &set_column_keys)
91
- add_option(:rest_key) do |value|
92
- value ? instance_exec(value, &check_and_set_column_key) : nil
93
- end
94
- add_option(:version_key, &check_and_set_column_key)
95
- add_option(:first_sync_time_key) do |value|
96
- value ? instance_exec(value, &check_and_set_column_key) : nil
97
- end
98
-
99
- add_option(:mapping_overrides)
100
- add_option(:additional_data)
101
- add_option(:default_values)
102
- add_option(:partitions)
103
- add_option(:skip)
104
- end
105
- end