async-background 0.4.5 → 0.5.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/clock.rb +25 -0
- data/lib/async/background/entry.rb +1 -3
- data/lib/async/background/job.rb +33 -0
- data/lib/async/background/queue/client.rb +38 -12
- data/lib/async/background/queue/notifier.rb +3 -1
- data/lib/async/background/queue/store.rb +15 -15
- data/lib/async/background/runner.rb +15 -17
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background.rb +3 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5959a648b21b8312d1b2d8d566ade149e736b6386eff0cf1fb9acd09801ba748
|
|
4
|
+
data.tar.gz: 839fd36e05ee3ef845ba1f2afaef50a5c996edf9812240a61198cb2f203d0e9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 408c3267a1acd854448a4dec42d0a3ab30a3ed8e40d40e5c841aca6c29cd2dc3378683ef7c5b305fa419b4fefea80292968efa67e1ff6eb8a4de5d7a5c403e00
|
|
7
|
+
data.tar.gz: 7d7af316122b5208e7adaaf681be8075821936fda225e78def6f8a9b76cf5350e50cd0bf6015888e585c149101c741de2d5f52ba82ffa4aa35734a3d86183511
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
# Shared clock helpers used across Runner, Queue::Store, and Queue::Client.
|
|
6
|
+
#
|
|
7
|
+
# monotonic_now — CLOCK_MONOTONIC, for in-process intervals and durations
|
|
8
|
+
# (immune to NTP drift / wall-clock jumps)
|
|
9
|
+
#
|
|
10
|
+
# realtime_now — CLOCK_REALTIME, for persisted timestamps (SQLite run_at,
|
|
11
|
+
# created_at, locked_at) and human-readable metrics
|
|
12
|
+
#
|
|
13
|
+
module Clock
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def monotonic_now
|
|
17
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def realtime_now
|
|
21
|
+
Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
module Async
|
|
4
4
|
module Background
|
|
5
5
|
class Entry
|
|
6
|
-
MIN_SLEEP_TIME = 0.1
|
|
7
|
-
|
|
8
6
|
attr_reader :name, :job_class, :interval, :cron, :timeout
|
|
9
7
|
attr_accessor :next_run_at, :running
|
|
10
8
|
|
|
@@ -25,7 +23,7 @@ module Async
|
|
|
25
23
|
else
|
|
26
24
|
now_wall = Time.now
|
|
27
25
|
wait = cron.next_time(now_wall).to_f - now_wall.to_f
|
|
28
|
-
@next_run_at = monotonic_now + [wait, MIN_SLEEP_TIME].max
|
|
26
|
+
@next_run_at = monotonic_now + [wait, Async::Background::MIN_SLEEP_TIME].max
|
|
29
27
|
end
|
|
30
28
|
end
|
|
31
29
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Job
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def perform_now(*args)
|
|
12
|
+
new.perform(*args)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def perform_async(*args)
|
|
16
|
+
Async::Background::Queue.enqueue(self, *args)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def perform_in(delay, *args)
|
|
20
|
+
Async::Background::Queue.enqueue_in(delay, self, *args)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def perform_at(time, *args)
|
|
24
|
+
Async::Background::Queue.enqueue_at(time, self, *args)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def perform(*args)
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #perform"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,38 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../clock'
|
|
4
|
+
|
|
3
5
|
module Async
|
|
4
6
|
module Background
|
|
5
7
|
module Queue
|
|
6
|
-
# Usage:
|
|
7
|
-
# Async::Background::Queue.enqueue(SendEmailJob, user_id, "welcome")
|
|
8
|
-
#
|
|
9
8
|
class Client
|
|
9
|
+
include Clock
|
|
10
|
+
|
|
10
11
|
def initialize(store:, notifier: nil)
|
|
11
12
|
@store = store
|
|
12
13
|
@notifier = notifier
|
|
13
14
|
end
|
|
14
15
|
|
|
15
|
-
def push(class_name, args = [])
|
|
16
|
-
id = @store.enqueue(class_name, args)
|
|
16
|
+
def push(class_name, args = [], run_at = nil)
|
|
17
|
+
id = @store.enqueue(class_name, args, run_at)
|
|
17
18
|
@notifier&.notify
|
|
18
19
|
id
|
|
19
20
|
end
|
|
21
|
+
|
|
22
|
+
def push_in(delay, class_name, args = [])
|
|
23
|
+
run_at = realtime_now + delay.to_f
|
|
24
|
+
push(class_name, args, run_at)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def push_at(time, class_name, args = [])
|
|
28
|
+
run_at = time.respond_to?(:to_f) ? time.to_f : time
|
|
29
|
+
push(class_name, args, run_at)
|
|
30
|
+
end
|
|
20
31
|
end
|
|
21
32
|
|
|
22
33
|
class << self
|
|
23
34
|
attr_accessor :default_client
|
|
24
35
|
|
|
25
36
|
def enqueue(job_class, *args)
|
|
37
|
+
ensure_configured!
|
|
38
|
+
default_client.push(resolve_class_name(job_class), args)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def enqueue_in(delay, job_class, *args)
|
|
42
|
+
ensure_configured!
|
|
43
|
+
default_client.push_in(delay, resolve_class_name(job_class), args)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def enqueue_at(time, job_class, *args)
|
|
47
|
+
ensure_configured!
|
|
48
|
+
default_client.push_at(time, resolve_class_name(job_class), args)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def ensure_configured!
|
|
26
54
|
raise "Async::Background::Queue not configured" unless default_client
|
|
55
|
+
end
|
|
27
56
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
raise ArgumentError, "#{job_class} must implement .perform_now" unless job_class.respond_to?(:perform_now)
|
|
32
|
-
class_name = job_class.name
|
|
33
|
-
end
|
|
57
|
+
def resolve_class_name(job_class)
|
|
58
|
+
return job_class if job_class.is_a?(String)
|
|
59
|
+
return job_class.name if job_class.respond_to?(:perform_now)
|
|
34
60
|
|
|
35
|
-
|
|
61
|
+
raise ArgumentError, "#{job_class} must include Async::Background::Job"
|
|
36
62
|
end
|
|
37
63
|
end
|
|
38
64
|
end
|
|
@@ -4,6 +4,8 @@ module Async
|
|
|
4
4
|
module Background
|
|
5
5
|
module Queue
|
|
6
6
|
class Notifier
|
|
7
|
+
IO_ERRORS = [IO::WaitReadable, EOFError, IOError].freeze
|
|
8
|
+
|
|
7
9
|
attr_reader :reader, :writer
|
|
8
10
|
|
|
9
11
|
def initialize
|
|
@@ -51,7 +53,7 @@ module Async
|
|
|
51
53
|
def drain
|
|
52
54
|
loop do
|
|
53
55
|
@reader.read_nonblock(256)
|
|
54
|
-
rescue
|
|
56
|
+
rescue *IO_ERRORS
|
|
55
57
|
break
|
|
56
58
|
end
|
|
57
59
|
nil
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require_relative '../clock'
|
|
4
5
|
|
|
5
6
|
module Async
|
|
6
7
|
module Background
|
|
7
8
|
module Queue
|
|
8
9
|
class Store
|
|
10
|
+
include Clock
|
|
11
|
+
|
|
9
12
|
SCHEMA = <<~SQL
|
|
10
13
|
PRAGMA auto_vacuum = INCREMENTAL;
|
|
11
14
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
@@ -14,10 +17,11 @@ module Async
|
|
|
14
17
|
args TEXT NOT NULL DEFAULT '[]',
|
|
15
18
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
16
19
|
created_at REAL NOT NULL,
|
|
20
|
+
run_at REAL NOT NULL,
|
|
17
21
|
locked_by INTEGER,
|
|
18
22
|
locked_at REAL
|
|
19
23
|
);
|
|
20
|
-
CREATE INDEX IF NOT EXISTS
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status_run_at_id ON jobs(status, run_at, id);
|
|
21
25
|
SQL
|
|
22
26
|
|
|
23
27
|
MMAP_SIZE = 268_435_456
|
|
@@ -56,16 +60,20 @@ module Async
|
|
|
56
60
|
@schema_checked = true
|
|
57
61
|
end
|
|
58
62
|
|
|
59
|
-
def enqueue(class_name, args = [])
|
|
63
|
+
def enqueue(class_name, args = [], run_at = nil)
|
|
60
64
|
ensure_connection
|
|
61
|
-
|
|
65
|
+
run_at ||= realtime_now
|
|
66
|
+
@enqueue_stmt.execute(class_name, JSON.generate(args), realtime_now, run_at)
|
|
62
67
|
@db.last_insert_row_id
|
|
63
68
|
end
|
|
64
69
|
|
|
65
70
|
def fetch(worker_id)
|
|
66
71
|
ensure_connection
|
|
72
|
+
now = realtime_now
|
|
67
73
|
@db.execute("BEGIN IMMEDIATE")
|
|
68
|
-
|
|
74
|
+
results = @fetch_stmt.execute(worker_id, now, now)
|
|
75
|
+
row = results.first
|
|
76
|
+
@fetch_stmt.reset!
|
|
69
77
|
@db.execute("COMMIT")
|
|
70
78
|
return unless row
|
|
71
79
|
|
|
@@ -75,7 +83,6 @@ module Async
|
|
|
75
83
|
@db.execute("ROLLBACK") rescue nil
|
|
76
84
|
raise
|
|
77
85
|
end
|
|
78
|
-
|
|
79
86
|
def complete(job_id)
|
|
80
87
|
ensure_connection
|
|
81
88
|
@complete_stmt.execute(job_id)
|
|
@@ -134,7 +141,7 @@ module Async
|
|
|
134
141
|
|
|
135
142
|
def prepare_statements
|
|
136
143
|
@enqueue_stmt = @db.prepare(
|
|
137
|
-
"INSERT INTO jobs (class_name, args, created_at) VALUES (?, ?, ?)"
|
|
144
|
+
"INSERT INTO jobs (class_name, args, created_at, run_at) VALUES (?, ?, ?, ?)"
|
|
138
145
|
)
|
|
139
146
|
|
|
140
147
|
@fetch_stmt = @db.prepare(<<~SQL)
|
|
@@ -142,8 +149,8 @@ module Async
|
|
|
142
149
|
SET status = 'running', locked_by = ?, locked_at = ?
|
|
143
150
|
WHERE id = (
|
|
144
151
|
SELECT id FROM jobs
|
|
145
|
-
WHERE status = 'pending'
|
|
146
|
-
ORDER BY id
|
|
152
|
+
WHERE status = 'pending' AND run_at <= ?
|
|
153
|
+
ORDER BY run_at, id
|
|
147
154
|
LIMIT 1
|
|
148
155
|
)
|
|
149
156
|
RETURNING id, class_name, args
|
|
@@ -180,13 +187,6 @@ module Async
|
|
|
180
187
|
end
|
|
181
188
|
end
|
|
182
189
|
|
|
183
|
-
def realtime_now
|
|
184
|
-
Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def monotonic_now
|
|
188
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
189
|
-
end
|
|
190
190
|
end
|
|
191
191
|
end
|
|
192
192
|
end
|
|
@@ -7,12 +7,14 @@ module Async
|
|
|
7
7
|
module Background
|
|
8
8
|
class ConfigError < StandardError; end
|
|
9
9
|
|
|
10
|
-
DEFAULT_TIMEOUT
|
|
11
|
-
MIN_SLEEP_TIME
|
|
12
|
-
MAX_JITTER
|
|
10
|
+
DEFAULT_TIMEOUT = 30
|
|
11
|
+
MIN_SLEEP_TIME = 0.1
|
|
12
|
+
MAX_JITTER = 5
|
|
13
13
|
QUEUE_POLL_INTERVAL = 5
|
|
14
14
|
|
|
15
15
|
class Runner
|
|
16
|
+
include Clock
|
|
17
|
+
|
|
16
18
|
attr_reader :logger, :semaphore, :heap, :worker_index, :total_workers, :shutdown, :metrics, :queue_store
|
|
17
19
|
|
|
18
20
|
def initialize(
|
|
@@ -53,7 +55,7 @@ module Async
|
|
|
53
55
|
@running = false
|
|
54
56
|
logger.info { "Async::Background: stopping gracefully" }
|
|
55
57
|
shutdown.signal
|
|
56
|
-
@queue_notifier&.notify
|
|
58
|
+
@queue_notifier&.notify
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
def running?
|
|
@@ -96,13 +98,13 @@ module Async
|
|
|
96
98
|
job = @queue_store.fetch(worker_index)
|
|
97
99
|
break unless job
|
|
98
100
|
|
|
99
|
-
semaphore.async { run_queue_job(
|
|
101
|
+
semaphore.async { |job_task| run_queue_job(job_task, job) }
|
|
100
102
|
end
|
|
101
103
|
end
|
|
102
104
|
end
|
|
103
105
|
end
|
|
104
106
|
|
|
105
|
-
def run_queue_job(
|
|
107
|
+
def run_queue_job(job_task, job)
|
|
106
108
|
class_name = job[:class_name]
|
|
107
109
|
args = job[:args]
|
|
108
110
|
klass = resolve_job_class(class_name)
|
|
@@ -110,7 +112,7 @@ module Async
|
|
|
110
112
|
metrics.job_started(nil)
|
|
111
113
|
t = monotonic_now
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
job_task.with_timeout(DEFAULT_TIMEOUT) { klass.perform_now(*args) }
|
|
114
116
|
|
|
115
117
|
duration = monotonic_now - t
|
|
116
118
|
metrics.job_finished(nil, duration)
|
|
@@ -140,7 +142,7 @@ module Async
|
|
|
140
142
|
mod.const_get(name, false)
|
|
141
143
|
end
|
|
142
144
|
|
|
143
|
-
raise ConfigError, "#{class_name} must
|
|
145
|
+
raise ConfigError, "#{class_name} must include Async::Background::Job" unless klass.respond_to?(:perform_now)
|
|
144
146
|
|
|
145
147
|
klass
|
|
146
148
|
end
|
|
@@ -164,8 +166,8 @@ module Async
|
|
|
164
166
|
metrics.job_skipped(entry)
|
|
165
167
|
else
|
|
166
168
|
entry.running = true
|
|
167
|
-
semaphore.async do
|
|
168
|
-
run_job(
|
|
169
|
+
semaphore.async do |job_task|
|
|
170
|
+
run_job(job_task, entry)
|
|
169
171
|
ensure
|
|
170
172
|
entry.running = false
|
|
171
173
|
end
|
|
@@ -252,7 +254,7 @@ module Async
|
|
|
252
254
|
raise ConfigError, "[#{name}] unknown class: #{class_name}"
|
|
253
255
|
end
|
|
254
256
|
|
|
255
|
-
raise ConfigError, "[#{name}] #{class_name} must
|
|
257
|
+
raise ConfigError, "[#{name}] #{class_name} must include Async::Background::Job" unless job_class.respond_to?(:perform_now)
|
|
256
258
|
|
|
257
259
|
interval = config['every']&.then { |v|
|
|
258
260
|
int = v.to_i
|
|
@@ -274,14 +276,10 @@ module Async
|
|
|
274
276
|
}
|
|
275
277
|
end
|
|
276
278
|
|
|
277
|
-
def
|
|
278
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def run_job(task, entry)
|
|
279
|
+
def run_job(job_task, entry)
|
|
282
280
|
metrics.job_started(entry)
|
|
283
281
|
t = monotonic_now
|
|
284
|
-
|
|
282
|
+
job_task.with_timeout(entry.timeout) { entry.job_class.perform_now }
|
|
285
283
|
|
|
286
284
|
duration = monotonic_now - t
|
|
287
285
|
metrics.job_finished(entry, duration)
|
data/lib/async/background.rb
CHANGED
|
@@ -7,7 +7,10 @@ require 'fugit'
|
|
|
7
7
|
require 'tmpdir'
|
|
8
8
|
|
|
9
9
|
require_relative 'background/version'
|
|
10
|
+
require_relative 'background/clock'
|
|
10
11
|
require_relative 'background/min_heap'
|
|
11
12
|
require_relative 'background/entry'
|
|
12
13
|
require_relative 'background/metrics'
|
|
13
14
|
require_relative 'background/runner'
|
|
15
|
+
require_relative 'background/queue/client'
|
|
16
|
+
require_relative 'background/job'
|
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.5.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-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -91,7 +91,9 @@ extensions: []
|
|
|
91
91
|
extra_rdoc_files: []
|
|
92
92
|
files:
|
|
93
93
|
- lib/async/background.rb
|
|
94
|
+
- lib/async/background/clock.rb
|
|
94
95
|
- lib/async/background/entry.rb
|
|
96
|
+
- lib/async/background/job.rb
|
|
95
97
|
- lib/async/background/metrics.rb
|
|
96
98
|
- lib/async/background/min_heap.rb
|
|
97
99
|
- lib/async/background/queue/client.rb
|