tobox 0.5.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0481835fffcdbaf85fb53ebf116a6b117ad267cfb4a4efbcbae05b2f87058951'
4
- data.tar.gz: 39589407d92d28fc538aebb59a039a606d833aeb0eb26f82b7cf96228e73d5b3
3
+ metadata.gz: 15d95687a102c98fcc33859b3a369dc9026f04fa58f7f785bcad1d9ccb40010f
4
+ data.tar.gz: 656b296f9d72d67877945317d4a6d5505de63044f74e08c506970b2c841599f4
5
5
  SHA512:
6
- metadata.gz: 8a1137598cc58cb22573ccbec80f448aa55db3505a97b741cc146eaef6f16392ba3159b9db9f61498cea530dba33c431bba4f9c847325064a770a96553d9ec20
7
- data.tar.gz: 274be354130a8ae5ab59188ad98c4de208adac78cd7c9720b569db98de75d2ec0514a2560db05c080b9118e6967b573b7ea1fd11b0297edf9c07f05478acbefe
6
+ metadata.gz: 172b54fd340f865dbc26a30266f815fe556424cd150c8205c872da02d3725def786648710097b0880de41cc16940a39bd34c8592049d22a907d527ff84b26a3a
7
+ data.tar.gz: 1dafbf50e8d18c4eb7f4dfd31539322230bd1d11d403f250e4612a0579e878c366fe6b2d44eb24ae17f32476bd25994457563d3fd5ef1a46d7499dc92a93ee1d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.1] - 2024-09-26
4
+
5
+ ### Improvements
6
+
7
+ * Refactoring of management of event id which replaces `SELECT id IN (?)` resulting queries with `SELECT id = ?`.
8
+ * Process shutdown is now more predictable, also in the grace period.
9
+ * `grace_shutdown_timeout` is a new configuration, by default 5 (seconds).
10
+
3
11
  ## [0.5.0] - 2024-09-16
4
12
 
5
13
  ### Features
data/README.md CHANGED
@@ -241,6 +241,10 @@ Time (in seconds) to wait before checking again for events in the outbox.
241
241
 
242
242
  Time (in seconds) to wait for events to finishing processing, before hard-killing the process.
243
243
 
244
+ ### `grace_shutdown_timeout`
245
+
246
+ Grace period (in seconds) to wait after, hard-killing the work in progress, and before exiting the process.
247
+
244
248
  ### `on(event_type) { |before, after| }`
245
249
 
246
250
  callback executed when processing an event of the given type. By default, it'll yield the state of data before and after the event (unless `message_to_arguments` is set).
@@ -583,6 +587,7 @@ on_stats(5) do |stats_collector| # every 5 seconds
583
587
  # now you can send them to your statsd collector
584
588
  #
585
589
  StatsD.gauge('outbox_pending_backlog', stats[:pending_count])
590
+ StatsD.gauge('outbox_oldest_message_age', stats[:oldest_event_age_in_seconds])
586
591
  end
587
592
  ```
588
593
 
data/lib/tobox/cli.rb CHANGED
@@ -108,12 +108,18 @@ module Tobox
108
108
  opts[:tag] = arg
109
109
  end
110
110
 
111
- o.on "-t", "--shutdown-timeout NUM", Integer, "Shutdown timeout (in seconds)" do |arg|
111
+ o.on "-t", "--shutdown-timeout NUM", Float, "Shutdown timeout (in seconds)" do |arg|
112
112
  raise ArgumentError, "must be positive" unless arg.positive?
113
113
 
114
114
  opts[:shutdown_timeout] = arg
115
115
  end
116
116
 
117
+ o.on "-g", "--shutdown-grace-timeout NUM", Float, "Shutdown grace timeout (in seconds)" do |arg|
118
+ raise ArgumentError, "must be positive" unless arg.positive?
119
+
120
+ opts[:grace_shutdown_timeout] = arg
121
+ end
122
+
117
123
  o.on "--verbose", "Print more verbose output" do |arg|
118
124
  opts[:verbose] = arg
119
125
  end
@@ -24,6 +24,7 @@ module Tobox
24
24
  exponential_retry_factor: 2,
25
25
  wait_for_events_delay: 5,
26
26
  shutdown_timeout: 10,
27
+ grace_shutdown_timeout: 5,
27
28
  concurrency: 4, # TODO: CPU count
28
29
  worker: :thread
29
30
  }.freeze
data/lib/tobox/fetcher.rb CHANGED
@@ -38,27 +38,20 @@ module Tobox
38
38
  def fetch_events(&blk)
39
39
  num_events = 0
40
40
  events_tr do
41
- event_ids = EMPTY
41
+ event_id = nil
42
42
 
43
- event_ids_tr do
44
- event_ids = fetch_event_ids
45
- mark_as_fetched(event_ids)
43
+ event_id_tr do
44
+ event_id = fetch_event_id
45
+ mark_as_fetched(event_id) if event_id
46
46
  end
47
47
 
48
- evts = nil
48
+ if event_id
49
+ with_event(event_id) do |event|
50
+ num_events = 1
49
51
 
50
- unless event_ids.empty?
51
- with_events(event_ids) do |events|
52
- evts = events
53
- num_events = events.count
54
-
55
- evts = events.filter_map do |ev|
56
- prepare_event(ev, &blk)
57
- end
52
+ prepare_event(event, &blk)
58
53
  end
59
54
  end
60
-
61
- return 0 if evts.nil?
62
55
  end
63
56
 
64
57
  num_events
@@ -70,56 +63,52 @@ module Tobox
70
63
  event[:metadata] = try_json_parse(event[:metadata])
71
64
  handle_before_event(event)
72
65
  yield(to_message(event))
73
- event
74
66
  end
75
67
 
76
- def fetch_event_ids
68
+ def fetch_event_id
77
69
  @pick_next_sql.for_update
78
70
  .skip_locked
79
- .limit(1).select_map(:id) # lock starts here
71
+ .limit(1).select_map(:id).first # lock starts here
80
72
  end
81
73
 
82
- def mark_as_fetched(event_ids)
83
- @ds.where(id: event_ids).update(@mark_as_fetched_params) unless event_ids.empty?
74
+ def mark_as_fetched(event_id)
75
+ @ds.where(id: event_id).update(@mark_as_fetched_params)
84
76
  end
85
77
 
86
78
  def events_tr(&block)
87
79
  @db.transaction(savepoint: false, &block)
88
80
  end
89
81
 
90
- def event_ids_tr
82
+ def event_id_tr
91
83
  yield
92
84
  end
93
85
 
94
- def with_events(event_ids, &blk)
95
- events, error = yield_events(event_ids, &blk)
86
+ def with_event(event_id, &blk)
87
+ event, error = yield_event(event_id, &blk)
96
88
 
97
- events.each do |event|
98
- if error
99
- event.merge!(mark_as_error(event, error))
100
- handle_error_event(event, error)
101
- else
102
- handle_after_event(event)
103
- end
89
+ if error
90
+ event.merge!(mark_as_error(event, error))
91
+ handle_error_event(event, error)
92
+ else
93
+ handle_after_event(event)
104
94
  end
105
95
  end
106
96
 
107
- def yield_events(event_ids)
108
- events_ds = @ds.where(id: event_ids)
109
- events = EMPTY
110
- error = nil
97
+ def yield_event(event_id)
98
+ events_ds = @ds.where(id: event_id)
99
+ event = error = nil
111
100
 
112
101
  begin
113
- events = events_ds.all
102
+ event = events_ds.first
114
103
 
115
- yield events
104
+ yield event
116
105
 
117
106
  events_ds.delete
118
107
  rescue StandardError => e
119
108
  error = e
120
109
  end
121
110
 
122
- [events, error]
111
+ [event, error]
123
112
  end
124
113
 
125
114
  def log_message(msg)
@@ -16,7 +16,7 @@ module Tobox
16
16
 
17
17
  private
18
18
 
19
- def fetch_event_ids
19
+ def fetch_event_id
20
20
  group = @pick_next_sql.for_update
21
21
  .skip_locked
22
22
  .limit(1)
@@ -37,7 +37,7 @@ module Tobox
37
37
  end
38
38
 
39
39
  # lock all, process 1
40
- event_ids[0, 1]
40
+ event_ids.first
41
41
  end
42
42
  end
43
43
  end
@@ -23,7 +23,7 @@ module Tobox
23
23
  yield
24
24
  end
25
25
 
26
- def event_ids_tr(&block)
26
+ def event_id_tr(&block)
27
27
  @db.transaction(savepoint: false, &block)
28
28
  end
29
29
  end
@@ -5,34 +5,74 @@ require "fiber_scheduler"
5
5
 
6
6
  module Tobox
7
7
  class FiberPool < Pool
8
- def initialize(_configuration)
8
+ def initialize(_)
9
9
  Sequel.extension(:fiber_concurrency)
10
10
  super
11
+ @fibers = []
12
+
13
+ @fiber_mtx = Mutex.new
14
+ @fiber_cond = ConditionVariable.new
15
+ @fiber_thread = nil
11
16
  end
12
17
 
13
18
  def start
14
19
  @fiber_thread = Thread.start do
15
20
  Thread.current.name = "tobox-fibers-thread"
16
21
 
17
- FiberScheduler do
18
- @workers.each_with_index do |wk, _idx|
19
- Fiber.schedule { do_work(wk) }
22
+ begin
23
+ FiberScheduler do
24
+ @fiber_mtx.synchronize do
25
+ @workers.each do |worker|
26
+ @fibers << start_fiber_worker(worker)
27
+ end
28
+ @fiber_cond.signal
29
+ end
20
30
  end
31
+ rescue KillError
32
+ @fibers.each { |f| f.raise(KillError) }
21
33
  end
22
34
  end
35
+ @fiber_mtx.synchronize do
36
+ @fiber_cond.wait(@fiber_mtx)
37
+ end
23
38
  end
24
39
 
25
40
  def stop
26
41
  shutdown_timeout = @configuration[:shutdown_timeout]
42
+ grace_shutdown_timeout = @configuration[:grace_shutdown_timeout]
27
43
 
28
44
  super
29
45
 
30
- begin
31
- Timeout.timeout(shutdown_timeout) { @fiber_thread.value }
32
- rescue Timeout::Error
33
- # hard exit
34
- @fiber_thread.raise(KillError)
35
- @fiber_thread.value
46
+ @fiber_thread.join(shutdown_timeout)
47
+
48
+ return unless @fiber_thread.alive?
49
+
50
+ @fiber_thread.raise(KillError)
51
+ @fiber_thread.join(grace_shutdown_timeout)
52
+ @fiber_thread.kill
53
+ @fiber_thread.join(1)
54
+ end
55
+
56
+ private
57
+
58
+ def start_fiber_worker(worker)
59
+ Fiber.schedule do
60
+ do_work(worker)
61
+
62
+ @fiber_mtx.synchronize do
63
+ @fibers.delete(Fiber.current)
64
+
65
+ if worker.finished? && @running
66
+ idx = @workers.index(worker)
67
+
68
+ raise Error, "worker not found" unless idx
69
+
70
+ subst_worker = Worker.new(worker.label, @configuration)
71
+ @workers[idx] = subst_worker
72
+ subst_fiber = start_fiber_worker(subst_worker)
73
+ @fiber_mtx.synchronize { @fibers << subst_fiber }
74
+ end
75
+ end
36
76
  end
37
77
  end
38
78
  end
@@ -22,24 +22,35 @@ module Tobox
22
22
 
23
23
  def stop
24
24
  shutdown_timeout = @configuration[:shutdown_timeout]
25
-
26
- deadline = Process.clock_gettime(::Process::CLOCK_MONOTONIC)
25
+ grace_shutdown_timeout = @configuration[:grace_shutdown_timeout]
27
26
 
28
27
  super
29
28
  Thread.pass # let workers finish
30
29
 
31
30
  # soft exit
32
- while Process.clock_gettime(::Process::CLOCK_MONOTONIC) - deadline < shutdown_timeout
33
- return if @threads.empty?
31
+ join = lambda do |timeout|
32
+ start = Process.clock_gettime(::Process::CLOCK_MONOTONIC)
33
+
34
+ loop do
35
+ terminating_th = @threads.synchronize { @threads.first }
36
+
37
+ return unless terminating_th
38
+
39
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
34
40
 
35
- sleep 0.5
41
+ break if elapsed > timeout
42
+
43
+ terminating_th.join(timeout - elapsed)
44
+ end
36
45
  end
37
46
 
47
+ join.call(shutdown_timeout)
48
+
38
49
  # hard exit
39
- @threads.each { |th| th.raise(KillError) }
40
- while (th = @threads.pop)
41
- th.value # waits
42
- end
50
+ @threads.synchronize { @threads.each { |th| th.raise(KillError) } }
51
+ join.call(grace_shutdown_timeout)
52
+ @threads.synchronize { @threads.each(&:kill) }
53
+ join.call(1)
43
54
  end
44
55
 
45
56
  private
@@ -63,8 +74,6 @@ module Tobox
63
74
  subst_thread = start_thread_worker(subst_worker)
64
75
  @threads << subst_thread
65
76
  end
66
- # all workers went down abruply, we need to kill the process.
67
- # @parent_thread.raise(Interrupt) if wk.finished? && @threads.empty? && @running
68
77
  end
69
78
  end
70
79
  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.0"
4
+ VERSION = "0.5.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tobox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
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-16 00:00:00.000000000 Z
11
+ date: 2024-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel