async-background 0.7.1 → 0.7.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 +5 -0
- data/README.md +48 -7
- data/async-background.gemspec +2 -1
- data/lib/async/background/job.rb +5 -3
- data/lib/async/background/metrics.rb +157 -86
- 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 +160 -0
- data/lib/async/background/queue/sql.rb +205 -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 +127 -0
- data/lib/async/background/runner.rb +99 -231
- data/lib/async/background/version.rb +1 -1
- metadata +27 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'sql'
|
|
4
|
+
|
|
5
|
+
module Async
|
|
6
|
+
module Background
|
|
7
|
+
module Queue
|
|
8
|
+
class Store
|
|
9
|
+
class SchemaError < StandardError; end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Schema
|
|
13
|
+
VERSION = 1
|
|
14
|
+
MIGRATION_BUSY_TIMEOUT_MS = 30_000
|
|
15
|
+
CORE_INDEXES = %w[idx_jobs_pending].freeze
|
|
16
|
+
DASHBOARD_INDEXES = %w[idx_jobs_done_finished_at idx_jobs_failed_finished_at idx_jobs_running].freeze
|
|
17
|
+
REQUIRED_INDEXES = CORE_INDEXES
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def migrate!(db)
|
|
22
|
+
reject_future_version!(db)
|
|
23
|
+
return if current?(db)
|
|
24
|
+
|
|
25
|
+
enable_incremental_vacuum!(db) unless jobs_table?(db)
|
|
26
|
+
|
|
27
|
+
with_migration_timeout(db) do
|
|
28
|
+
immediate_transaction(db) do
|
|
29
|
+
reject_future_version!(db)
|
|
30
|
+
upgrade!(db) unless current?(db)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def prepare_dashboard!(db)
|
|
36
|
+
migrate!(db)
|
|
37
|
+
return if dashboard_indexes_current?(db)
|
|
38
|
+
|
|
39
|
+
with_migration_timeout(db) do
|
|
40
|
+
immediate_transaction(db) do
|
|
41
|
+
create_dashboard_indexes!(db) unless dashboard_indexes_current?(db)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def current?(db)
|
|
47
|
+
jobs_table?(db) && version(db) == VERSION && core_indexes_current?(db)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def dashboard_indexes_current?(db)
|
|
51
|
+
(DASHBOARD_INDEXES - index_names(db)).empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def version(db)
|
|
55
|
+
db.get_first_value(SQL::USER_VERSION).to_i
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def upgrade!(db)
|
|
59
|
+
jobs_table?(db) ? upgrade_existing_database!(db) : create_current_schema!(db)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_current_schema!(db)
|
|
63
|
+
db.execute_batch(SQL::CREATE_SCHEMA)
|
|
64
|
+
set_version!(db, VERSION)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def upgrade_existing_database!(db)
|
|
68
|
+
add_column_unless_exists!(db, 'options', 'TEXT')
|
|
69
|
+
add_lifecycle_columns!(db)
|
|
70
|
+
backfill_finished_at!(db)
|
|
71
|
+
ensure_pending_index!(db)
|
|
72
|
+
set_version!(db, VERSION)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_lifecycle_columns!(db)
|
|
76
|
+
{
|
|
77
|
+
'claim_token' => 'TEXT',
|
|
78
|
+
'started_at' => 'REAL',
|
|
79
|
+
'finished_at' => 'REAL',
|
|
80
|
+
'duration_ms' => 'INTEGER',
|
|
81
|
+
'last_error_class' => 'TEXT',
|
|
82
|
+
'last_error_message' => 'TEXT'
|
|
83
|
+
}.each { |name, type| add_column_unless_exists!(db, name, type) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def backfill_finished_at!(db)
|
|
87
|
+
db.execute(SQL::BACKFILL_FINISHED_AT)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def ensure_pending_index!(db)
|
|
91
|
+
db.execute(SQL::DROP_LEGACY_PENDING_INDEX)
|
|
92
|
+
db.execute(SQL::CREATE_PENDING_INDEX)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create_dashboard_indexes!(db)
|
|
96
|
+
SQL::CREATE_DASHBOARD_INDEXES.each { |statement| db.execute(statement) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def core_indexes_current?(db)
|
|
100
|
+
(CORE_INDEXES - index_names(db)).empty?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def jobs_table?(db)
|
|
104
|
+
!db.get_first_value(SQL::JOBS_TABLE_EXISTS).nil?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def index_names(db)
|
|
108
|
+
db.execute(SQL::JOB_INDEX_NAMES).map(&:first)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def add_column_unless_exists!(db, name, sql_type)
|
|
112
|
+
return if table_columns(db).include?(name)
|
|
113
|
+
|
|
114
|
+
db.execute(SQL.add_column(name, sql_type))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def table_columns(db)
|
|
118
|
+
db.execute(SQL::TABLE_INFO).map { |row| row[1] }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def reject_future_version!(db)
|
|
122
|
+
return unless version(db) > VERSION
|
|
123
|
+
|
|
124
|
+
raise Store::SchemaError,
|
|
125
|
+
"queue database schema #{version(db)} is newer than supported schema #{VERSION}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def set_version!(db, value)
|
|
129
|
+
db.execute(SQL.user_version(value))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def enable_incremental_vacuum!(db)
|
|
133
|
+
db.execute(SQL::AUTO_VACUUM_INCREMENTAL)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def with_migration_timeout(db)
|
|
137
|
+
original_timeout = db.get_first_value(SQL::BUSY_TIMEOUT).to_i
|
|
138
|
+
db.execute(SQL.busy_timeout(MIGRATION_BUSY_TIMEOUT_MS))
|
|
139
|
+
yield
|
|
140
|
+
ensure
|
|
141
|
+
begin
|
|
142
|
+
db.execute(SQL.busy_timeout(original_timeout)) if defined?(original_timeout)
|
|
143
|
+
rescue StandardError
|
|
144
|
+
# Restoring a connection option must not hide the migration exception.
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def immediate_transaction(db)
|
|
149
|
+
db.execute(SQL::BEGIN_IMMEDIATE)
|
|
150
|
+
result = yield
|
|
151
|
+
db.execute(SQL::COMMIT)
|
|
152
|
+
result
|
|
153
|
+
rescue StandardError
|
|
154
|
+
db.execute(SQL::ROLLBACK) rescue nil
|
|
155
|
+
raise
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Queue
|
|
6
|
+
module SQL
|
|
7
|
+
USER_VERSION = 'PRAGMA user_version'.freeze
|
|
8
|
+
DATA_VERSION = 'PRAGMA data_version'.freeze
|
|
9
|
+
BUSY_TIMEOUT = 'PRAGMA busy_timeout'.freeze
|
|
10
|
+
TABLE_INFO = 'PRAGMA table_info(jobs)'.freeze
|
|
11
|
+
OPTIMIZE = 'PRAGMA optimize'.freeze
|
|
12
|
+
INCREMENTAL_VACUUM = 'PRAGMA incremental_vacuum'.freeze
|
|
13
|
+
AUTO_VACUUM_INCREMENTAL = 'PRAGMA auto_vacuum = INCREMENTAL'.freeze
|
|
14
|
+
BEGIN_IMMEDIATE = 'BEGIN IMMEDIATE'.freeze
|
|
15
|
+
COMMIT = 'COMMIT'.freeze
|
|
16
|
+
ROLLBACK = 'ROLLBACK'.freeze
|
|
17
|
+
|
|
18
|
+
def self.busy_timeout(milliseconds)
|
|
19
|
+
"PRAGMA busy_timeout = #{Integer(milliseconds)}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.user_version(version)
|
|
23
|
+
"PRAGMA user_version = #{Integer(version)}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.add_column(name, sql_type)
|
|
27
|
+
"ALTER TABLE jobs ADD COLUMN #{name} #{sql_type}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
CREATE_SCHEMA = <<~SQL.freeze
|
|
31
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
32
|
+
id INTEGER PRIMARY KEY,
|
|
33
|
+
class_name TEXT NOT NULL,
|
|
34
|
+
args TEXT NOT NULL DEFAULT '[]',
|
|
35
|
+
options TEXT,
|
|
36
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
37
|
+
created_at REAL NOT NULL,
|
|
38
|
+
run_at REAL NOT NULL,
|
|
39
|
+
locked_by INTEGER,
|
|
40
|
+
locked_at REAL,
|
|
41
|
+
claim_token TEXT,
|
|
42
|
+
started_at REAL,
|
|
43
|
+
finished_at REAL,
|
|
44
|
+
duration_ms INTEGER,
|
|
45
|
+
last_error_class TEXT,
|
|
46
|
+
last_error_message TEXT
|
|
47
|
+
);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_pending
|
|
49
|
+
ON jobs(run_at) WHERE status = 'pending';
|
|
50
|
+
SQL
|
|
51
|
+
|
|
52
|
+
INSERT_JOB = <<~SQL.freeze
|
|
53
|
+
INSERT INTO jobs (class_name, args, options, created_at, run_at)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?)
|
|
55
|
+
SQL
|
|
56
|
+
|
|
57
|
+
FETCH_NEXT_JOB = <<~SQL.freeze
|
|
58
|
+
UPDATE jobs
|
|
59
|
+
SET status = 'running',
|
|
60
|
+
locked_by = ?,
|
|
61
|
+
locked_at = ?,
|
|
62
|
+
claim_token = ?,
|
|
63
|
+
started_at = NULL,
|
|
64
|
+
finished_at = NULL,
|
|
65
|
+
duration_ms = NULL
|
|
66
|
+
WHERE id = (
|
|
67
|
+
SELECT id FROM jobs
|
|
68
|
+
WHERE status = 'pending' AND run_at <= ?
|
|
69
|
+
ORDER BY run_at, id
|
|
70
|
+
LIMIT 1
|
|
71
|
+
)
|
|
72
|
+
RETURNING id, class_name, args, options
|
|
73
|
+
SQL
|
|
74
|
+
|
|
75
|
+
MARK_STARTED = <<~SQL.freeze
|
|
76
|
+
UPDATE jobs
|
|
77
|
+
SET started_at = ?
|
|
78
|
+
WHERE id = ? AND claim_token = ? AND status = 'running' AND started_at IS NULL
|
|
79
|
+
SQL
|
|
80
|
+
|
|
81
|
+
COMPLETE_JOB = <<~SQL.freeze
|
|
82
|
+
UPDATE jobs
|
|
83
|
+
SET status = 'done',
|
|
84
|
+
locked_by = NULL,
|
|
85
|
+
locked_at = NULL,
|
|
86
|
+
finished_at = ?,
|
|
87
|
+
duration_ms = ?
|
|
88
|
+
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
89
|
+
SQL
|
|
90
|
+
|
|
91
|
+
FAIL_JOB = <<~SQL.freeze
|
|
92
|
+
UPDATE jobs
|
|
93
|
+
SET status = 'failed',
|
|
94
|
+
locked_by = NULL,
|
|
95
|
+
locked_at = NULL,
|
|
96
|
+
finished_at = ?,
|
|
97
|
+
duration_ms = ?,
|
|
98
|
+
last_error_class = ?,
|
|
99
|
+
last_error_message = ?
|
|
100
|
+
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
101
|
+
SQL
|
|
102
|
+
|
|
103
|
+
RETRY_STATE = <<~SQL.freeze
|
|
104
|
+
SELECT options
|
|
105
|
+
FROM jobs
|
|
106
|
+
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
107
|
+
SQL
|
|
108
|
+
|
|
109
|
+
LEASE_ALIVE = <<~SQL.freeze
|
|
110
|
+
SELECT 1
|
|
111
|
+
FROM jobs
|
|
112
|
+
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
113
|
+
SQL
|
|
114
|
+
|
|
115
|
+
RETRY_JOB = <<~SQL.freeze
|
|
116
|
+
UPDATE jobs
|
|
117
|
+
SET status = 'pending',
|
|
118
|
+
locked_by = NULL,
|
|
119
|
+
locked_at = NULL,
|
|
120
|
+
claim_token = NULL,
|
|
121
|
+
started_at = NULL,
|
|
122
|
+
finished_at = NULL,
|
|
123
|
+
duration_ms = NULL,
|
|
124
|
+
run_at = ?,
|
|
125
|
+
options = ?,
|
|
126
|
+
last_error_class = ?,
|
|
127
|
+
last_error_message = ?
|
|
128
|
+
WHERE id = ? AND claim_token = ? AND status = 'running'
|
|
129
|
+
SQL
|
|
130
|
+
|
|
131
|
+
RECOVER_WORKER = <<~SQL.freeze
|
|
132
|
+
UPDATE jobs
|
|
133
|
+
SET status = 'pending',
|
|
134
|
+
locked_by = NULL,
|
|
135
|
+
locked_at = NULL,
|
|
136
|
+
claim_token = NULL,
|
|
137
|
+
started_at = NULL
|
|
138
|
+
WHERE status = 'running' AND locked_by = ?
|
|
139
|
+
SQL
|
|
140
|
+
|
|
141
|
+
BACKFILL_FINISHED_AT = <<~SQL.freeze
|
|
142
|
+
UPDATE jobs
|
|
143
|
+
SET finished_at = created_at
|
|
144
|
+
WHERE finished_at IS NULL AND status IN ('done', 'failed')
|
|
145
|
+
SQL
|
|
146
|
+
|
|
147
|
+
CLEANUP_DONE = <<~SQL.freeze
|
|
148
|
+
DELETE FROM jobs
|
|
149
|
+
WHERE status = 'done' AND finished_at IS NOT NULL AND finished_at < ?
|
|
150
|
+
SQL
|
|
151
|
+
|
|
152
|
+
CLEANUP_FAILED = <<~SQL.freeze
|
|
153
|
+
DELETE FROM jobs
|
|
154
|
+
WHERE status = 'failed' AND finished_at IS NOT NULL AND finished_at < ?
|
|
155
|
+
SQL
|
|
156
|
+
|
|
157
|
+
NEXT_PENDING_RUN_AT = <<~SQL.freeze
|
|
158
|
+
SELECT MIN(run_at)
|
|
159
|
+
FROM jobs
|
|
160
|
+
WHERE status = 'pending'
|
|
161
|
+
SQL
|
|
162
|
+
|
|
163
|
+
JOBS_TABLE_EXISTS = <<~SQL.freeze
|
|
164
|
+
SELECT 1
|
|
165
|
+
FROM sqlite_master
|
|
166
|
+
WHERE type = 'table' AND name = 'jobs'
|
|
167
|
+
LIMIT 1
|
|
168
|
+
SQL
|
|
169
|
+
|
|
170
|
+
JOB_INDEX_NAMES = <<~SQL.freeze
|
|
171
|
+
SELECT name
|
|
172
|
+
FROM sqlite_master
|
|
173
|
+
WHERE type = 'index' AND tbl_name = 'jobs'
|
|
174
|
+
SQL
|
|
175
|
+
|
|
176
|
+
DROP_LEGACY_PENDING_INDEX = 'DROP INDEX IF EXISTS idx_jobs_status_run_at_id'.freeze
|
|
177
|
+
|
|
178
|
+
CREATE_PENDING_INDEX = <<~SQL.freeze
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_pending
|
|
180
|
+
ON jobs(run_at) WHERE status = 'pending'
|
|
181
|
+
SQL
|
|
182
|
+
|
|
183
|
+
CREATE_DONE_INDEX = <<~SQL.freeze
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_done_finished_at
|
|
185
|
+
ON jobs(finished_at DESC, id DESC)
|
|
186
|
+
WHERE status = 'done'
|
|
187
|
+
SQL
|
|
188
|
+
|
|
189
|
+
CREATE_FAILED_INDEX = <<~SQL.freeze
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_failed_finished_at
|
|
191
|
+
ON jobs(finished_at DESC, id DESC)
|
|
192
|
+
WHERE status = 'failed'
|
|
193
|
+
SQL
|
|
194
|
+
|
|
195
|
+
CREATE_RUNNING_INDEX = <<~SQL.freeze
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_running
|
|
197
|
+
ON jobs(locked_at)
|
|
198
|
+
WHERE status = 'running'
|
|
199
|
+
SQL
|
|
200
|
+
|
|
201
|
+
CREATE_DASHBOARD_INDEXES = [CREATE_DONE_INDEX, CREATE_FAILED_INDEX, CREATE_RUNNING_INDEX].freeze
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|