async-background 1.0.2 → 1.1.1
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 +81 -0
- data/README.md +36 -15
- data/async-background.gemspec +13 -7
- data/lib/async/background/job.rb +75 -28
- data/lib/async/background/metrics.rb +41 -33
- data/lib/async/background/min_heap.rb +16 -12
- data/lib/async/background/queue/client.rb +11 -8
- data/lib/async/background/queue/options.rb +3 -3
- data/lib/async/background/queue/schema.rb +19 -15
- data/lib/async/background/queue/socket_notifier.rb +14 -4
- data/lib/async/background/queue/socket_waker.rb +139 -56
- data/lib/async/background/queue/sql.rb +31 -31
- data/lib/async/background/queue/store.rb +44 -45
- data/lib/async/background/runner/lifecycle.rb +131 -0
- data/lib/async/background/runner/queue_execution.rb +101 -57
- data/lib/async/background/runner/schedule.rb +20 -17
- data/lib/async/background/runner.rb +98 -77
- data/lib/async/background/runtime/notification.rb +42 -0
- data/lib/async/background/runtime/semaphore.rb +151 -0
- data/lib/async/background/runtime/task.rb +146 -0
- data/lib/async/background/runtime/task_group.rb +81 -0
- data/lib/async/background/runtime.rb +246 -0
- data/lib/async/background/scheduler.rb +145 -0
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +43 -40
- data/lib/async/background/web/assets/app.css +174 -0
- data/lib/async/background/web/assets/app.js +486 -0
- data/lib/async/background/web/assets/index.html +29 -0
- data/lib/async/background/web/assets.rb +4 -697
- data/lib/async/background/web/auth.rb +3 -3
- data/lib/async/background/web/configuration.rb +40 -93
- data/lib/async/background/web/cursor.rb +22 -26
- data/lib/async/background/web/event_hub.rb +12 -7
- data/lib/async/background/web/metrics_reader.rb +28 -36
- data/lib/async/background/web/response.rb +39 -51
- data/lib/async/background/web/serializer.rb +23 -63
- data/lib/async/background/web/snapshot.rb +35 -81
- data/lib/async/background/web/sql.rb +63 -69
- data/lib/async/background/web/stream.rb +52 -32
- data/lib/async/background/web.rb +1 -1
- data/lib/async/background.rb +1 -2
- metadata +30 -22
|
@@ -29,11 +29,9 @@ module Async
|
|
|
29
29
|
|
|
30
30
|
enable_incremental_vacuum!(db) unless jobs_table?(db)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
upgrade!(db) unless current?(db)
|
|
36
|
-
end
|
|
32
|
+
synchronized_change(db) do
|
|
33
|
+
reject_future_version!(db)
|
|
34
|
+
upgrade!(db) unless current?(db)
|
|
37
35
|
end
|
|
38
36
|
end
|
|
39
37
|
|
|
@@ -41,20 +39,20 @@ module Async
|
|
|
41
39
|
migrate!(db)
|
|
42
40
|
return if dashboard_indexes_current?(db)
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
create_dashboard_indexes!(db) unless dashboard_indexes_current?(db)
|
|
47
|
-
end
|
|
42
|
+
synchronized_change(db) do
|
|
43
|
+
create_dashboard_indexes!(db) unless dashboard_indexes_current?(db)
|
|
48
44
|
end
|
|
49
45
|
end
|
|
50
46
|
|
|
47
|
+
def synchronized_change(db, &change)
|
|
48
|
+
with_migration_timeout(db) { immediate_transaction(db, &change) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
51
|
def current?(db)
|
|
52
52
|
jobs_table?(db) && version(db) == VERSION && core_indexes_current?(db)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def dashboard_indexes_current?(db)
|
|
56
|
-
(DASHBOARD_INDEXES - index_names(db)).empty?
|
|
57
|
-
end
|
|
55
|
+
def dashboard_indexes_current?(db) = indexes_present?(db, DASHBOARD_INDEXES)
|
|
58
56
|
|
|
59
57
|
def version(db)
|
|
60
58
|
db.get_first_value(SQL::USER_VERSION).to_i
|
|
@@ -101,8 +99,10 @@ module Async
|
|
|
101
99
|
SQL::CREATE_DASHBOARD_INDEXES.each { |statement| db.execute(statement) }
|
|
102
100
|
end
|
|
103
101
|
|
|
104
|
-
def core_indexes_current?(db)
|
|
105
|
-
|
|
102
|
+
def core_indexes_current?(db) = indexes_present?(db, CORE_INDEXES)
|
|
103
|
+
|
|
104
|
+
def indexes_present?(db, names)
|
|
105
|
+
(names - index_names(db)).empty?
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def jobs_table?(db)
|
|
@@ -156,7 +156,11 @@ module Async
|
|
|
156
156
|
db.execute(SQL::COMMIT)
|
|
157
157
|
result
|
|
158
158
|
rescue StandardError
|
|
159
|
-
|
|
159
|
+
begin
|
|
160
|
+
db.execute(SQL::ROLLBACK)
|
|
161
|
+
rescue StandardError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
160
164
|
raise
|
|
161
165
|
end
|
|
162
166
|
end
|
|
@@ -19,7 +19,7 @@ module Async
|
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
21
|
DEAD_WORKER_TTL = 5.0
|
|
22
|
-
WAKE_BYTE = "\x01"
|
|
22
|
+
WAKE_BYTE = "\x01"
|
|
23
23
|
|
|
24
24
|
def initialize(socket_dir:, total_workers:)
|
|
25
25
|
@socket_dir = socket_dir
|
|
@@ -64,7 +64,11 @@ module Async
|
|
|
64
64
|
begin
|
|
65
65
|
socket.write_nonblock(WAKE_BYTE)
|
|
66
66
|
ensure
|
|
67
|
-
|
|
67
|
+
begin
|
|
68
|
+
socket.close
|
|
69
|
+
rescue StandardError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
68
72
|
end
|
|
69
73
|
true
|
|
70
74
|
rescue IO::WaitWritable
|
|
@@ -72,9 +76,15 @@ module Async
|
|
|
72
76
|
rescue *UNAVAILABLE
|
|
73
77
|
mark_dead(index, now)
|
|
74
78
|
false
|
|
75
|
-
rescue => e
|
|
79
|
+
rescue StandardError => e
|
|
76
80
|
mark_dead(index, now)
|
|
77
|
-
|
|
81
|
+
begin
|
|
82
|
+
Console.logger.warn(self) do
|
|
83
|
+
"SocketNotifier#notify_one(#{index + 1}) failed: #{e.class} #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
78
88
|
false
|
|
79
89
|
end
|
|
80
90
|
|
|
@@ -1,113 +1,196 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'socket'
|
|
4
|
-
require 'async/notification'
|
|
5
4
|
require 'fileutils'
|
|
5
|
+
require_relative '../runtime'
|
|
6
6
|
|
|
7
7
|
module Async
|
|
8
8
|
module Background
|
|
9
9
|
module Queue
|
|
10
10
|
class SocketWaker
|
|
11
|
+
CLOSE_GRACE = 2
|
|
12
|
+
|
|
11
13
|
attr_reader :path
|
|
12
14
|
|
|
13
15
|
def initialize(path)
|
|
14
16
|
@path = path
|
|
15
17
|
@server = nil
|
|
16
|
-
@notification = ::
|
|
18
|
+
@notification = Runtime::Notification.new
|
|
17
19
|
@running = false
|
|
18
20
|
@accept_task = nil
|
|
21
|
+
@clients = Runtime::TaskGroup.new
|
|
22
|
+
@sockets = {}
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def open!
|
|
22
|
-
cleanup_stale_socket
|
|
23
26
|
ensure_directory
|
|
27
|
+
cleanup_stale_socket
|
|
24
28
|
@server = UNIXServer.new(@path)
|
|
25
|
-
File.chmod(0600, @path)
|
|
26
29
|
@running = true
|
|
30
|
+
self
|
|
27
31
|
rescue Errno::EADDRINUSE
|
|
28
32
|
raise "Socket #{@path} is already in use by another process"
|
|
29
33
|
end
|
|
30
34
|
|
|
31
|
-
def start_accept_loop(
|
|
32
|
-
@accept_task =
|
|
33
|
-
while @running
|
|
34
|
-
begin
|
|
35
|
-
client = @server.accept_nonblock
|
|
36
|
-
handle_client(task, client)
|
|
37
|
-
rescue IO::WaitReadable
|
|
38
|
-
@server.wait_readable
|
|
39
|
-
rescue Errno::EBADF, IOError
|
|
40
|
-
break
|
|
41
|
-
rescue => e
|
|
42
|
-
Console.logger.error(self) { "SocketWaker accept error: #{e.class} #{e.message}" }
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
rescue => e
|
|
46
|
-
Console.logger.error(self) { "SocketWaker loop crashed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}" }
|
|
47
|
-
ensure
|
|
48
|
-
@accept_task = nil
|
|
49
|
-
end
|
|
35
|
+
def start_accept_loop(_parent_task = nil)
|
|
36
|
+
@accept_task = Runtime.spawn(name: 'socket-waker-accept') { accept_loop }
|
|
50
37
|
end
|
|
51
38
|
|
|
52
39
|
def wait(timeout: nil)
|
|
53
|
-
|
|
54
|
-
::Async::Task.current.with_timeout(timeout) { @notification.wait }
|
|
55
|
-
else
|
|
56
|
-
@notification.wait
|
|
57
|
-
end
|
|
58
|
-
rescue ::Async::TimeoutError
|
|
59
|
-
# Timeout is normal - listener will fall back to polling
|
|
40
|
+
@notification.wait(timeout)
|
|
60
41
|
end
|
|
61
42
|
|
|
62
43
|
def signal
|
|
63
|
-
@notification.
|
|
44
|
+
@notification.signal_all
|
|
45
|
+
true
|
|
64
46
|
end
|
|
65
47
|
|
|
66
48
|
def close
|
|
49
|
+
return unless @running || @server
|
|
50
|
+
|
|
67
51
|
@running = false
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
stop_accept_loop
|
|
53
|
+
@notification.signal_all
|
|
54
|
+
hang_up_clients
|
|
55
|
+
stop_clients
|
|
71
56
|
|
|
72
|
-
|
|
57
|
+
begin
|
|
58
|
+
@server&.close
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
73
62
|
@server = nil
|
|
74
|
-
|
|
63
|
+
@accept_task = nil
|
|
64
|
+
begin
|
|
65
|
+
File.unlink(@path)
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
75
69
|
end
|
|
76
70
|
|
|
77
71
|
private
|
|
78
72
|
|
|
79
|
-
def
|
|
80
|
-
|
|
73
|
+
def accept_loop
|
|
74
|
+
while @running
|
|
75
|
+
begin
|
|
76
|
+
client = @server.accept_nonblock
|
|
77
|
+
rescue IO::WaitReadable
|
|
78
|
+
@server.wait_readable
|
|
79
|
+
next
|
|
80
|
+
rescue Errno::EBADF, IOError
|
|
81
|
+
break
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
Console.logger.error(self) { "SocketWaker accept error: #{e.class} #{e.message}" }
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
break unless @running
|
|
88
|
+
|
|
89
|
+
handle_client(client)
|
|
90
|
+
end
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
Console.logger.error(self) { "SocketWaker loop crashed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}" }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def handle_client(client)
|
|
96
|
+
@sockets[client] = true
|
|
97
|
+
|
|
98
|
+
@clients.spawn(name: 'socket-waker-client') do
|
|
99
|
+
loop do
|
|
100
|
+
client.read_nonblock(256)
|
|
101
|
+
@notification.signal_all
|
|
102
|
+
rescue IO::WaitReadable
|
|
103
|
+
client.wait_readable
|
|
104
|
+
retry
|
|
105
|
+
rescue EOFError, Errno::ECONNRESET, Errno::EBADF, IOError
|
|
106
|
+
break
|
|
107
|
+
end
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
Console.logger.warn(self) { "SocketWaker client handler error: #{e.class} #{e.message}" }
|
|
110
|
+
ensure
|
|
111
|
+
@sockets.delete(client)
|
|
81
112
|
begin
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@notification.signal
|
|
86
|
-
rescue IO::WaitReadable
|
|
87
|
-
client.wait_readable
|
|
88
|
-
retry
|
|
89
|
-
rescue EOFError, Errno::ECONNRESET
|
|
90
|
-
break
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
rescue => e
|
|
94
|
-
Console.logger.warn(self) { "SocketWaker client handler error: #{e.class} #{e.message}" }
|
|
95
|
-
ensure
|
|
96
|
-
client.close rescue nil
|
|
97
|
-
@notification.signal
|
|
113
|
+
client.close
|
|
114
|
+
rescue StandardError
|
|
115
|
+
nil
|
|
98
116
|
end
|
|
117
|
+
@notification.signal_all
|
|
99
118
|
end
|
|
100
119
|
end
|
|
101
120
|
|
|
121
|
+
def stop_accept_loop
|
|
122
|
+
task = @accept_task or return
|
|
123
|
+
|
|
124
|
+
wake_accept_loop
|
|
125
|
+
return if await(task, CLOSE_GRACE)
|
|
126
|
+
|
|
127
|
+
task.stop
|
|
128
|
+
await(task, CLOSE_GRACE)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def wake_accept_loop
|
|
132
|
+
return unless @server
|
|
133
|
+
|
|
134
|
+
UNIXSocket.open(@path) do |s|
|
|
135
|
+
s.write_nonblock("\x00")
|
|
136
|
+
rescue StandardError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
rescue StandardError
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def hang_up_clients
|
|
144
|
+
@sockets.each_key do |socket|
|
|
145
|
+
socket.close
|
|
146
|
+
rescue StandardError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def stop_clients
|
|
152
|
+
return if @clients.empty?
|
|
153
|
+
|
|
154
|
+
return if await_group(@clients, CLOSE_GRACE)
|
|
155
|
+
|
|
156
|
+
@clients.stop_all(CLOSE_GRACE)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def await(task, grace)
|
|
160
|
+
return true unless Runtime.scheduler
|
|
161
|
+
|
|
162
|
+
task.wait(grace)
|
|
163
|
+
true
|
|
164
|
+
rescue Runtime::TimeoutError
|
|
165
|
+
false
|
|
166
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
167
|
+
true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def await_group(group, grace)
|
|
171
|
+
return true unless Runtime.scheduler
|
|
172
|
+
|
|
173
|
+
group.wait(grace)
|
|
174
|
+
true
|
|
175
|
+
rescue Runtime::TimeoutError
|
|
176
|
+
false
|
|
177
|
+
rescue StandardError
|
|
178
|
+
true
|
|
179
|
+
end
|
|
180
|
+
|
|
102
181
|
def cleanup_stale_socket
|
|
103
182
|
return unless File.exist?(@path)
|
|
104
183
|
|
|
105
184
|
begin
|
|
106
|
-
UNIXSocket.open(@path
|
|
185
|
+
UNIXSocket.open(@path, &:close)
|
|
107
186
|
|
|
108
187
|
raise "Socket #{@path} is already in use by another process (worker_index conflict?)"
|
|
109
188
|
rescue Errno::ECONNREFUSED, Errno::ENOENT
|
|
110
|
-
|
|
189
|
+
begin
|
|
190
|
+
File.unlink(@path)
|
|
191
|
+
rescue StandardError
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
111
194
|
end
|
|
112
195
|
end
|
|
113
196
|
|
|
@@ -4,17 +4,17 @@ module Async
|
|
|
4
4
|
module Background
|
|
5
5
|
module Queue
|
|
6
6
|
module SQL
|
|
7
|
-
USER_VERSION = 'PRAGMA user_version'
|
|
8
|
-
DATA_VERSION = 'PRAGMA data_version'
|
|
9
|
-
BUSY_TIMEOUT = 'PRAGMA busy_timeout'
|
|
10
|
-
TABLE_INFO = 'PRAGMA table_info(jobs)'
|
|
11
|
-
OPTIMIZE = 'PRAGMA optimize'
|
|
7
|
+
USER_VERSION = 'PRAGMA user_version'
|
|
8
|
+
DATA_VERSION = 'PRAGMA data_version'
|
|
9
|
+
BUSY_TIMEOUT = 'PRAGMA busy_timeout'
|
|
10
|
+
TABLE_INFO = 'PRAGMA table_info(jobs)'
|
|
11
|
+
OPTIMIZE = 'PRAGMA optimize'
|
|
12
12
|
INCREMENTAL_VACUUM_PAGES = 64
|
|
13
13
|
INCREMENTAL_VACUUM = "PRAGMA incremental_vacuum(#{INCREMENTAL_VACUUM_PAGES})".freeze
|
|
14
|
-
AUTO_VACUUM_INCREMENTAL = 'PRAGMA auto_vacuum = INCREMENTAL'
|
|
15
|
-
BEGIN_IMMEDIATE = 'BEGIN IMMEDIATE'
|
|
16
|
-
COMMIT = 'COMMIT'
|
|
17
|
-
ROLLBACK = 'ROLLBACK'
|
|
14
|
+
AUTO_VACUUM_INCREMENTAL = 'PRAGMA auto_vacuum = INCREMENTAL'
|
|
15
|
+
BEGIN_IMMEDIATE = 'BEGIN IMMEDIATE'
|
|
16
|
+
COMMIT = 'COMMIT'
|
|
17
|
+
ROLLBACK = 'ROLLBACK'
|
|
18
18
|
|
|
19
19
|
def self.busy_timeout(milliseconds)
|
|
20
20
|
"PRAGMA busy_timeout = #{Integer(milliseconds)}"
|
|
@@ -28,7 +28,7 @@ module Async
|
|
|
28
28
|
"ALTER TABLE jobs ADD COLUMN #{name} #{sql_type}"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
CREATE_SCHEMA = <<~SQL
|
|
31
|
+
CREATE_SCHEMA = <<~SQL
|
|
32
32
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
33
33
|
id INTEGER PRIMARY KEY,
|
|
34
34
|
class_name TEXT NOT NULL,
|
|
@@ -50,12 +50,12 @@ module Async
|
|
|
50
50
|
ON jobs(run_at) WHERE status = 'pending';
|
|
51
51
|
SQL
|
|
52
52
|
|
|
53
|
-
INSERT_JOB = <<~SQL
|
|
53
|
+
INSERT_JOB = <<~SQL
|
|
54
54
|
INSERT INTO jobs (class_name, args, options, created_at, run_at)
|
|
55
55
|
VALUES (?, ?, ?, ?, ?)
|
|
56
56
|
SQL
|
|
57
57
|
|
|
58
|
-
FETCH_NEXT_JOB = <<~SQL
|
|
58
|
+
FETCH_NEXT_JOB = <<~SQL
|
|
59
59
|
UPDATE jobs
|
|
60
60
|
SET status = 'running',
|
|
61
61
|
locked_by = ?,
|
|
@@ -73,13 +73,13 @@ module Async
|
|
|
73
73
|
RETURNING id, class_name, args, options
|
|
74
74
|
SQL
|
|
75
75
|
|
|
76
|
-
MARK_STARTED = <<~SQL
|
|
76
|
+
MARK_STARTED = <<~SQL
|
|
77
77
|
UPDATE jobs
|
|
78
78
|
SET started_at = ?
|
|
79
79
|
WHERE id = ? AND claim_token = ? AND status = 'running' AND started_at IS NULL
|
|
80
80
|
SQL
|
|
81
81
|
|
|
82
|
-
COMPLETE_JOB = <<~SQL
|
|
82
|
+
COMPLETE_JOB = <<~SQL
|
|
83
83
|
UPDATE jobs
|
|
84
84
|
SET status = 'done',
|
|
85
85
|
locked_by = NULL,
|
|
@@ -89,7 +89,7 @@ module Async
|
|
|
89
89
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
90
90
|
SQL
|
|
91
91
|
|
|
92
|
-
FAIL_JOB = <<~SQL
|
|
92
|
+
FAIL_JOB = <<~SQL
|
|
93
93
|
UPDATE jobs
|
|
94
94
|
SET status = 'failed',
|
|
95
95
|
locked_by = NULL,
|
|
@@ -101,19 +101,19 @@ module Async
|
|
|
101
101
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
102
102
|
SQL
|
|
103
103
|
|
|
104
|
-
RETRY_STATE = <<~SQL
|
|
104
|
+
RETRY_STATE = <<~SQL
|
|
105
105
|
SELECT options
|
|
106
106
|
FROM jobs
|
|
107
107
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
108
108
|
SQL
|
|
109
109
|
|
|
110
|
-
LEASE_ALIVE = <<~SQL
|
|
110
|
+
LEASE_ALIVE = <<~SQL
|
|
111
111
|
SELECT 1
|
|
112
112
|
FROM jobs
|
|
113
113
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
114
114
|
SQL
|
|
115
115
|
|
|
116
|
-
RETRY_JOB = <<~SQL
|
|
116
|
+
RETRY_JOB = <<~SQL
|
|
117
117
|
UPDATE jobs
|
|
118
118
|
SET status = 'pending',
|
|
119
119
|
locked_by = NULL,
|
|
@@ -129,7 +129,7 @@ module Async
|
|
|
129
129
|
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
130
130
|
SQL
|
|
131
131
|
|
|
132
|
-
RECOVER_WORKER = <<~SQL
|
|
132
|
+
RECOVER_WORKER = <<~SQL
|
|
133
133
|
UPDATE jobs
|
|
134
134
|
SET status = 'pending',
|
|
135
135
|
locked_by = NULL,
|
|
@@ -139,67 +139,67 @@ module Async
|
|
|
139
139
|
WHERE status = 'running' AND locked_by = ?
|
|
140
140
|
SQL
|
|
141
141
|
|
|
142
|
-
BACKFILL_FINISHED_AT = <<~SQL
|
|
142
|
+
BACKFILL_FINISHED_AT = <<~SQL
|
|
143
143
|
UPDATE jobs
|
|
144
144
|
SET finished_at = created_at
|
|
145
145
|
WHERE finished_at IS NULL AND status IN ('done', 'failed')
|
|
146
146
|
SQL
|
|
147
147
|
|
|
148
|
-
CLEANUP_DONE = <<~SQL
|
|
148
|
+
CLEANUP_DONE = <<~SQL
|
|
149
149
|
DELETE FROM jobs
|
|
150
150
|
WHERE status = 'done' AND finished_at IS NOT NULL AND finished_at < ?
|
|
151
151
|
SQL
|
|
152
152
|
|
|
153
|
-
CLEANUP_FAILED = <<~SQL
|
|
153
|
+
CLEANUP_FAILED = <<~SQL
|
|
154
154
|
DELETE FROM jobs
|
|
155
155
|
WHERE status = 'failed' AND finished_at IS NOT NULL AND finished_at < ?
|
|
156
156
|
SQL
|
|
157
157
|
|
|
158
|
-
NEXT_PENDING_RUN_AT = <<~SQL
|
|
158
|
+
NEXT_PENDING_RUN_AT = <<~SQL
|
|
159
159
|
SELECT MIN(run_at)
|
|
160
160
|
FROM jobs
|
|
161
161
|
WHERE status = 'pending'
|
|
162
162
|
SQL
|
|
163
163
|
|
|
164
|
-
JOBS_TABLE_EXISTS = <<~SQL
|
|
164
|
+
JOBS_TABLE_EXISTS = <<~SQL
|
|
165
165
|
SELECT 1
|
|
166
166
|
FROM sqlite_master
|
|
167
167
|
WHERE type = 'table' AND name = 'jobs'
|
|
168
168
|
LIMIT 1
|
|
169
169
|
SQL
|
|
170
170
|
|
|
171
|
-
JOB_INDEX_NAMES = <<~SQL
|
|
171
|
+
JOB_INDEX_NAMES = <<~SQL
|
|
172
172
|
SELECT name
|
|
173
173
|
FROM sqlite_master
|
|
174
174
|
WHERE type = 'index' AND tbl_name = 'jobs'
|
|
175
175
|
SQL
|
|
176
176
|
|
|
177
|
-
DROP_LEGACY_PENDING_INDEX = 'DROP INDEX IF EXISTS idx_jobs_status_run_at_id'
|
|
177
|
+
DROP_LEGACY_PENDING_INDEX = 'DROP INDEX IF EXISTS idx_jobs_status_run_at_id'
|
|
178
178
|
|
|
179
|
-
CREATE_PENDING_INDEX = <<~SQL
|
|
179
|
+
CREATE_PENDING_INDEX = <<~SQL
|
|
180
180
|
CREATE INDEX IF NOT EXISTS idx_jobs_pending
|
|
181
181
|
ON jobs(run_at) WHERE status = 'pending'
|
|
182
182
|
SQL
|
|
183
183
|
|
|
184
|
-
CREATE_DONE_INDEX = <<~SQL
|
|
184
|
+
CREATE_DONE_INDEX = <<~SQL
|
|
185
185
|
CREATE INDEX IF NOT EXISTS idx_jobs_done_finished_at
|
|
186
186
|
ON jobs(finished_at DESC, id DESC)
|
|
187
187
|
WHERE status = 'done'
|
|
188
188
|
SQL
|
|
189
189
|
|
|
190
|
-
CREATE_FAILED_INDEX = <<~SQL
|
|
190
|
+
CREATE_FAILED_INDEX = <<~SQL
|
|
191
191
|
CREATE INDEX IF NOT EXISTS idx_jobs_failed_finished_at
|
|
192
192
|
ON jobs(finished_at DESC, id DESC)
|
|
193
193
|
WHERE status = 'failed'
|
|
194
194
|
SQL
|
|
195
195
|
|
|
196
|
-
CREATE_EXECUTING_INDEX = <<~SQL
|
|
196
|
+
CREATE_EXECUTING_INDEX = <<~SQL
|
|
197
197
|
CREATE INDEX IF NOT EXISTS idx_jobs_executing_started_at
|
|
198
198
|
ON jobs(started_at)
|
|
199
199
|
WHERE status = 'running' AND started_at IS NOT NULL
|
|
200
200
|
SQL
|
|
201
201
|
|
|
202
|
-
CREATE_CLAIMED_INDEX = <<~SQL
|
|
202
|
+
CREATE_CLAIMED_INDEX = <<~SQL
|
|
203
203
|
CREATE INDEX IF NOT EXISTS idx_jobs_claimed_locked_at
|
|
204
204
|
ON jobs(locked_at)
|
|
205
205
|
WHERE status = 'running' AND started_at IS NULL
|
|
@@ -24,7 +24,7 @@ module Async
|
|
|
24
24
|
FAILED_RETENTION_AGE = 7 * 24 * 3600
|
|
25
25
|
CLEANUP_VACUUM_THRESHOLD = 100
|
|
26
26
|
ERROR_MESSAGE_MAX_LEN = 2_000
|
|
27
|
-
EMPTY_ARGS_JSON = '[]'
|
|
27
|
+
EMPTY_ARGS_JSON = '[]'
|
|
28
28
|
|
|
29
29
|
attr_reader :path, :options
|
|
30
30
|
|
|
@@ -131,7 +131,8 @@ module Async
|
|
|
131
131
|
@db.changes.positive?
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
def fail(job_id, claim_token:, error_class: nil, error_message: nil, finished_at: realtime_now,
|
|
134
|
+
def fail(job_id, claim_token:, error_class: nil, error_message: nil, finished_at: realtime_now,
|
|
135
|
+
duration_ms: nil)
|
|
135
136
|
ensure_connection
|
|
136
137
|
bind_failure(@fail_stmt, finished_at, duration_ms, error_class, error_message, job_id, claim_token)
|
|
137
138
|
@db.changes.positive?
|
|
@@ -153,8 +154,11 @@ module Async
|
|
|
153
154
|
next unless lease_alive?(job_id, claim_token)
|
|
154
155
|
|
|
155
156
|
policy = retry_policy(stored_options, fallback_options)
|
|
156
|
-
policy_retries?(policy)
|
|
157
|
+
if policy_retries?(policy)
|
|
158
|
+
retry_job!(job_id, claim_token, policy, error_class, error_message)
|
|
159
|
+
else
|
|
157
160
|
fail_job!(job_id, claim_token, error_class, error_message, finished_at, duration_ms)
|
|
161
|
+
end
|
|
158
162
|
end
|
|
159
163
|
end
|
|
160
164
|
|
|
@@ -178,7 +182,11 @@ module Async
|
|
|
178
182
|
return unless connected?
|
|
179
183
|
|
|
180
184
|
finalize_statements
|
|
181
|
-
|
|
185
|
+
begin
|
|
186
|
+
@db.execute(SQL::OPTIMIZE)
|
|
187
|
+
rescue StandardError
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
182
190
|
@db.close
|
|
183
191
|
@db = nil
|
|
184
192
|
@schema_checked = false
|
|
@@ -211,7 +219,7 @@ module Async
|
|
|
211
219
|
require 'sqlite3'
|
|
212
220
|
rescue LoadError
|
|
213
221
|
raise LoadError,
|
|
214
|
-
|
|
222
|
+
'sqlite3 gem is required for Async::Background::Queue. ' \
|
|
215
223
|
"Add `gem 'sqlite3', '~> 2.0'` to your Gemfile."
|
|
216
224
|
end
|
|
217
225
|
|
|
@@ -338,7 +346,11 @@ module Async
|
|
|
338
346
|
yield statement if block_given?
|
|
339
347
|
statement.step
|
|
340
348
|
ensure
|
|
341
|
-
|
|
349
|
+
begin
|
|
350
|
+
statement.reset!
|
|
351
|
+
rescue StandardError
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
342
354
|
end
|
|
343
355
|
|
|
344
356
|
def dump_args(args)
|
|
@@ -360,56 +372,43 @@ module Async
|
|
|
360
372
|
Job::Options.new(**options)
|
|
361
373
|
end
|
|
362
374
|
|
|
375
|
+
STATEMENTS = {
|
|
376
|
+
:@enqueue_stmt => SQL::INSERT_JOB,
|
|
377
|
+
:@fetch_stmt => SQL::FETCH_NEXT_JOB,
|
|
378
|
+
:@mark_started_stmt => SQL::MARK_STARTED,
|
|
379
|
+
:@complete_stmt => SQL::COMPLETE_JOB,
|
|
380
|
+
:@fail_stmt => SQL::FAIL_JOB,
|
|
381
|
+
:@retry_state_stmt => SQL::RETRY_STATE,
|
|
382
|
+
:@lease_check_stmt => SQL::LEASE_ALIVE,
|
|
383
|
+
:@retry_stmt => SQL::RETRY_JOB,
|
|
384
|
+
:@requeue_stmt => SQL::RECOVER_WORKER,
|
|
385
|
+
:@cleanup_done_stmt => SQL::CLEANUP_DONE,
|
|
386
|
+
:@cleanup_failed_stmt => SQL::CLEANUP_FAILED,
|
|
387
|
+
:@next_pending_stmt => SQL::NEXT_PENDING_RUN_AT,
|
|
388
|
+
:@begin_stmt => SQL::BEGIN_IMMEDIATE,
|
|
389
|
+
:@commit_stmt => SQL::COMMIT,
|
|
390
|
+
:@rollback_stmt => SQL::ROLLBACK
|
|
391
|
+
}.freeze
|
|
392
|
+
|
|
363
393
|
def prepare_statements
|
|
364
|
-
|
|
365
|
-
@fetch_stmt = @db.prepare(SQL::FETCH_NEXT_JOB)
|
|
366
|
-
@mark_started_stmt = @db.prepare(SQL::MARK_STARTED)
|
|
367
|
-
@complete_stmt = @db.prepare(SQL::COMPLETE_JOB)
|
|
368
|
-
@fail_stmt = @db.prepare(SQL::FAIL_JOB)
|
|
369
|
-
@retry_state_stmt = @db.prepare(SQL::RETRY_STATE)
|
|
370
|
-
@lease_check_stmt = @db.prepare(SQL::LEASE_ALIVE)
|
|
371
|
-
@retry_stmt = @db.prepare(SQL::RETRY_JOB)
|
|
372
|
-
@requeue_stmt = @db.prepare(SQL::RECOVER_WORKER)
|
|
373
|
-
@cleanup_done_stmt = @db.prepare(SQL::CLEANUP_DONE)
|
|
374
|
-
@cleanup_failed_stmt = @db.prepare(SQL::CLEANUP_FAILED)
|
|
375
|
-
@next_pending_stmt = @db.prepare(SQL::NEXT_PENDING_RUN_AT)
|
|
376
|
-
@begin_stmt = @db.prepare(SQL::BEGIN_IMMEDIATE)
|
|
377
|
-
@commit_stmt = @db.prepare(SQL::COMMIT)
|
|
378
|
-
@rollback_stmt = @db.prepare(SQL::ROLLBACK)
|
|
394
|
+
STATEMENTS.each { |name, sql| instance_variable_set(name, @db.prepare(sql)) }
|
|
379
395
|
end
|
|
380
396
|
|
|
381
397
|
def finalize_statements
|
|
382
|
-
statements.each
|
|
398
|
+
statements.each do |statement|
|
|
399
|
+
statement&.close
|
|
400
|
+
rescue StandardError
|
|
401
|
+
nil
|
|
402
|
+
end
|
|
383
403
|
clear_statements
|
|
384
404
|
end
|
|
385
405
|
|
|
386
406
|
def statements
|
|
387
|
-
|
|
388
|
-
@enqueue_stmt,
|
|
389
|
-
@fetch_stmt,
|
|
390
|
-
@mark_started_stmt,
|
|
391
|
-
@complete_stmt,
|
|
392
|
-
@fail_stmt,
|
|
393
|
-
@retry_state_stmt,
|
|
394
|
-
@lease_check_stmt,
|
|
395
|
-
@retry_stmt,
|
|
396
|
-
@requeue_stmt,
|
|
397
|
-
@cleanup_done_stmt,
|
|
398
|
-
@cleanup_failed_stmt,
|
|
399
|
-
@next_pending_stmt,
|
|
400
|
-
@begin_stmt,
|
|
401
|
-
@commit_stmt,
|
|
402
|
-
@rollback_stmt
|
|
403
|
-
]
|
|
407
|
+
STATEMENTS.keys.map { |name| instance_variable_get(name) }
|
|
404
408
|
end
|
|
405
409
|
|
|
406
410
|
def clear_statements
|
|
407
|
-
|
|
408
|
-
@complete_stmt = @fail_stmt = @retry_state_stmt = @lease_check_stmt = nil
|
|
409
|
-
@retry_stmt = @requeue_stmt = nil
|
|
410
|
-
@cleanup_done_stmt = @cleanup_failed_stmt = nil
|
|
411
|
-
@next_pending_stmt = nil
|
|
412
|
-
@begin_stmt = @commit_stmt = @rollback_stmt = nil
|
|
411
|
+
STATEMENTS.each_key { |name| instance_variable_set(name, nil) }
|
|
413
412
|
end
|
|
414
413
|
|
|
415
414
|
def maybe_cleanup
|