async-background 0.7.1 → 1.0.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/CHANGELOG.md +113 -1
- data/README.md +56 -9
- data/async-background.gemspec +5 -2
- data/lib/async/background/job.rb +5 -3
- data/lib/async/background/metrics.rb +160 -87
- data/lib/async/background/queue/client.rb +33 -15
- data/lib/async/background/queue/options.rb +70 -0
- data/lib/async/background/queue/schema.rb +165 -0
- data/lib/async/background/queue/sql.rb +216 -0
- data/lib/async/background/queue/store.rb +270 -148
- data/lib/async/background/runner/queue_execution.rb +199 -0
- data/lib/async/background/runner/schedule.rb +129 -0
- data/lib/async/background/runner.rb +112 -229
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +138 -0
- data/lib/async/background/web/assets.rb +726 -0
- data/lib/async/background/web/auth.rb +19 -0
- data/lib/async/background/web/configuration.rb +158 -0
- data/lib/async/background/web/cursor.rb +58 -0
- data/lib/async/background/web/errors.rb +14 -0
- data/lib/async/background/web/event_hub.rb +194 -0
- data/lib/async/background/web/metrics_reader.rb +96 -0
- data/lib/async/background/web/request.rb +36 -0
- data/lib/async/background/web/response.rb +85 -0
- data/lib/async/background/web/router.rb +30 -0
- data/lib/async/background/web/serializer.rb +154 -0
- data/lib/async/background/web/snapshot.rb +247 -0
- data/lib/async/background/web/sql.rb +88 -0
- data/lib/async/background/web/stream.rb +43 -0
- data/lib/async/background/web.rb +52 -0
- metadata +71 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'cursor'
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Background
|
|
8
|
+
module Web
|
|
9
|
+
class Serializer
|
|
10
|
+
EMPTY_OPTIONS = {}.freeze
|
|
11
|
+
EMPTY_ARGS = [].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def overview(snapshot_data, metrics_data = nil)
|
|
18
|
+
payload = {
|
|
19
|
+
counts: snapshot_data.fetch(:counts),
|
|
20
|
+
next_pending_run_at: snapshot_data[:next_pending_run_at],
|
|
21
|
+
data_version: snapshot_data.fetch(:data_version),
|
|
22
|
+
generated_at: snapshot_data.fetch(:generated_at)
|
|
23
|
+
}
|
|
24
|
+
payload[:metrics] = metrics_data if metrics_data
|
|
25
|
+
payload
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def executing(rows)
|
|
29
|
+
rows.map { |row| executing_item(row) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def claimed(rows)
|
|
33
|
+
rows.map { |row| claimed_item(row) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def done(rows)
|
|
37
|
+
page(rows.map { |row| done_item(row) }) { |item| Cursor.encode_finished(item[:finished_at], item[:id]) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def failed(rows)
|
|
41
|
+
page(rows.map { |row| failed_item(row) }) { |item| Cursor.encode_finished(item[:finished_at], item[:id]) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pending(rows)
|
|
45
|
+
page(rows.map { |row| pending_item(row) }) { |item| Cursor.encode_pending(item[:run_at], item[:id]) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def page(items)
|
|
51
|
+
{items: items, next_cursor: items.empty? ? nil : yield(items.last)}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def executing_item(row)
|
|
55
|
+
args, args_count = args_for(row[:args_raw])
|
|
56
|
+
{
|
|
57
|
+
id: row[:id],
|
|
58
|
+
class_name: row[:class_name],
|
|
59
|
+
args: args,
|
|
60
|
+
args_count: args_count,
|
|
61
|
+
options: parse_options(row[:options_raw]),
|
|
62
|
+
started_at: row[:started_at],
|
|
63
|
+
locked_by: row[:locked_by],
|
|
64
|
+
locked_at: row[:locked_at]
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def claimed_item(row)
|
|
69
|
+
args, args_count = args_for(row[:args_raw])
|
|
70
|
+
{
|
|
71
|
+
id: row[:id],
|
|
72
|
+
class_name: row[:class_name],
|
|
73
|
+
args: args,
|
|
74
|
+
args_count: args_count,
|
|
75
|
+
options: parse_options(row[:options_raw]),
|
|
76
|
+
locked_at: row[:locked_at],
|
|
77
|
+
locked_by: row[:locked_by]
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def done_item(row)
|
|
82
|
+
args, args_count = args_for(row[:args_raw])
|
|
83
|
+
{
|
|
84
|
+
id: row[:id],
|
|
85
|
+
class_name: row[:class_name],
|
|
86
|
+
args: args,
|
|
87
|
+
args_count: args_count,
|
|
88
|
+
options: parse_options(row[:options_raw]),
|
|
89
|
+
finished_at: row[:finished_at],
|
|
90
|
+
duration_ms: row[:duration_ms]
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def failed_item(row)
|
|
95
|
+
args, args_count = args_for(row[:args_raw])
|
|
96
|
+
{
|
|
97
|
+
id: row[:id],
|
|
98
|
+
class_name: row[:class_name],
|
|
99
|
+
args: args,
|
|
100
|
+
args_count: args_count,
|
|
101
|
+
options: parse_options(row[:options_raw]),
|
|
102
|
+
finished_at: row[:finished_at],
|
|
103
|
+
duration_ms: row[:duration_ms],
|
|
104
|
+
last_error_class: row[:last_error_class],
|
|
105
|
+
last_error_message: row[:last_error_message]
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def pending_item(row)
|
|
110
|
+
args, args_count = args_for(row[:args_raw])
|
|
111
|
+
{
|
|
112
|
+
id: row[:id],
|
|
113
|
+
class_name: row[:class_name],
|
|
114
|
+
args: args,
|
|
115
|
+
args_count: args_count,
|
|
116
|
+
options: parse_options(row[:options_raw]),
|
|
117
|
+
created_at: row[:created_at],
|
|
118
|
+
run_at: row[:run_at]
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def args_for(raw)
|
|
123
|
+
if raw.nil? || raw.empty? || raw == '[]'
|
|
124
|
+
return [@config.expose_args ? redact(EMPTY_ARGS) : nil, 0]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
parsed = parse_json(raw)
|
|
128
|
+
count = parsed.is_a?(Array) ? parsed.length : 0
|
|
129
|
+
return [nil, count] unless @config.expose_args
|
|
130
|
+
|
|
131
|
+
[redact(parsed), count]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def redact(args)
|
|
135
|
+
redactor = @config.redact_args
|
|
136
|
+
redactor ? redactor.call(args) : args
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def parse_options(raw)
|
|
140
|
+
return EMPTY_OPTIONS if raw.nil? || raw.empty? || raw == '{}'
|
|
141
|
+
|
|
142
|
+
parsed = parse_json(raw)
|
|
143
|
+
parsed.is_a?(Hash) ? parsed : EMPTY_OPTIONS
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def parse_json(raw)
|
|
147
|
+
JSON.parse(raw)
|
|
148
|
+
rescue JSON::ParserError
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
require_relative '../clock'
|
|
6
|
+
require_relative '../queue/sql'
|
|
7
|
+
require_relative 'sql'
|
|
8
|
+
|
|
9
|
+
module Async
|
|
10
|
+
module Background
|
|
11
|
+
module Web
|
|
12
|
+
class Snapshot
|
|
13
|
+
include Clock
|
|
14
|
+
|
|
15
|
+
CacheEntry = Data.define(:value, :created_at)
|
|
16
|
+
|
|
17
|
+
def initialize(path:, counts_cache_ttl:)
|
|
18
|
+
@path = path
|
|
19
|
+
@overview_cache_ttl = counts_cache_ttl
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
@db = nil
|
|
22
|
+
@overview_cache = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def open!
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
return self if connected?
|
|
28
|
+
|
|
29
|
+
db = open_database
|
|
30
|
+
configure_database(db)
|
|
31
|
+
@db = db
|
|
32
|
+
rescue StandardError
|
|
33
|
+
db&.close unless db&.closed?
|
|
34
|
+
raise
|
|
35
|
+
end
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def close
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@db&.close unless @db&.closed?
|
|
42
|
+
@db = nil
|
|
43
|
+
@overview_cache = nil
|
|
44
|
+
end
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def closed?
|
|
49
|
+
@mutex.synchronize { !connected? }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def data_version
|
|
53
|
+
with_database { |db| db.get_first_value(Queue::SQL::DATA_VERSION).to_i }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def overview(force: false)
|
|
57
|
+
with_database do |db|
|
|
58
|
+
now = monotonic_now
|
|
59
|
+
return @overview_cache.value if !force && overview_cache_current?(now)
|
|
60
|
+
|
|
61
|
+
value = read_transaction(db) { overview_from(db) }.freeze
|
|
62
|
+
@overview_cache = CacheEntry.new(value, now)
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def executing(limit:)
|
|
68
|
+
read_rows(SQL::EXECUTING, [limit]).map { |row| executing_row(row) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def claimed(limit:)
|
|
72
|
+
read_rows(SQL::CLAIMED, [limit]).map { |row| claimed_row(row) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def recent_done(limit:, cursor: nil)
|
|
76
|
+
sql, binds = terminal_query(SQL::DONE, SQL::DONE_AFTER, limit, cursor)
|
|
77
|
+
read_rows(sql, binds).map { |row| done_row(row) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def recent_failed(limit:, cursor: nil)
|
|
81
|
+
sql, binds = terminal_query(SQL::FAILED, SQL::FAILED_AFTER, limit, cursor)
|
|
82
|
+
read_rows(sql, binds).map { |row| failed_row(row) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def pending(limit:, cursor: nil)
|
|
86
|
+
sql, binds = pending_query(limit, cursor)
|
|
87
|
+
read_rows(sql, binds).map { |row| pending_row(row) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def connected?
|
|
93
|
+
@db && !@db.closed?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def open_database
|
|
97
|
+
require_sqlite3
|
|
98
|
+
SQLite3::Database.new(database_uri, uri: true)
|
|
99
|
+
rescue LoadError
|
|
100
|
+
raise
|
|
101
|
+
rescue StandardError => error
|
|
102
|
+
raise UnavailableError, "cannot open queue database: #{error.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def database_uri
|
|
106
|
+
path = URI::DEFAULT_PARSER.escape(File.expand_path(@path)).gsub('?', '%3F')
|
|
107
|
+
"file:#{path}?mode=ro"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def configure_database(db)
|
|
111
|
+
db.execute(SQL::BUSY_TIMEOUT)
|
|
112
|
+
db.execute(SQL::QUERY_ONLY)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def with_database
|
|
116
|
+
@mutex.synchronize do
|
|
117
|
+
raise ClosedError, 'snapshot is closed' unless connected?
|
|
118
|
+
|
|
119
|
+
yield @db
|
|
120
|
+
end
|
|
121
|
+
rescue ClosedError, UnavailableError
|
|
122
|
+
raise
|
|
123
|
+
rescue StandardError
|
|
124
|
+
raise UnavailableError, 'queue database is unavailable'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def read_rows(sql, binds)
|
|
128
|
+
with_database { |db| read_transaction(db) { db.execute(sql, binds) } }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def read_transaction(db)
|
|
132
|
+
db.execute(SQL::BEGIN_READ_TRANSACTION)
|
|
133
|
+
result = yield
|
|
134
|
+
db.execute(SQL::COMMIT)
|
|
135
|
+
result
|
|
136
|
+
rescue StandardError
|
|
137
|
+
rollback(db)
|
|
138
|
+
raise
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def rollback(db)
|
|
142
|
+
db.execute(SQL::ROLLBACK)
|
|
143
|
+
rescue StandardError
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def overview_cache_current?(now)
|
|
148
|
+
cache = @overview_cache
|
|
149
|
+
cache && (now - cache.created_at) < @overview_cache_ttl
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def overview_from(db)
|
|
153
|
+
{
|
|
154
|
+
counts: {
|
|
155
|
+
executing: db.get_first_value(SQL::OVERVIEW_EXECUTING).to_i,
|
|
156
|
+
claimed: db.get_first_value(SQL::OVERVIEW_CLAIMED).to_i,
|
|
157
|
+
pending: db.get_first_value(SQL::OVERVIEW_PENDING).to_i,
|
|
158
|
+
done: db.get_first_value(SQL::OVERVIEW_DONE).to_i,
|
|
159
|
+
failed: db.get_first_value(SQL::OVERVIEW_FAILED).to_i
|
|
160
|
+
}.freeze,
|
|
161
|
+
next_pending_run_at: db.get_first_value(SQL::OVERVIEW_NEXT_PENDING),
|
|
162
|
+
data_version: db.get_first_value(Queue::SQL::DATA_VERSION).to_i,
|
|
163
|
+
generated_at: realtime_now
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def terminal_query(first_page_sql, next_page_sql, limit, cursor)
|
|
168
|
+
return [first_page_sql, [limit]] unless cursor
|
|
169
|
+
|
|
170
|
+
[next_page_sql, [cursor.fetch(:finished_at), cursor.fetch(:id), limit]]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def pending_query(limit, cursor)
|
|
174
|
+
return [SQL::PENDING, [limit]] unless cursor
|
|
175
|
+
|
|
176
|
+
[SQL::PENDING_AFTER, [cursor.fetch(:run_at), cursor.fetch(:id), limit]]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def executing_row(row)
|
|
180
|
+
{
|
|
181
|
+
id: row[0],
|
|
182
|
+
class_name: row[1],
|
|
183
|
+
args_raw: row[2],
|
|
184
|
+
options_raw: row[3],
|
|
185
|
+
started_at: row[4],
|
|
186
|
+
locked_by: row[5],
|
|
187
|
+
locked_at: row[6]
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def claimed_row(row)
|
|
192
|
+
{
|
|
193
|
+
id: row[0],
|
|
194
|
+
class_name: row[1],
|
|
195
|
+
args_raw: row[2],
|
|
196
|
+
options_raw: row[3],
|
|
197
|
+
locked_at: row[4],
|
|
198
|
+
locked_by: row[5]
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def done_row(row)
|
|
203
|
+
{
|
|
204
|
+
id: row[0],
|
|
205
|
+
class_name: row[1],
|
|
206
|
+
args_raw: row[2],
|
|
207
|
+
options_raw: row[3],
|
|
208
|
+
finished_at: row[4],
|
|
209
|
+
duration_ms: row[5]
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def failed_row(row)
|
|
214
|
+
{
|
|
215
|
+
id: row[0],
|
|
216
|
+
class_name: row[1],
|
|
217
|
+
args_raw: row[2],
|
|
218
|
+
options_raw: row[3],
|
|
219
|
+
finished_at: row[4],
|
|
220
|
+
duration_ms: row[5],
|
|
221
|
+
last_error_class: row[6],
|
|
222
|
+
last_error_message: row[7]
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def pending_row(row)
|
|
227
|
+
{
|
|
228
|
+
id: row[0],
|
|
229
|
+
class_name: row[1],
|
|
230
|
+
args_raw: row[2],
|
|
231
|
+
options_raw: row[3],
|
|
232
|
+
created_at: row[4],
|
|
233
|
+
run_at: row[5]
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def require_sqlite3
|
|
238
|
+
require 'sqlite3'
|
|
239
|
+
rescue LoadError
|
|
240
|
+
raise LoadError,
|
|
241
|
+
"sqlite3 gem is required for Async::Background::Web. " \
|
|
242
|
+
"Add `gem 'sqlite3', '~> 2.0'` to your Gemfile."
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Web
|
|
6
|
+
module SQL
|
|
7
|
+
BEGIN_READ_TRANSACTION = 'BEGIN'.freeze
|
|
8
|
+
COMMIT = 'COMMIT'.freeze
|
|
9
|
+
ROLLBACK = 'ROLLBACK'.freeze
|
|
10
|
+
QUERY_ONLY = 'PRAGMA query_only = ON'.freeze
|
|
11
|
+
BUSY_TIMEOUT = 'PRAGMA busy_timeout = 2000'.freeze
|
|
12
|
+
|
|
13
|
+
OVERVIEW_EXECUTING = "SELECT COUNT(*) FROM jobs WHERE status = 'running' AND started_at IS NOT NULL".freeze
|
|
14
|
+
OVERVIEW_CLAIMED = "SELECT COUNT(*) FROM jobs WHERE status = 'running' AND started_at IS NULL".freeze
|
|
15
|
+
OVERVIEW_PENDING = "SELECT COUNT(*) FROM jobs WHERE status = 'pending'".freeze
|
|
16
|
+
OVERVIEW_DONE = "SELECT COUNT(*) FROM jobs WHERE status = 'done'".freeze
|
|
17
|
+
OVERVIEW_FAILED = "SELECT COUNT(*) FROM jobs WHERE status = 'failed'".freeze
|
|
18
|
+
OVERVIEW_NEXT_PENDING = "SELECT MIN(run_at) FROM jobs WHERE status = 'pending'".freeze
|
|
19
|
+
|
|
20
|
+
EXECUTING = <<~SQL.freeze
|
|
21
|
+
SELECT id, class_name, args, options, started_at, locked_by, locked_at
|
|
22
|
+
FROM jobs
|
|
23
|
+
WHERE status = 'running' AND started_at IS NOT NULL
|
|
24
|
+
ORDER BY started_at, id
|
|
25
|
+
LIMIT ?
|
|
26
|
+
SQL
|
|
27
|
+
|
|
28
|
+
CLAIMED = <<~SQL.freeze
|
|
29
|
+
SELECT id, class_name, args, options, locked_at, locked_by
|
|
30
|
+
FROM jobs
|
|
31
|
+
WHERE status = 'running' AND started_at IS NULL
|
|
32
|
+
ORDER BY locked_at, id
|
|
33
|
+
LIMIT ?
|
|
34
|
+
SQL
|
|
35
|
+
|
|
36
|
+
DONE = <<~SQL.freeze
|
|
37
|
+
SELECT id, class_name, args, options, finished_at, duration_ms
|
|
38
|
+
FROM jobs
|
|
39
|
+
WHERE status = 'done'
|
|
40
|
+
ORDER BY finished_at DESC, id DESC
|
|
41
|
+
LIMIT ?
|
|
42
|
+
SQL
|
|
43
|
+
|
|
44
|
+
DONE_AFTER = <<~SQL.freeze
|
|
45
|
+
SELECT id, class_name, args, options, finished_at, duration_ms
|
|
46
|
+
FROM jobs
|
|
47
|
+
WHERE status = 'done' AND (finished_at, id) < (?, ?)
|
|
48
|
+
ORDER BY finished_at DESC, id DESC
|
|
49
|
+
LIMIT ?
|
|
50
|
+
SQL
|
|
51
|
+
|
|
52
|
+
FAILED = <<~SQL.freeze
|
|
53
|
+
SELECT id, class_name, args, options, finished_at, duration_ms,
|
|
54
|
+
last_error_class, last_error_message
|
|
55
|
+
FROM jobs
|
|
56
|
+
WHERE status = 'failed'
|
|
57
|
+
ORDER BY finished_at DESC, id DESC
|
|
58
|
+
LIMIT ?
|
|
59
|
+
SQL
|
|
60
|
+
|
|
61
|
+
FAILED_AFTER = <<~SQL.freeze
|
|
62
|
+
SELECT id, class_name, args, options, finished_at, duration_ms,
|
|
63
|
+
last_error_class, last_error_message
|
|
64
|
+
FROM jobs
|
|
65
|
+
WHERE status = 'failed' AND (finished_at, id) < (?, ?)
|
|
66
|
+
ORDER BY finished_at DESC, id DESC
|
|
67
|
+
LIMIT ?
|
|
68
|
+
SQL
|
|
69
|
+
|
|
70
|
+
PENDING = <<~SQL.freeze
|
|
71
|
+
SELECT id, class_name, args, options, created_at, run_at
|
|
72
|
+
FROM jobs
|
|
73
|
+
WHERE status = 'pending'
|
|
74
|
+
ORDER BY run_at, id
|
|
75
|
+
LIMIT ?
|
|
76
|
+
SQL
|
|
77
|
+
|
|
78
|
+
PENDING_AFTER = <<~SQL.freeze
|
|
79
|
+
SELECT id, class_name, args, options, created_at, run_at
|
|
80
|
+
FROM jobs
|
|
81
|
+
WHERE status = 'pending' AND (run_at, id) > (?, ?)
|
|
82
|
+
ORDER BY run_at, id
|
|
83
|
+
LIMIT ?
|
|
84
|
+
SQL
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Web
|
|
6
|
+
class Stream
|
|
7
|
+
def initialize(hub, heartbeat_seconds:, retry_ms:)
|
|
8
|
+
@hub = hub
|
|
9
|
+
@heartbeat_seconds = heartbeat_seconds
|
|
10
|
+
@retry_ms = retry_ms
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def each
|
|
14
|
+
subscription, initial_frame = @hub.subscribe
|
|
15
|
+
yield "retry: #{@retry_ms}\n\n"
|
|
16
|
+
yield initial_frame
|
|
17
|
+
|
|
18
|
+
loop do
|
|
19
|
+
frame = subscription.pop(timeout: @heartbeat_seconds)
|
|
20
|
+
break if frame.nil? && subscription.closed?
|
|
21
|
+
|
|
22
|
+
yield(frame || EventHub::HEARTBEAT_FRAME)
|
|
23
|
+
end
|
|
24
|
+
rescue Errno::EPIPE, IOError
|
|
25
|
+
nil
|
|
26
|
+
rescue ClosedError, UnavailableError
|
|
27
|
+
safe_yield(EventHub::UNAVAILABLE_FRAME) { |frame| yield frame }
|
|
28
|
+
nil
|
|
29
|
+
ensure
|
|
30
|
+
@hub.unsubscribe(subscription) if subscription
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def safe_yield(frame)
|
|
36
|
+
yield frame
|
|
37
|
+
rescue Errno::EPIPE, IOError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require 'rack'
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise LoadError,
|
|
7
|
+
"Async::Background::Web requires 'rack'. " \
|
|
8
|
+
"Add `gem 'rack', '~> 3.0'` to your Gemfile."
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require_relative 'web/errors'
|
|
12
|
+
require_relative 'web/configuration'
|
|
13
|
+
require_relative 'web/sql'
|
|
14
|
+
require_relative 'web/cursor'
|
|
15
|
+
require_relative 'web/request'
|
|
16
|
+
require_relative 'web/response'
|
|
17
|
+
require_relative 'web/snapshot'
|
|
18
|
+
require_relative 'web/metrics_reader'
|
|
19
|
+
require_relative 'web/serializer'
|
|
20
|
+
require_relative 'web/auth'
|
|
21
|
+
require_relative 'web/router'
|
|
22
|
+
require_relative 'web/event_hub'
|
|
23
|
+
require_relative 'web/stream'
|
|
24
|
+
require_relative 'web/assets'
|
|
25
|
+
require_relative 'web/app'
|
|
26
|
+
|
|
27
|
+
module Async
|
|
28
|
+
module Background
|
|
29
|
+
module Web
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def configure
|
|
33
|
+
@configuration ||= Configuration.new
|
|
34
|
+
yield @configuration if block_given?
|
|
35
|
+
@configuration
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def configuration
|
|
39
|
+
@configuration or raise NotConfiguredError,
|
|
40
|
+
'Async::Background::Web is not configured. Call Async::Background::Web.configure.'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset!
|
|
44
|
+
@configuration = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def app
|
|
48
|
+
App.new(configuration)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
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: 1.0.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-
|
|
11
|
+
date: 2026-06-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -52,6 +52,20 @@ dependencies:
|
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '1.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: base64
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.2'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.2'
|
|
55
69
|
- !ruby/object:Gem::Dependency
|
|
56
70
|
name: rake
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -80,6 +94,40 @@ dependencies:
|
|
|
80
94
|
- - "~>"
|
|
81
95
|
- !ruby/object:Gem::Version
|
|
82
96
|
version: '3.12'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rack
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: async-utilization
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0.3'
|
|
118
|
+
- - "<"
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0.5'
|
|
121
|
+
type: :development
|
|
122
|
+
prerelease: false
|
|
123
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: '0.3'
|
|
128
|
+
- - "<"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0.5'
|
|
83
131
|
description: A production-grade lightweight scheduler built on top of Async. Single
|
|
84
132
|
event loop with min-heap timer, skip-overlapping execution, jitter, monotonic clock
|
|
85
133
|
intervals, semaphore concurrency control, and deterministic worker sharding. Designed
|
|
@@ -102,11 +150,32 @@ files:
|
|
|
102
150
|
- lib/async/background/min_heap.rb
|
|
103
151
|
- lib/async/background/queue/client.rb
|
|
104
152
|
- lib/async/background/queue/notifier.rb
|
|
153
|
+
- lib/async/background/queue/options.rb
|
|
154
|
+
- lib/async/background/queue/schema.rb
|
|
105
155
|
- lib/async/background/queue/socket_notifier.rb
|
|
106
156
|
- lib/async/background/queue/socket_waker.rb
|
|
157
|
+
- lib/async/background/queue/sql.rb
|
|
107
158
|
- lib/async/background/queue/store.rb
|
|
108
159
|
- lib/async/background/runner.rb
|
|
160
|
+
- lib/async/background/runner/queue_execution.rb
|
|
161
|
+
- lib/async/background/runner/schedule.rb
|
|
109
162
|
- lib/async/background/version.rb
|
|
163
|
+
- lib/async/background/web.rb
|
|
164
|
+
- lib/async/background/web/app.rb
|
|
165
|
+
- lib/async/background/web/assets.rb
|
|
166
|
+
- lib/async/background/web/auth.rb
|
|
167
|
+
- lib/async/background/web/configuration.rb
|
|
168
|
+
- lib/async/background/web/cursor.rb
|
|
169
|
+
- lib/async/background/web/errors.rb
|
|
170
|
+
- lib/async/background/web/event_hub.rb
|
|
171
|
+
- lib/async/background/web/metrics_reader.rb
|
|
172
|
+
- lib/async/background/web/request.rb
|
|
173
|
+
- lib/async/background/web/response.rb
|
|
174
|
+
- lib/async/background/web/router.rb
|
|
175
|
+
- lib/async/background/web/serializer.rb
|
|
176
|
+
- lib/async/background/web/snapshot.rb
|
|
177
|
+
- lib/async/background/web/sql.rb
|
|
178
|
+
- lib/async/background/web/stream.rb
|
|
110
179
|
homepage: https://github.com/roman-haidarov/async-background
|
|
111
180
|
licenses:
|
|
112
181
|
- MIT
|