tobox 0.5.0 → 0.5.1

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: '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