pub_sub_model_sync 1.0.1 → 1.2.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +2 -6
- data/README.md +30 -24
- data/lib/pub_sub_model_sync/config.rb +1 -2
- data/lib/pub_sub_model_sync/message_publisher.rb +1 -1
- data/lib/pub_sub_model_sync/payload.rb +1 -1
- data/lib/pub_sub_model_sync/payload_builder.rb +2 -2
- data/lib/pub_sub_model_sync/publisher_concern.rb +1 -10
- data/lib/pub_sub_model_sync/railtie.rb +0 -1
- data/lib/pub_sub_model_sync/service_base.rb +1 -1
- data/lib/pub_sub_model_sync/service_google.rb +1 -1
- data/lib/pub_sub_model_sync/transaction.rb +11 -10
- data/lib/pub_sub_model_sync/version.rb +1 -1
- metadata +2 -3
- data/lib/pub_sub_model_sync/initializers/before_commit.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 86dd60cd72d095630d44b3d1d6bb0004f0d7ce35326f5613127815047376530a
|
4
|
+
data.tar.gz: ef7c8edd08a315adcbad4a4314a467e1564a8dcc2233764ef53d09985a41eaf3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 690ca42fe8d463cabdaa4e0e557ee1fa19ffa0e1c9a4ad19ee1a1195a7a78faab458ebcd0b0581c39b917ac73c9dc01f408bf1c9ae7f25445b7d20afba2592fe
|
7
|
+
data.tar.gz: 72d3e0a10a8879f670e65a6f50dbc71e0351893ac1b57dc6fe1796373f7a38d55ecc7a6508c86eba7256dab03c903a42b59f9a0b7a881f6c4c28086b59584049
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
# 1.2.1 (October 28, 2021)
|
4
|
+
chore: improve logs
|
5
|
+
|
6
|
+
# 1.2.0 (October 28, 2021)
|
7
|
+
- feat: rename Payload `:key` into `:internal_key` to avoid confusions while debugging
|
8
|
+
|
9
|
+
# 1.1.1 (October 25, 2021)
|
10
|
+
- feat: do not exclude `ordering_key topic_name` when delivering a notification (required when debugging)
|
11
|
+
- doc: improve docs
|
12
|
+
|
13
|
+
# 1.1.0 (October 25, 2021)
|
14
|
+
- feat: change `transactions_max_buffer` default value to 1 to deliver notifications once they were called
|
15
|
+
- feat: use `after_commit` instead of `before_commit` callback and remove the horrible AR patch for rails 4
|
16
|
+
|
3
17
|
# 1.0.1 (August 20, 2021)
|
4
18
|
- refactor: improve service exit when running in k8s
|
5
19
|
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pub_sub_model_sync (1.
|
4
|
+
pub_sub_model_sync (1.2.1)
|
5
5
|
rails
|
6
6
|
|
7
7
|
GEM
|
@@ -116,7 +116,6 @@ GEM
|
|
116
116
|
google-cloud-errors (~> 1.0)
|
117
117
|
grpc-google-iam-v1 (>= 0.6.10, < 2.0)
|
118
118
|
google-protobuf (3.17.0)
|
119
|
-
google-protobuf (3.17.0-x86_64-linux)
|
120
119
|
googleapis-common-protos (1.3.11)
|
121
120
|
google-protobuf (~> 3.14)
|
122
121
|
googleapis-common-protos-types (>= 1.0.6, < 2.0)
|
@@ -133,9 +132,6 @@ GEM
|
|
133
132
|
grpc (1.37.1)
|
134
133
|
google-protobuf (~> 3.15)
|
135
134
|
googleapis-common-protos-types (~> 1.0)
|
136
|
-
grpc (1.37.1-x86_64-linux)
|
137
|
-
google-protobuf (~> 3.15)
|
138
|
-
googleapis-common-protos-types (~> 1.0)
|
139
135
|
grpc-google-iam-v1 (0.6.11)
|
140
136
|
google-protobuf (~> 3.14)
|
141
137
|
googleapis-common-protos (>= 1.3.11, < 2.0)
|
@@ -264,4 +260,4 @@ DEPENDENCIES
|
|
264
260
|
sqlite3
|
265
261
|
|
266
262
|
BUNDLED WITH
|
267
|
-
2.2.
|
263
|
+
2.2.29
|
data/README.md
CHANGED
@@ -245,7 +245,7 @@ PubSubModelSync::Payload.new({ ids: [my_user.id] }, { klass: 'User', action: :ba
|
|
245
245
|
- `method_name` (Symbol, optional) method to be called to process action callback, sample: `def my_method(action) ... end`
|
246
246
|
- `block` (Proc, optional) Block to be called to process action callback, sample: `{ |action| ... }`
|
247
247
|
|
248
|
-
**Note1**: Due to rails callback ordering, this method uses `
|
248
|
+
**Note1**: Due to rails callback ordering, this method uses `after_commit on: action {...}` callback when creating or updating models to ensure expected notifications order (More details [**here**](#transactions)).
|
249
249
|
**Note2**: Due to rails callback ordering, this method uses `after_destroy` callback when destroying models to ensure the expected notifications order.
|
250
250
|
|
251
251
|
- `ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)` Delivers an instance notification via pubsub
|
@@ -311,11 +311,14 @@ Any notification before delivering is transformed as a Payload for a better port
|
|
311
311
|
- `klass`: (String) Notification class name
|
312
312
|
- `mode`: (Symbol: `:model`|`:class`) Kind of notification
|
313
313
|
* `headers`: (Hash) Notification settings that defines how the notification will be processed or delivered.
|
314
|
-
- `
|
315
|
-
|
316
|
-
|
317
|
-
- `topic_name`: (String|Array<String>, optional): Specific topic name
|
318
|
-
- `forced_ordering_key`: (String, optional):
|
314
|
+
- `ordering_key`: (String, optional): notifications with the same `ordering_key` are processed in the same order they were delivered, default: `<model.class.name>/<model.id>` when instance notification and `klass_name` when class notification.
|
315
|
+
Note: Final `ordering_key` is calculated as: `payload.headers[:forced_ordering_key] || current_transaction&.key || payload.headers[:ordering_key]`
|
316
|
+
- `internal_key`: (String, optional) Internal identifier of the payload, default: `<model.class.name>/<action>/<model.id>` when model notification and `<klass_name>/<action>` when class notification (Useful for caching techniques).
|
317
|
+
- `topic_name`: (String|Array<String>, optional): Specific topic name where to deliver the notification (default `PubSubModelSync::Config.topic_name`).
|
318
|
+
- `forced_ordering_key`: (String, optional): Overrides `ordering_key` with the provided value even withing transactions. Default `nil`.
|
319
|
+
- `app_key`: (Auto calculated): Name of the application who delivered the notification.
|
320
|
+
- `uuid`: (Auto calculated): Unique notification identifier (Very useful when debugging).
|
321
|
+
Note: To reduce Payload size, some header info are not delivered (Enable debug mode to deliver all payload info).
|
319
322
|
|
320
323
|
- Actions
|
321
324
|
```ruby
|
@@ -340,21 +343,21 @@ Any notification before delivering is transformed as a Payload for a better port
|
|
340
343
|
ps_after_action([:create, :update, :destroy]) { |action| ps_publish(action, mapping: %i[id user_id title]) }
|
341
344
|
end
|
342
345
|
```
|
343
|
-
- When created (all notifications use the same
|
346
|
+
- When created (all notifications use the same ordering_key to be processed in the same order)
|
344
347
|
```ruby
|
345
348
|
user = User.create!(name: 'test', posts_attributes: [{ title: 'Post 1' }, { title: 'Post 2' }])
|
346
349
|
# notification #1 => <Payload data: {id: 1, name: 'sample'}, info: { klass: 'User', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
|
347
350
|
# notification #2 => <Payload data: {id: 1, title: 'Post 1', user_id: 1}, info: { klass: 'Post', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
|
348
351
|
# notification #3 => <Payload data: {id: 2, title: 'Post 2', user_id: 1}, info: { klass: 'Post', action: :create, mode: :model }, headers: { ordering_key = `User/1` }>
|
349
352
|
```
|
350
|
-
- When updated (all notifications use the same
|
353
|
+
- When updated (all notifications use the same ordering_key to be processed in the same order)
|
351
354
|
```ruby
|
352
355
|
user.update!(name: 'changed', posts_attributes: [{ id: 1, title: 'Post 1C' }, { id: 2, title: 'Post 2C' }])
|
353
356
|
# notification #1 => <Payload data: {id: 1, name: 'changed'}, info: { klass: 'User', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
|
354
357
|
# notification #2 => <Payload data: {id: 1, title: 'Post 1C', user_id: 1}, info: { klass: 'Post', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
|
355
358
|
# notification #3 => <Payload data: {id: 2, title: 'Post 2C', user_id: 1}, info: { klass: 'Post', action: :update, mode: :model }, headers: { ordering_key = `User/1` }>
|
356
359
|
```
|
357
|
-
- When destroyed (all notifications use the same
|
360
|
+
- When destroyed (all notifications use the same ordering_key to be processed in the same order)
|
358
361
|
**Note**: The notifications order were reordered in order to avoid inconsistency in other apps
|
359
362
|
```ruby
|
360
363
|
user.destroy!
|
@@ -368,10 +371,8 @@ Any notification before delivering is transformed as a Payload for a better port
|
|
368
371
|
|
369
372
|
- Manual transactions
|
370
373
|
`PubSubModelSync::MessagePublisher::transaction(key, max_buffer: , &block)`
|
371
|
-
- `key` (String|nil) Key used as the
|
372
|
-
- `max_buffer:` (
|
373
|
-
If true: will save all notifications and deliver all them when transaction has successfully finished. If transaction has failed, then all saved notifications will be discarded (not delivered).
|
374
|
-
If false: will deliver all notifications immediately (no way to rollback notifications if transaction has failed)
|
374
|
+
- `key` (String|nil) Key used as the ordering_key for all inner notifications (When nil, will use `ordering_key` of the first notification)
|
375
|
+
- `max_buffer:` (Integer, default: `PubSubModelSync::Config.transactions_max_buffer`) Transaction buffer size (more details in #transactions_max_buffer).
|
375
376
|
Sample:
|
376
377
|
```ruby
|
377
378
|
PubSubModelSync::MessagePublisher::transaction('my-custom-key') do
|
@@ -530,7 +531,7 @@ config.debug = true
|
|
530
531
|
Topic name(s) to be used to listen all notifications from when listening. Additionally first topic name is used as the default topic name when publishing a notification.
|
531
532
|
- `.subscription_name = "my-app-1"`: (String, default Rails.application.name)
|
532
533
|
Subscriber's identifier which helps to:
|
533
|
-
* skip self
|
534
|
+
* skip self notifications
|
534
535
|
* continue the sync from the last synced notification when service was restarted.
|
535
536
|
- `.default_topic_name = "my_topic"`: (String|Array<String>, optional(default first topic from `topic_name`))
|
536
537
|
Topic name used as the default topic if not defined in the payload when publishing a notification
|
@@ -539,24 +540,26 @@ config.debug = true
|
|
539
540
|
- ```.logger = Rails.logger```
|
540
541
|
(Logger) => define custom logger
|
541
542
|
- ```.on_before_processing = ->(payload, {subscriber:}) { puts payload }```
|
542
|
-
(Proc) => called before processing received
|
543
|
+
(Proc) => called before processing a received notification (:cancel can be returned to skip processing)
|
543
544
|
- ```.on_success_processing = ->(payload, {subscriber:}) { puts payload }```
|
544
|
-
(Proc) => called when a
|
545
|
+
(Proc) => called when a notification was successfully processed
|
545
546
|
- ```.on_error_processing = ->(exception, {payload:, subscriber:}) { payload.delay(...).process! }```
|
546
|
-
(Proc) => called when a
|
547
|
+
(Proc) => called when a notification has failed when processing (delayed_job or similar can be used for retrying)
|
547
548
|
- ```.on_before_publish = ->(payload) { puts payload }```
|
548
|
-
(Proc) => called before publishing a
|
549
|
+
(Proc) => called before publishing a notification (:cancel can be returned to skip publishing)
|
549
550
|
- ```.on_after_publish = ->(payload) { puts payload }```
|
550
|
-
(Proc) => called after publishing a
|
551
|
+
(Proc) => called after publishing a notification
|
551
552
|
- ```.on_error_publish = ->(exception, {payload:}) { payload.delay(...).publish! }```
|
552
|
-
(Proc) => called when failed publishing a
|
553
|
-
- ```.transactions_max_buffer =
|
554
|
-
|
555
|
-
|
553
|
+
(Proc) => called when failed publishing a notification (delayed_job or similar can be used for retrying)
|
554
|
+
- ```.transactions_max_buffer = 1``` (Integer, default 1) Controls the maximum quantity of notifications to be enqueued to the transaction-buffer before delivering them and thus adds the ability to rollback notifications if the transaction fails.
|
555
|
+
Once this quantity of notifications is reached, then all notifications of the current transaction will immediately be delivered (can be customized per transaction).
|
556
|
+
Note: There is no way to rollback delivered notifications if current transaction fails later.
|
557
|
+
Note2: Only notifications from the buffer can be rollbacked if the current transaction has failed.
|
556
558
|
|
557
559
|
## **TODO**
|
560
|
+
- add the ability to raise SKIP_ACKNOWLEDGE to auto retry by PubSub
|
558
561
|
- Auto publish update only if payload has changed (see ways to compare previous payload vs new payload)
|
559
|
-
- Improve transactions to exclude similar
|
562
|
+
- Improve transactions to exclude similar notifications by klass and action. Sample:
|
560
563
|
```PubSubModelSync::MessagePublisher.transaction(key, { same_keys: :use_last_as_first|:use_last|:use_first_as_last|:keep*, same_data: :use_last_as_first*|:use_last|:use_first_as_last|:keep })```
|
561
564
|
- Add DB table to use as a shield to prevent publishing similar notifications and publish partial notifications (similar idea when processing notif)
|
562
565
|
- Last notification is not being delivered immediately in google pubsub (maybe force with timeout 10secs and service.deliver_messages)
|
@@ -564,6 +567,9 @@ config.debug = true
|
|
564
567
|
- Services support to deliver multiple payloads from transactions
|
565
568
|
- Fix deprecation warnings: pub_sub_model_sync/service_google.rb:39: warning: Splitting the last argument into positional and keyword parameters is deprecated
|
566
569
|
- Add if/unless to ps_after_action
|
570
|
+
- Add subscription liveness checker using thread without db connection to check periodically pending notifications from google pubsub
|
571
|
+
- Unify .stop() and 'Listener stopped'
|
572
|
+
- TODO: Publish new version 1.2.1 (improve logs)
|
567
573
|
|
568
574
|
## **Q&A**
|
569
575
|
- I'm getting error "could not obtain a connection from the pool within 5.000 seconds"... what does this mean?
|
@@ -8,8 +8,7 @@ module PubSubModelSync
|
|
8
8
|
# customizable callbacks
|
9
9
|
cattr_accessor(:debug) { false }
|
10
10
|
cattr_accessor :logger # LoggerInst
|
11
|
-
cattr_accessor(:transactions_max_buffer) {
|
12
|
-
cattr_accessor(:enable_rails4_before_commit) { Rails::VERSION::MAJOR == 4 }
|
11
|
+
cattr_accessor(:transactions_max_buffer) { 1 }
|
13
12
|
|
14
13
|
cattr_accessor(:on_before_processing) { ->(_payload, _info) {} } # return :cancel to skip
|
15
14
|
cattr_accessor(:on_success_processing) { ->(_payload, _info) {} }
|
@@ -31,7 +31,7 @@ module PubSubModelSync
|
|
31
31
|
end
|
32
32
|
|
33
33
|
# Starts a new transaction
|
34
|
-
# @param key (String
|
34
|
+
# @param key (String, Nil)
|
35
35
|
# @return (Transaction)
|
36
36
|
def init_transaction(key, settings = {})
|
37
37
|
new_transaction = PubSubModelSync::Transaction.new(key, settings)
|
@@ -86,7 +86,7 @@ module PubSubModelSync
|
|
86
86
|
|
87
87
|
def build_headers
|
88
88
|
headers[:app_key] ||= PubSubModelSync::Config.subscription_key
|
89
|
-
headers[:
|
89
|
+
headers[:internal_key] ||= [klass, action].join('/')
|
90
90
|
headers[:ordering_key] ||= klass
|
91
91
|
headers[:uuid] ||= SecureRandom.uuid
|
92
92
|
end
|
@@ -43,8 +43,8 @@ module PubSubModelSync
|
|
43
43
|
|
44
44
|
def headers_data
|
45
45
|
klass_name = model.class.name
|
46
|
-
|
47
|
-
def_data = { ordering_key: self.class.ordering_key_for(model),
|
46
|
+
internal_key = [klass_name, action, model.id || SecureRandom.uuid].join('/')
|
47
|
+
def_data = { ordering_key: self.class.ordering_key_for(model), internal_key: internal_key }
|
48
48
|
def_data.merge(compute_value(headers))
|
49
49
|
end
|
50
50
|
|
@@ -67,7 +67,7 @@ module PubSubModelSync
|
|
67
67
|
if action == :destroy
|
68
68
|
after_destroy { instance_exec(action, &callback) }
|
69
69
|
else
|
70
|
-
|
70
|
+
send(:after_commit, on: action) { instance_exec(action, &callback) }
|
71
71
|
end
|
72
72
|
end
|
73
73
|
end
|
@@ -80,15 +80,6 @@ module PubSubModelSync
|
|
80
80
|
|
81
81
|
private
|
82
82
|
|
83
|
-
def ps_define_commit_action(action, callback)
|
84
|
-
if PubSubModelSync::Config.enable_rails4_before_commit # rails 4 compatibility
|
85
|
-
define_method("ps_before_#{action}_commit") { instance_exec(action, &callback) }
|
86
|
-
else
|
87
|
-
commit_name = respond_to?(:before_commit) ? :before_commit : :after_commit
|
88
|
-
send(commit_name, on: action) { instance_exec(action, &callback) }
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
83
|
# Initialize calls to start and end pub_sub transactions and deliver all them in the same order
|
93
84
|
def ps_init_transaction_callbacks
|
94
85
|
start_transaction = lambda do
|
@@ -24,7 +24,7 @@ module PubSubModelSync
|
|
24
24
|
# @return (String): Json Format
|
25
25
|
def encode_payload(payload)
|
26
26
|
data = payload.to_h
|
27
|
-
not_important_keys = %i[
|
27
|
+
not_important_keys = %i[forced_ordering_key]
|
28
28
|
reduce_payload_size = !config.debug
|
29
29
|
data[:headers].except!(*not_important_keys) if reduce_payload_size
|
30
30
|
data.to_json
|
@@ -5,7 +5,7 @@ module PubSubModelSync
|
|
5
5
|
PUBLISHER_KLASS = PubSubModelSync::MessagePublisher
|
6
6
|
attr_accessor :key, :payloads, :max_buffer, :root, :children, :finished
|
7
7
|
|
8
|
-
# @param key (String,
|
8
|
+
# @param key (String,Nil) Transaction key, if empty will use the ordering_key from first payload
|
9
9
|
# @param max_buffer (Integer) Once this quantity of notifications is reached, then all notifications
|
10
10
|
# will immediately be delivered.
|
11
11
|
# Note: There is no way to rollback delivered notifications if current transaction fails
|
@@ -19,10 +19,11 @@ module PubSubModelSync
|
|
19
19
|
# @param payload (Payload)
|
20
20
|
def add_payload(payload)
|
21
21
|
payloads << payload
|
22
|
-
|
22
|
+
print_log = config.debug && max_buffer > 1
|
23
|
+
log("Payload added to current transaction: #{payload.inspect}") if print_log
|
23
24
|
return unless payloads.count >= max_buffer
|
24
25
|
|
25
|
-
log("Payloads buffer was filled, delivering current payloads: #{payloads.count}")
|
26
|
+
log("Payloads buffer was filled, delivering current payloads: #{payloads.count}") if print_log
|
26
27
|
deliver_payloads
|
27
28
|
end
|
28
29
|
|
@@ -60,14 +61,14 @@ module PubSubModelSync
|
|
60
61
|
private
|
61
62
|
|
62
63
|
def deliver_payloads
|
63
|
-
payloads.each
|
64
|
-
begin # rubocop:disable Style/RedundantBegin (ruby 2.4 support)
|
65
|
-
PUBLISHER_KLASS.connector_publish(payload)
|
66
|
-
rescue => e
|
67
|
-
PUBLISHER_KLASS.send(:notify_error, e, payload)
|
68
|
-
end
|
69
|
-
end
|
64
|
+
payloads.each(&method(:deliver_payload))
|
70
65
|
self.payloads = []
|
71
66
|
end
|
67
|
+
|
68
|
+
def deliver_payload(payload)
|
69
|
+
PUBLISHER_KLASS.connector_publish(payload)
|
70
|
+
rescue => e
|
71
|
+
PUBLISHER_KLASS.send(:notify_error, e, payload)
|
72
|
+
end
|
72
73
|
end
|
73
74
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pub_sub_model_sync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Owen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -111,7 +111,6 @@ files:
|
|
111
111
|
- lib/pub_sub_model_sync/base.rb
|
112
112
|
- lib/pub_sub_model_sync/config.rb
|
113
113
|
- lib/pub_sub_model_sync/connector.rb
|
114
|
-
- lib/pub_sub_model_sync/initializers/before_commit.rb
|
115
114
|
- lib/pub_sub_model_sync/message_processor.rb
|
116
115
|
- lib/pub_sub_model_sync/message_publisher.rb
|
117
116
|
- lib/pub_sub_model_sync/mock_google_service.rb
|
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Rails 4 backward compatibility (Add "simple" ps_before_*_commit callbacks)
|
4
|
-
ActiveRecord::ConnectionAdapters::RealTransaction.class_eval do
|
5
|
-
alias_method :commit_without_before_commit, :commit
|
6
|
-
|
7
|
-
def commit
|
8
|
-
call_before_commit_records if PubSubModelSync::Config.enable_rails4_before_commit
|
9
|
-
commit_without_before_commit
|
10
|
-
end
|
11
|
-
|
12
|
-
private
|
13
|
-
|
14
|
-
def call_before_commit_records
|
15
|
-
ite = records.uniq
|
16
|
-
ite.each do |record|
|
17
|
-
action = record.previous_changes.include?(:id) ? :create : :update
|
18
|
-
action = :destroy if record.destroyed?
|
19
|
-
callback_name = "ps_before_#{action}_commit".to_sym
|
20
|
-
record.send(callback_name) if record.respond_to?(callback_name)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|