async-background 0.2.5 → 0.3.0
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/lib/async/background/metrics.rb +143 -0
- data/lib/async/background/min_heap.rb +5 -0
- data/lib/async/background/runner.rb +25 -19
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background.rb +2 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00b7ba6035c743fb4c4568d2309da4bc7f454a3cbe5e17ac5d2251a417a4a31c
|
|
4
|
+
data.tar.gz: a3dff9b2b51f590a095120a18f1e3d5424b339ab5a6cff90b148f8e9316ee242
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9c36893b1cb14ba251d9c5a1c614ac50b988ffcbd69c43a876c1e0a2995ee749592618c2e328d2e05573ccf46186616a59615d7c01a785b28fe5897a1e0650f
|
|
7
|
+
data.tar.gz: 3f47cf30226b45b6c543f48812e95778206e9cccc07445c5066e32cdf88a71f68bf88be9f0134750344cd7e0ed8df1a907107d0e5a3c910ce00a3a493ea700e7
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
class Metrics
|
|
6
|
+
SCHEMA_FIELDS = {
|
|
7
|
+
total_runs: :u64,
|
|
8
|
+
total_successes: :u64,
|
|
9
|
+
total_failures: :u64,
|
|
10
|
+
total_timeouts: :u64,
|
|
11
|
+
total_skips: :u64,
|
|
12
|
+
active_jobs: :u32,
|
|
13
|
+
last_run_at: :u64,
|
|
14
|
+
last_duration_ms: :u32
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :registry
|
|
18
|
+
|
|
19
|
+
def initialize(worker_index:, total_workers:, shm_path: self.class.default_shm_path)
|
|
20
|
+
require 'async/utilization'
|
|
21
|
+
|
|
22
|
+
@registry = nil
|
|
23
|
+
@enabled = false
|
|
24
|
+
@registry = ::Async::Utilization::Registry.new
|
|
25
|
+
@enabled = true
|
|
26
|
+
|
|
27
|
+
ensure_shm!(total_workers, shm_path)
|
|
28
|
+
attach_observer!(worker_index, total_workers, shm_path)
|
|
29
|
+
rescue LoadError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def enabled?
|
|
33
|
+
@enabled
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def job_started(entry)
|
|
37
|
+
return unless @enabled
|
|
38
|
+
|
|
39
|
+
@registry.increment(:total_runs)
|
|
40
|
+
@registry.increment(:active_jobs)
|
|
41
|
+
@registry.set(:last_run_at, Process.clock_gettime(Process::CLOCK_REALTIME).to_i)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def job_finished(entry, duration)
|
|
45
|
+
return unless @enabled
|
|
46
|
+
|
|
47
|
+
@registry.decrement(:active_jobs)
|
|
48
|
+
@registry.increment(:total_successes)
|
|
49
|
+
@registry.set(:last_duration_ms, (duration * 1000).to_i)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def job_failed(entry, error)
|
|
53
|
+
return unless @enabled
|
|
54
|
+
|
|
55
|
+
@registry.decrement(:active_jobs)
|
|
56
|
+
@registry.increment(:total_failures)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def job_timed_out(entry)
|
|
60
|
+
return unless @enabled
|
|
61
|
+
|
|
62
|
+
@registry.decrement(:active_jobs)
|
|
63
|
+
@registry.increment(:total_timeouts)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def job_skipped(entry)
|
|
67
|
+
return unless @enabled
|
|
68
|
+
|
|
69
|
+
@registry.increment(:total_skips)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def values
|
|
73
|
+
return {} unless @enabled
|
|
74
|
+
|
|
75
|
+
@registry.values
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.schema
|
|
79
|
+
require 'async/utilization'
|
|
80
|
+
::Async::Utilization::Schema.build(SCHEMA_FIELDS)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Read metrics for all workers from the shm file.
|
|
84
|
+
# No server needed — just reads the mmap'd file.
|
|
85
|
+
#
|
|
86
|
+
# Async::Background::Metrics.read_all(total_workers: 2)
|
|
87
|
+
# # => [
|
|
88
|
+
# # { worker: 1, total_runs: 142, active_jobs: 1, ... },
|
|
89
|
+
# # { worker: 2, total_runs: 98, active_jobs: 0, ... }
|
|
90
|
+
# # ]
|
|
91
|
+
#
|
|
92
|
+
def self.read_all(total_workers:, path: default_shm_path)
|
|
93
|
+
require 'async/utilization'
|
|
94
|
+
|
|
95
|
+
s = schema
|
|
96
|
+
segment = segment_size
|
|
97
|
+
file_size = segment * total_workers
|
|
98
|
+
|
|
99
|
+
buffer = File.open(path, "rb") do |f|
|
|
100
|
+
IO::Buffer.map(f, file_size, 0)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
(1..total_workers).map do |i|
|
|
104
|
+
base = (i - 1) * segment
|
|
105
|
+
row = { worker: i }
|
|
106
|
+
s.fields.each do |field|
|
|
107
|
+
row[field.name] = buffer.get_value(field.type, base + field.offset)
|
|
108
|
+
end
|
|
109
|
+
row
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.default_shm_path
|
|
114
|
+
File.join(Dir.tmpdir, "async-background.shm")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.segment_size
|
|
118
|
+
SCHEMA_FIELDS.sum { |_, type| IO::Buffer.size_of(type) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def ensure_shm!(total_workers, path)
|
|
124
|
+
required = self.class.segment_size * total_workers
|
|
125
|
+
|
|
126
|
+
File.open(path, File::CREAT | File::RDWR, 0644) do |f|
|
|
127
|
+
f.flock(File::LOCK_EX)
|
|
128
|
+
f.truncate(required) if f.size < required
|
|
129
|
+
f.flock(File::LOCK_UN)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def attach_observer!(worker_index, total_workers, path)
|
|
134
|
+
segment = self.class.segment_size
|
|
135
|
+
offset = (worker_index - 1) * segment
|
|
136
|
+
observer = ::Async::Utilization::Observer.open(
|
|
137
|
+
self.class.schema, path, segment, offset
|
|
138
|
+
)
|
|
139
|
+
@registry.observer = observer
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -12,7 +12,7 @@ module Async
|
|
|
12
12
|
MAX_JITTER = 5
|
|
13
13
|
|
|
14
14
|
class Runner
|
|
15
|
-
attr_reader :logger, :semaphore, :heap, :worker_index, :total_workers, :shutdown
|
|
15
|
+
attr_reader :logger, :semaphore, :heap, :worker_index, :total_workers, :shutdown, :metrics
|
|
16
16
|
|
|
17
17
|
def initialize(config_path:, job_count: 2, worker_index:, total_workers:)
|
|
18
18
|
@logger = Console.logger
|
|
@@ -20,6 +20,7 @@ module Async
|
|
|
20
20
|
@total_workers = total_workers
|
|
21
21
|
@running = true
|
|
22
22
|
@shutdown = ::Async::Condition.new
|
|
23
|
+
@metrics = Metrics.new(worker_index: worker_index, total_workers: total_workers)
|
|
23
24
|
|
|
24
25
|
logger.info { "Async::Background worker_index=#{worker_index}/#{total_workers}, job_count=#{job_count}" }
|
|
25
26
|
|
|
@@ -30,6 +31,7 @@ module Async
|
|
|
30
31
|
def run
|
|
31
32
|
Async do |task|
|
|
32
33
|
setup_signal_handlers
|
|
34
|
+
start_signal_watcher(task)
|
|
33
35
|
|
|
34
36
|
loop do
|
|
35
37
|
entry = heap.peek
|
|
@@ -41,13 +43,12 @@ module Async
|
|
|
41
43
|
break unless running?
|
|
42
44
|
|
|
43
45
|
now = monotonic_now
|
|
44
|
-
while (
|
|
46
|
+
while (entry = heap.peek) && entry.next_run_at <= now
|
|
45
47
|
break unless running?
|
|
46
48
|
|
|
47
|
-
entry = heap.pop
|
|
48
|
-
|
|
49
49
|
if entry.running
|
|
50
50
|
logger.warn('Async::Background') { "#{entry.name}: skipped, previous run still active" }
|
|
51
|
+
metrics.job_skipped(entry)
|
|
51
52
|
else
|
|
52
53
|
entry.running = true
|
|
53
54
|
semaphore.async do
|
|
@@ -58,7 +59,7 @@ module Async
|
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
entry.reschedule(monotonic_now)
|
|
61
|
-
heap.
|
|
62
|
+
heap.replace_top(entry)
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
@@ -91,21 +92,20 @@ module Async
|
|
|
91
92
|
end
|
|
92
93
|
end
|
|
93
94
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@signal_r.read_nonblock(256) rescue nil
|
|
103
|
-
shutdown.signal
|
|
95
|
+
def start_signal_watcher(task)
|
|
96
|
+
task.async(transient: true) do
|
|
97
|
+
loop do
|
|
98
|
+
@signal_r.wait_readable
|
|
99
|
+
@signal_r.read_nonblock(256) rescue nil
|
|
100
|
+
shutdown.signal
|
|
101
|
+
break unless running?
|
|
102
|
+
end
|
|
104
103
|
end
|
|
104
|
+
end
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
def wait_with_shutdown(task, duration)
|
|
107
|
+
task.with_timeout(duration) { shutdown.wait }
|
|
108
|
+
rescue ::Async::TimeoutError
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
def build_heap(config_path)
|
|
@@ -182,14 +182,20 @@ module Async
|
|
|
182
182
|
end
|
|
183
183
|
|
|
184
184
|
def run_job(task, entry)
|
|
185
|
+
metrics.job_started(entry)
|
|
185
186
|
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
186
187
|
task.with_timeout(entry.timeout) { entry.job_class.perform_now }
|
|
188
|
+
|
|
189
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t
|
|
190
|
+
metrics.job_finished(entry, duration)
|
|
187
191
|
logger.info('Async::Background') {
|
|
188
|
-
"#{entry.name}: completed in #{
|
|
192
|
+
"#{entry.name}: completed in #{duration.round(2)}s"
|
|
189
193
|
}
|
|
190
194
|
rescue ::Async::TimeoutError
|
|
195
|
+
metrics.job_timed_out(entry)
|
|
191
196
|
logger.error('Async::Background') { "#{entry.name}: timed out after #{entry.timeout}s" }
|
|
192
197
|
rescue => e
|
|
198
|
+
metrics.job_failed(entry, e)
|
|
193
199
|
logger.error('Async::Background') {
|
|
194
200
|
"#{entry.name}: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}"
|
|
195
201
|
}
|
data/lib/async/background.rb
CHANGED
|
@@ -4,8 +4,10 @@ require 'async'
|
|
|
4
4
|
require 'async/semaphore'
|
|
5
5
|
require 'console'
|
|
6
6
|
require 'fugit'
|
|
7
|
+
require 'tmpdir'
|
|
7
8
|
|
|
8
9
|
require_relative 'background/version'
|
|
9
10
|
require_relative 'background/min_heap'
|
|
10
11
|
require_relative 'background/entry'
|
|
12
|
+
require_relative 'background/metrics'
|
|
11
13
|
require_relative 'background/runner'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-background
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Hajdarov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -92,6 +92,7 @@ extra_rdoc_files: []
|
|
|
92
92
|
files:
|
|
93
93
|
- lib/async/background.rb
|
|
94
94
|
- lib/async/background/entry.rb
|
|
95
|
+
- lib/async/background/metrics.rb
|
|
95
96
|
- lib/async/background/min_heap.rb
|
|
96
97
|
- lib/async/background/runner.rb
|
|
97
98
|
- lib/async/background/version.rb
|
|
@@ -117,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
117
118
|
- !ruby/object:Gem::Version
|
|
118
119
|
version: '0'
|
|
119
120
|
requirements: []
|
|
120
|
-
rubygems_version: 3.
|
|
121
|
+
rubygems_version: 3.3.27
|
|
121
122
|
signing_key:
|
|
122
123
|
specification_version: 4
|
|
123
124
|
summary: Lightweight heap-based cron/interval scheduler for Async.
|