async-background 0.6.1 → 0.6.2
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/CHANGELOG.md +25 -0
- data/lib/async/background/job.rb +18 -6
- data/lib/async/background/queue/client.rb +30 -12
- data/lib/async/background/queue/store.rb +16 -12
- data/lib/async/background/runner.rb +4 -2
- data/lib/async/background/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1890dfac1632afedbbb20c367d4d770c3037f6201bb6fac77396f07481df477
|
|
4
|
+
data.tar.gz: 1b435f70b2fd290bd569c0c081d19ae94250a181367ada7d3b4d157c213fd5b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0e9c2398241b48d0cc8fe66ed0b227cca3c93a5969e7f753c0a8dbb08a41462626d73709ae9eece1a254f8567f4a9044958e96a2658a77ab8f6b11bbdb6a1c14
|
|
7
|
+
data.tar.gz: 702016f46a3bbaa255735defcc2746baafe66a26400d2ad9525b80d89848970143998a706ceb4644519433cfcd558d9936c0caae4f968e335a2f8b8496afdec1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.2
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- **Configurable timeout for queue jobs** — queue jobs previously used a hardcoded 30-second timeout (`DEFAULT_TIMEOUT`). Now configurable via `options` hash at two levels:
|
|
7
|
+
```ruby
|
|
8
|
+
# Class-level default
|
|
9
|
+
class HeavyImportJob
|
|
10
|
+
include Async::Background::Job
|
|
11
|
+
options timeout: 600
|
|
12
|
+
|
|
13
|
+
def perform(user_id) = # ...
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Call-site override (wins over class-level)
|
|
17
|
+
HeavyImportJob.perform_async(user_id, options: { timeout: 120 })
|
|
18
|
+
```
|
|
19
|
+
Priority: call-site `options:` → class-level `options` → `DEFAULT_TIMEOUT` (30s). Options are merged at enqueue time so the runner simply reads the final value from the payload
|
|
20
|
+
- **`options:` hash across the entire enqueue chain** — single extensible contract from `perform_async` through `Client` down to `Store`. Currently supports `:timeout`, designed to accommodate future keys (e.g. `:retry`) without API changes
|
|
21
|
+
- **`Job::Options` schema via `Data.define`** — declares known option keys with types and defaults. Unknown keys raise `ArgumentError`, invalid types raise `TypeError`. No manual validation code
|
|
22
|
+
- **`options TEXT` column in SQLite** — stores the merged options hash as JSON. Extensible without schema changes when new options are added
|
|
23
|
+
|
|
24
|
+
### Improvements
|
|
25
|
+
- **Queue timeout logged on failure** — `run_queue_job` error log now includes actual timeout value: `"timed out after 120s"` instead of generic `"timed out"`
|
|
26
|
+
- **Idempotent schema migration** — existing databases get `ALTER TABLE jobs ADD COLUMN options TEXT` on first connection, wrapped in `rescue nil` for safe re-runs. New databases include the column in `CREATE TABLE`
|
|
27
|
+
|
|
3
28
|
## 0.6.1
|
|
4
29
|
|
|
5
30
|
### Bug Fixes
|
data/lib/async/background/job.rb
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
module Async
|
|
4
4
|
module Background
|
|
5
5
|
module Job
|
|
6
|
+
DEFAULT_TIMEOUT = 120
|
|
7
|
+
|
|
8
|
+
Options = Data.define(:timeout) do
|
|
9
|
+
def initialize(timeout: DEFAULT_TIMEOUT) = super(timeout: Integer(timeout))
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
def self.included(base)
|
|
7
13
|
base.extend(ClassMethods)
|
|
8
14
|
end
|
|
@@ -12,17 +18,23 @@ module Async
|
|
|
12
18
|
new.perform(*args)
|
|
13
19
|
end
|
|
14
20
|
|
|
15
|
-
def perform_async(*args)
|
|
16
|
-
Async::Background::Queue.enqueue(self, *args)
|
|
21
|
+
def perform_async(*args, options: {})
|
|
22
|
+
Async::Background::Queue.enqueue(self, *args, options: options)
|
|
17
23
|
end
|
|
18
24
|
|
|
19
|
-
def perform_in(delay, *args)
|
|
20
|
-
Async::Background::Queue.enqueue_in(delay, self, *args)
|
|
25
|
+
def perform_in(delay, *args, options: {})
|
|
26
|
+
Async::Background::Queue.enqueue_in(delay, self, *args, options: options)
|
|
21
27
|
end
|
|
22
28
|
|
|
23
|
-
def perform_at(time, *args)
|
|
24
|
-
Async::Background::Queue.enqueue_at(time, self, *args)
|
|
29
|
+
def perform_at(time, *args, options: {})
|
|
30
|
+
Async::Background::Queue.enqueue_at(time, self, *args, options: options)
|
|
25
31
|
end
|
|
32
|
+
|
|
33
|
+
def options(**values)
|
|
34
|
+
@options = Options.new(**values).to_h
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def resolve_options = @options || {}
|
|
26
38
|
end
|
|
27
39
|
|
|
28
40
|
def perform(*args)
|
|
@@ -5,6 +5,8 @@ require_relative '../clock'
|
|
|
5
5
|
module Async
|
|
6
6
|
module Background
|
|
7
7
|
module Queue
|
|
8
|
+
EMPTY_OPTIONS = {}.freeze
|
|
9
|
+
|
|
8
10
|
class Client
|
|
9
11
|
include Clock
|
|
10
12
|
|
|
@@ -13,43 +15,53 @@ module Async
|
|
|
13
15
|
@notifier = notifier
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
def push(class_name, args = [], run_at = nil)
|
|
17
|
-
id = @store.enqueue(class_name, args, run_at)
|
|
18
|
+
def push(class_name, args = [], run_at = nil, options: {})
|
|
19
|
+
id = @store.enqueue(class_name, args, run_at, options: options)
|
|
18
20
|
@notifier&.notify_all
|
|
19
21
|
id
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
def push_in(delay, class_name, args = [])
|
|
24
|
+
def push_in(delay, class_name, args = [], options: {})
|
|
23
25
|
run_at = realtime_now + delay.to_f
|
|
24
|
-
push(class_name, args, run_at)
|
|
26
|
+
push(class_name, args, run_at, options: options)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
def push_at(time, class_name, args = [])
|
|
29
|
+
def push_at(time, class_name, args = [], options: {})
|
|
28
30
|
run_at = time.respond_to?(:to_f) ? time.to_f : time
|
|
29
|
-
push(class_name, args, run_at)
|
|
31
|
+
push(class_name, args, run_at, options: options)
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
class << self
|
|
34
36
|
attr_accessor :default_client
|
|
35
37
|
|
|
36
|
-
def enqueue(job_class, *args)
|
|
38
|
+
def enqueue(job_class, *args, options: {})
|
|
37
39
|
ensure_configured!
|
|
38
|
-
|
|
40
|
+
merged = build_options(job_class, options)
|
|
41
|
+
default_client.push(resolve_class_name(job_class), args, nil, options: merged)
|
|
39
42
|
end
|
|
40
43
|
|
|
41
|
-
def enqueue_in(delay, job_class, *args)
|
|
44
|
+
def enqueue_in(delay, job_class, *args, options: {})
|
|
42
45
|
ensure_configured!
|
|
43
|
-
|
|
46
|
+
merged = build_options(job_class, options)
|
|
47
|
+
default_client.push_in(delay, resolve_class_name(job_class), args, options: merged)
|
|
44
48
|
end
|
|
45
49
|
|
|
46
|
-
def enqueue_at(time, job_class, *args)
|
|
50
|
+
def enqueue_at(time, job_class, *args, options: {})
|
|
47
51
|
ensure_configured!
|
|
48
|
-
|
|
52
|
+
merged = build_options(job_class, options)
|
|
53
|
+
default_client.push_at(time, resolve_class_name(job_class), args, options: merged)
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
private
|
|
52
57
|
|
|
58
|
+
def build_options(job_class, call_site_options)
|
|
59
|
+
merged = resolve_options(job_class).merge!(call_site_options.compact)
|
|
60
|
+
return EMPTY_OPTIONS if merged.empty?
|
|
61
|
+
|
|
62
|
+
Job::Options.new(**merged).to_h
|
|
63
|
+
end
|
|
64
|
+
|
|
53
65
|
def ensure_configured!
|
|
54
66
|
raise "Async::Background::Queue not configured" unless default_client
|
|
55
67
|
end
|
|
@@ -60,6 +72,12 @@ module Async
|
|
|
60
72
|
|
|
61
73
|
raise ArgumentError, "#{job_class} must include Async::Background::Job"
|
|
62
74
|
end
|
|
75
|
+
|
|
76
|
+
def resolve_options(job_class)
|
|
77
|
+
return {} unless job_class.respond_to?(:resolve_options)
|
|
78
|
+
|
|
79
|
+
job_class.resolve_options.dup
|
|
80
|
+
end
|
|
63
81
|
end
|
|
64
82
|
end
|
|
65
83
|
end
|
|
@@ -15,6 +15,7 @@ module Async
|
|
|
15
15
|
id INTEGER PRIMARY KEY,
|
|
16
16
|
class_name TEXT NOT NULL,
|
|
17
17
|
args TEXT NOT NULL DEFAULT '[]',
|
|
18
|
+
options TEXT,
|
|
18
19
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
19
20
|
created_at REAL NOT NULL,
|
|
20
21
|
run_at REAL NOT NULL,
|
|
@@ -45,6 +46,7 @@ module Async
|
|
|
45
46
|
def initialize(path: self.class.default_path, mmap: true)
|
|
46
47
|
@path = path
|
|
47
48
|
@mmap = mmap
|
|
49
|
+
@pragma_sql = PRAGMAS.call(mmap ? MMAP_SIZE : 0).freeze
|
|
48
50
|
@db = nil
|
|
49
51
|
@schema_checked = false
|
|
50
52
|
@last_cleanup_at = nil
|
|
@@ -54,17 +56,18 @@ module Async
|
|
|
54
56
|
require_sqlite3
|
|
55
57
|
db = SQLite3::Database.new(@path)
|
|
56
58
|
db.execute('PRAGMA busy_timeout = 5000')
|
|
57
|
-
db.execute_batch(
|
|
59
|
+
db.execute_batch(@pragma_sql)
|
|
58
60
|
db.execute_batch(SCHEMA)
|
|
59
61
|
db.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
60
62
|
db.close
|
|
61
63
|
@schema_checked = true
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
def enqueue(class_name, args = [], run_at = nil)
|
|
66
|
+
def enqueue(class_name, args = [], run_at = nil, options: {})
|
|
65
67
|
ensure_connection
|
|
66
68
|
run_at ||= realtime_now
|
|
67
|
-
|
|
69
|
+
options_json = options.empty? ? nil : JSON.generate(options)
|
|
70
|
+
@enqueue_stmt.execute(class_name, JSON.generate(args), options_json, realtime_now, run_at)
|
|
68
71
|
@db.last_insert_row_id
|
|
69
72
|
end
|
|
70
73
|
|
|
@@ -84,7 +87,8 @@ module Async
|
|
|
84
87
|
return unless row
|
|
85
88
|
|
|
86
89
|
maybe_cleanup
|
|
87
|
-
|
|
90
|
+
options = row[3] ? JSON.parse(row[3], symbolize_names: true) : {}
|
|
91
|
+
{ id: row[0], class_name: row[1], args: JSON.parse(row[2]), options: options }
|
|
88
92
|
rescue
|
|
89
93
|
@db.execute("ROLLBACK") rescue nil
|
|
90
94
|
raise
|
|
@@ -135,10 +139,11 @@ module Async
|
|
|
135
139
|
finalize_statements
|
|
136
140
|
@db = SQLite3::Database.new(@path)
|
|
137
141
|
@db.execute('PRAGMA busy_timeout = 5000')
|
|
138
|
-
@db.execute_batch(
|
|
142
|
+
@db.execute_batch(@pragma_sql)
|
|
139
143
|
|
|
140
144
|
unless @schema_checked
|
|
141
145
|
@db.execute_batch(SCHEMA)
|
|
146
|
+
@db.execute("ALTER TABLE jobs ADD COLUMN options TEXT") rescue nil
|
|
142
147
|
@schema_checked = true
|
|
143
148
|
end
|
|
144
149
|
|
|
@@ -148,7 +153,7 @@ module Async
|
|
|
148
153
|
|
|
149
154
|
def prepare_statements
|
|
150
155
|
@enqueue_stmt = @db.prepare(
|
|
151
|
-
"INSERT INTO jobs (class_name, args, created_at, run_at) VALUES (?, ?, ?, ?)"
|
|
156
|
+
"INSERT INTO jobs (class_name, args, options, created_at, run_at) VALUES (?, ?, ?, ?, ?)"
|
|
152
157
|
)
|
|
153
158
|
|
|
154
159
|
@fetch_stmt = @db.prepare(<<~SQL)
|
|
@@ -160,7 +165,7 @@ module Async
|
|
|
160
165
|
ORDER BY run_at, id
|
|
161
166
|
LIMIT 1
|
|
162
167
|
)
|
|
163
|
-
RETURNING id, class_name, args
|
|
168
|
+
RETURNING id, class_name, args, options
|
|
164
169
|
SQL
|
|
165
170
|
|
|
166
171
|
@complete_stmt = @db.prepare("UPDATE jobs SET status = 'done' WHERE id = ?")
|
|
@@ -175,11 +180,11 @@ module Async
|
|
|
175
180
|
end
|
|
176
181
|
|
|
177
182
|
def finalize_statements
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
stmt&.close rescue nil
|
|
181
|
-
instance_variable_set(:"@#{name}", nil)
|
|
183
|
+
[@enqueue_stmt, @fetch_stmt, @complete_stmt, @fail_stmt, @requeue_stmt, @cleanup_stmt].each do |s|
|
|
184
|
+
s&.close rescue next
|
|
182
185
|
end
|
|
186
|
+
|
|
187
|
+
@enqueue_stmt = @fetch_stmt = @complete_stmt = @fail_stmt = @requeue_stmt = @cleanup_stmt = nil
|
|
183
188
|
end
|
|
184
189
|
|
|
185
190
|
def maybe_cleanup
|
|
@@ -193,7 +198,6 @@ module Async
|
|
|
193
198
|
@db.execute("PRAGMA incremental_vacuum")
|
|
194
199
|
end
|
|
195
200
|
end
|
|
196
|
-
|
|
197
201
|
end
|
|
198
202
|
end
|
|
199
203
|
end
|
|
@@ -114,11 +114,13 @@ module Async
|
|
|
114
114
|
class_name = job[:class_name]
|
|
115
115
|
args = job[:args]
|
|
116
116
|
klass = resolve_job_class(class_name)
|
|
117
|
+
opts = Job::Options.new(**job[:options])
|
|
118
|
+
timeout = opts.timeout
|
|
117
119
|
|
|
118
120
|
metrics.job_started(nil)
|
|
119
121
|
t = monotonic_now
|
|
120
122
|
|
|
121
|
-
job_task.with_timeout(
|
|
123
|
+
job_task.with_timeout(timeout) { klass.perform_now(*args) }
|
|
122
124
|
|
|
123
125
|
duration = monotonic_now - t
|
|
124
126
|
metrics.job_finished(nil, duration)
|
|
@@ -130,7 +132,7 @@ module Async
|
|
|
130
132
|
rescue ::Async::TimeoutError
|
|
131
133
|
metrics.job_timed_out(nil)
|
|
132
134
|
@queue_store.fail(job[:id])
|
|
133
|
-
logger.error('Async::Background') { "queue(#{class_name}): timed out" }
|
|
135
|
+
logger.error('Async::Background') { "queue(#{class_name}): timed out after #{timeout}s" }
|
|
134
136
|
rescue => e
|
|
135
137
|
metrics.job_failed(nil, e)
|
|
136
138
|
@queue_store.fail(job[:id])
|
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.6.
|
|
4
|
+
version: 0.6.2
|
|
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-04-
|
|
11
|
+
date: 2026-04-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|