tobox 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15d95687a102c98fcc33859b3a369dc9026f04fa58f7f785bcad1d9ccb40010f
4
- data.tar.gz: 656b296f9d72d67877945317d4a6d5505de63044f74e08c506970b2c841599f4
3
+ metadata.gz: b8f6e85f173d4636a64617ede574141d4b2365799b774a6e56a76b70a59db6ba
4
+ data.tar.gz: 49bb80cc99034ba4db846964988c8d1fc2a1d8e062decbe57d5b786e892afb8b
5
5
  SHA512:
6
- metadata.gz: 172b54fd340f865dbc26a30266f815fe556424cd150c8205c872da02d3725def786648710097b0880de41cc16940a39bd34c8592049d22a907d527ff84b26a3a
7
- data.tar.gz: 1dafbf50e8d18c4eb7f4dfd31539322230bd1d11d403f250e4612a0579e878c366fe6b2d44eb24ae17f32476bd25994457563d3fd5ef1a46d7499dc92a93ee1d
6
+ metadata.gz: e1e3f588dbe62ceaf50d6d1e67e3fbcecf0798027728907961d2bd92d18ad5cf779d215c6cad614ace6bdf0c6bc00e8c32a2e3f0ddd30ae1c7f6c2f6524b3133
7
+ data.tar.gz: 6170813875d41ed03012ded3a418290446abfeac34a80fb5a7e21df3741032df23b05261d7b87c4c9ff3c2d230738ff20e93c7d178969606e092617d6f91357c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.2] - 2024-10-22
4
+
5
+ ## Bugfixes
6
+
7
+ * prevent `:max_connections` from being overidden.
8
+
9
+ ### Improvements
10
+
3
11
  ## [0.5.1] - 2024-09-26
4
12
 
5
13
  ### Improvements
data/README.md CHANGED
@@ -219,6 +219,20 @@ exponential_retry_factor 2
219
219
 
220
220
  **Note**: the new attempt will be retried in `n ** 4`, where `n` is the number of past attempts for that event.
221
221
 
222
+ ### `batch_size`
223
+
224
+ Number of events fetched in each outbox loop.
225
+
226
+ ```ruby
227
+ batch_size 10
228
+ ```
229
+
230
+ **Note**: event handlers will receive all events covered by the given callback in its arguments:
231
+
232
+ ```ruby
233
+ on(:event1, :event2) do |*events| # at most 10 events, may contain events of type 1 and 2
234
+ ```
235
+
222
236
  ### `concurrency`
223
237
 
224
238
  Number of workers processing events.
@@ -229,6 +243,21 @@ concurrency 4
229
243
 
230
244
  **Note**: the default concurrency is adapted and different for each worker pool type, so make sure you understand how this tweak may affect you.
231
245
 
246
+ ### `max_connections`
247
+
248
+ Number of database connections the workers will share to do their work.
249
+
250
+ In the (default) threaded worker mode, it'll default to the number of workers (set by the `concurrency` configuration).
251
+
252
+ ```ruby
253
+ # 10 workers sharing 4 database connections
254
+ max_connections 4
255
+ concurrency 10
256
+ ```
257
+
258
+ This can be useful when the database presents overhead in managing write intensive workload (such as the one an outbox generates), and you want to be able to scale without putting more work on the database.
259
+
260
+
232
261
  ### `worker`
233
262
 
234
263
  Type of the worker used to process events. Can be `:thread` (default), `:fiber`, or a class implementing the `Tobox::Pool` protocol (TBD: define what this protocol is).
@@ -20,6 +20,7 @@ module Tobox
20
20
  database_options: nil,
21
21
  table: :outbox,
22
22
  created_at_column: nil,
23
+ batch_size: 1,
23
24
  max_attempts: 10,
24
25
  exponential_retry_factor: 2,
25
26
  wait_for_events_delay: 5,
@@ -64,7 +65,7 @@ module Tobox
64
65
 
65
66
  @database = if @config[:database_uri]
66
67
  database_opts = @config[:database_options] || {}
67
- database_opts[:max_connections] = @config[:concurrency] if @config[:worker] == :thread
68
+ database_opts[:max_connections] ||= (@config[:concurrency] if @config[:worker] == :thread)
68
69
  db = Sequel.connect(@config[:database_uri].to_s, database_opts)
69
70
  Array(@lifecycle_events[:database_connect]).each { |cb| cb.call(db) }
70
71
  db
@@ -83,9 +84,10 @@ module Tobox
83
84
  freeze
84
85
  end
85
86
 
86
- def on(*events, &callback)
87
- events.each do |event|
88
- (@handlers[event.to_sym] ||= []) << callback
87
+ def on(*event_types, &callback)
88
+ callback_events = (@handlers[callback] ||= [])
89
+ event_types.each do |event_type|
90
+ callback_events << event_type.to_sym
89
91
  end
90
92
  self
91
93
  end
data/lib/tobox/fetcher.rb CHANGED
@@ -28,6 +28,8 @@ module Tobox
28
28
  .where(run_at_conds)
29
29
  .order(Sequel.desc(:run_at, nulls: :first), :id)
30
30
 
31
+ @batch_size = configuration[:batch_size]
32
+
31
33
  @mark_as_fetched_params = { attempts: Sequel[@table][:attempts] + 1, last_error: nil }
32
34
 
33
35
  @before_event_handlers = Array(@configuration.lifecycle_events[:before_event])
@@ -38,18 +40,19 @@ module Tobox
38
40
  def fetch_events(&blk)
39
41
  num_events = 0
40
42
  events_tr do
41
- event_id = nil
43
+ event_ids = nil
44
+ # @type var event_ids: Array[Integer]
42
45
 
43
46
  event_id_tr do
44
- event_id = fetch_event_id
45
- mark_as_fetched(event_id) if event_id
47
+ event_ids = fetch_event_ids
48
+ mark_as_fetched(event_ids) unless event_ids.empty?
46
49
  end
47
50
 
48
- if event_id
49
- with_event(event_id) do |event|
50
- num_events = 1
51
+ if event_ids && !event_ids.empty?
52
+ with_events(event_ids) do |events|
53
+ num_events = events.size
51
54
 
52
- prepare_event(event, &blk)
55
+ prepare_events(events, &blk)
53
56
  end
54
57
  end
55
58
  end
@@ -59,20 +62,23 @@ module Tobox
59
62
 
60
63
  private
61
64
 
62
- def prepare_event(event)
63
- event[:metadata] = try_json_parse(event[:metadata])
64
- handle_before_event(event)
65
- yield(to_message(event))
65
+ def prepare_events(events)
66
+ prepared_events = events.map do |event|
67
+ event[:metadata] = try_json_parse(event[:metadata]) if event[:metadata]
68
+ handle_before_event(event)
69
+ to_message(event)
70
+ end
71
+ yield(prepared_events)
66
72
  end
67
73
 
68
- def fetch_event_id
74
+ def fetch_event_ids
69
75
  @pick_next_sql.for_update
70
76
  .skip_locked
71
- .limit(1).select_map(:id).first # lock starts here
77
+ .limit(@batch_size).select_map(:id) # lock starts here
72
78
  end
73
79
 
74
- def mark_as_fetched(event_id)
75
- @ds.where(id: event_id).update(@mark_as_fetched_params)
80
+ def mark_as_fetched(event_ids)
81
+ @ds.where(id: event_ids).update(@mark_as_fetched_params)
76
82
  end
77
83
 
78
84
  def events_tr(&block)
@@ -83,32 +89,34 @@ module Tobox
83
89
  yield
84
90
  end
85
91
 
86
- def with_event(event_id, &blk)
87
- event, error = yield_event(event_id, &blk)
92
+ def with_events(event_ids, &blk)
93
+ events, error = yield_events(event_ids, &blk)
88
94
 
89
- if error
90
- event.merge!(mark_as_error(event, error))
91
- handle_error_event(event, error)
92
- else
93
- handle_after_event(event)
95
+ events.each do |event|
96
+ if error
97
+ event.merge!(mark_as_error(event, error))
98
+ handle_error_event(event, error)
99
+ else
100
+ handle_after_event(event)
101
+ end
94
102
  end
95
103
  end
96
104
 
97
- def yield_event(event_id)
98
- events_ds = @ds.where(id: event_id)
99
- event = error = nil
105
+ def yield_events(event_ids)
106
+ events_ds = @ds.where(id: event_ids)
107
+ events = error = nil
100
108
 
101
109
  begin
102
- event = events_ds.first
110
+ events = events_ds.all
103
111
 
104
- yield event
112
+ yield events
105
113
 
106
- events_ds.delete
114
+ events_ds.delete unless events.empty?
107
115
  rescue StandardError => e
108
116
  error = e
109
117
  end
110
118
 
111
- [event, error]
119
+ [events, error]
112
120
  end
113
121
 
114
122
  def log_message(msg)
@@ -141,15 +149,13 @@ module Tobox
141
149
  {
142
150
  id: event[:id],
143
151
  type: event[:type],
144
- before: try_json_parse(event[:data_before]),
145
- after: try_json_parse(event[:data_after]),
152
+ before: (try_json_parse(event[:data_before]) if event[:data_before]),
153
+ after: (try_json_parse(event[:data_after]) if event[:data_after]),
146
154
  at: event[:created_at]
147
155
  }
148
156
  end
149
157
 
150
158
  def try_json_parse(data)
151
- return unless data
152
-
153
159
  data = JSON.parse(data.to_s) unless data.respond_to?(:to_hash)
154
160
 
155
161
  data
@@ -16,7 +16,7 @@ module Tobox
16
16
 
17
17
  private
18
18
 
19
- def fetch_event_id
19
+ def fetch_event_ids
20
20
  group = @pick_next_sql.for_update
21
21
  .skip_locked
22
22
  .limit(1)
@@ -36,8 +36,8 @@ module Tobox
36
36
  event_ids = []
37
37
  end
38
38
 
39
- # lock all, process 1
40
- event_ids.first
39
+ # lock all
40
+ event_ids.first(@batch_size)
41
41
  end
42
42
  end
43
43
  end
@@ -20,24 +20,47 @@ module Tobox
20
20
 
21
21
  private
22
22
 
23
- def prepare_event(event, &blk)
24
- try_insert_inbox(event) { super }
23
+ def prepare_events(events)
24
+ try_insert_inbox(events) do |deduped_events|
25
+ super(deduped_events)
26
+ end
25
27
  end
26
28
 
27
- def try_insert_inbox(event)
29
+ def try_insert_inbox(events)
30
+ inboxed = nil
31
+
28
32
  if @inbox_ds.respond_to?(:supports_insert_conflict?) && @inbox_ds.supports_insert_conflict?
29
- ret = @inbox_ds.insert_conflict.insert(@inbox_column => event[@inbox_column])
33
+ if @inbox_ds.supports_returning?(:insert)
34
+ inboxed = @inbox_ds.insert_conflict
35
+ .returning(@inbox_column)
36
+ .multi_insert(events.map { |event| { @inbox_column => event[@inbox_column] } })
37
+ else
38
+ ret = @inbox_ds.insert_conflict.multi_insert(events.map do |event|
39
+ { @inbox_column => event[@inbox_column] }
40
+ end)
30
41
 
31
- return event unless ret
42
+ if ret
43
+ inboxed = @inbox_ds.where(@inbox_column => events.map do |ev|
44
+ ev[@inbox_column]
45
+ end).select_map(@inbox_column)
46
+ end
47
+ end
32
48
  else
33
- begin
49
+ inboxed = []
50
+ events.each do |event|
34
51
  @inbox_ds.insert(@inbox_column => event[@inbox_column])
52
+ inboxed << event[@inbox_column]
35
53
  rescue Sequel::UniqueConstraintViolation
36
- return event
54
+ # ignore
37
55
  end
56
+ return events if inboxed.empty?
38
57
  end
39
58
 
40
- yield
59
+ return events if inboxed && inboxed.empty?
60
+
61
+ yield events.select { |ev| inboxed.include?(ev[@inbox_column]) }
62
+
63
+ events
41
64
  end
42
65
  end
43
66
  end
data/lib/tobox/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tobox
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.2"
5
5
  end
data/lib/tobox/worker.rb CHANGED
@@ -33,15 +33,16 @@ module Tobox
33
33
  def do_work
34
34
  return if @finished
35
35
 
36
- sum_fetched_events = @fetcher.fetch_events do |event|
37
- event_type = event[:type].to_sym
38
- args = message_to_arguments(event)
39
-
40
- if @handlers.key?(event_type)
41
- @handlers[event_type].each do |handler|
42
- handler.call(args)
36
+ sum_fetched_events = @fetcher.fetch_events do |events|
37
+ cs = @handlers.each_with_object({}) do |(callback, event_types), bucket|
38
+ events.each do |event|
39
+ (bucket[callback] ||= []) << message_to_arguments(event) if event_types.include?(event[:type].to_sym)
43
40
  end
44
41
  end
42
+
43
+ cs.each do |callback, evs|
44
+ callback.call(*evs)
45
+ end
45
46
  end
46
47
 
47
48
  return if @finished
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tobox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - HoneyryderChuck
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-26 00:00:00.000000000 Z
11
+ date: 2024-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logger
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: sequel
15
29
  requirement: !ruby/object:Gem::Requirement