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
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../clock'
|
|
4
|
+
require_relative 'store'
|
|
4
5
|
|
|
5
6
|
module Async
|
|
6
7
|
module Background
|
|
7
8
|
module Queue
|
|
8
|
-
EMPTY_OPTIONS = {}.freeze
|
|
9
|
-
|
|
10
9
|
class Client
|
|
11
10
|
include Clock
|
|
12
11
|
|
|
@@ -15,17 +14,17 @@ module Async
|
|
|
15
14
|
@notifier = notifier
|
|
16
15
|
end
|
|
17
16
|
|
|
18
|
-
def push(class_name, args =
|
|
17
|
+
def push(class_name, args = EMPTY_ARGS, run_at = nil, options: EMPTY_OPTIONS)
|
|
19
18
|
id = @store.enqueue(class_name, args, run_at, options: options)
|
|
20
19
|
@notifier&.notify_all
|
|
21
20
|
id
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
def push_in(delay, class_name, args =
|
|
23
|
+
def push_in(delay, class_name, args = EMPTY_ARGS, options: EMPTY_OPTIONS)
|
|
25
24
|
push(class_name, args, realtime_now + delay.to_f, options: options)
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
def push_at(time, class_name, args =
|
|
27
|
+
def push_at(time, class_name, args = EMPTY_ARGS, options: EMPTY_OPTIONS)
|
|
29
28
|
run_at = time.respond_to?(:to_f) ? time.to_f : time
|
|
30
29
|
push(class_name, args, run_at, options: options)
|
|
31
30
|
end
|
|
@@ -34,19 +33,27 @@ module Async
|
|
|
34
33
|
class << self
|
|
35
34
|
attr_accessor :default_client
|
|
36
35
|
|
|
37
|
-
def
|
|
36
|
+
def migrate!(path: Store.default_path, options: {})
|
|
37
|
+
Store.migrate!(path: path, options: options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def prepare_dashboard!(path: Store.default_path, options: {})
|
|
41
|
+
Store.prepare_dashboard!(path: path, options: options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def enqueue(job_class, *args, options: EMPTY_OPTIONS)
|
|
38
45
|
ensure_configured!
|
|
39
|
-
default_client.push(resolve_class_name(job_class), args, nil, options: build_options(job_class, options))
|
|
46
|
+
default_client.push(resolve_class_name(job_class), normalized_args(args), nil, options: build_options(job_class, options))
|
|
40
47
|
end
|
|
41
48
|
|
|
42
|
-
def enqueue_in(delay, job_class, *args, options:
|
|
49
|
+
def enqueue_in(delay, job_class, *args, options: EMPTY_OPTIONS)
|
|
43
50
|
ensure_configured!
|
|
44
|
-
default_client.push_in(delay, resolve_class_name(job_class), args, options: build_options(job_class, options))
|
|
51
|
+
default_client.push_in(delay, resolve_class_name(job_class), normalized_args(args), options: build_options(job_class, options))
|
|
45
52
|
end
|
|
46
53
|
|
|
47
|
-
def enqueue_at(time, job_class, *args, options:
|
|
54
|
+
def enqueue_at(time, job_class, *args, options: EMPTY_OPTIONS)
|
|
48
55
|
ensure_configured!
|
|
49
|
-
default_client.push_at(time, resolve_class_name(job_class), args, options: build_options(job_class, options))
|
|
56
|
+
default_client.push_at(time, resolve_class_name(job_class), normalized_args(args), options: build_options(job_class, options))
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
private
|
|
@@ -55,8 +62,11 @@ module Async
|
|
|
55
62
|
private_constant :RETRY_KEYS
|
|
56
63
|
|
|
57
64
|
def build_options(job_class, call_site)
|
|
58
|
-
call_site ||=
|
|
59
|
-
|
|
65
|
+
call_site ||= EMPTY_OPTIONS
|
|
66
|
+
class_options = resolve_options(job_class)
|
|
67
|
+
return EMPTY_OPTIONS if class_options.empty? && call_site.empty?
|
|
68
|
+
|
|
69
|
+
merged = class_options.merge(call_site.compact)
|
|
60
70
|
apply_retry_overrides!(merged, call_site)
|
|
61
71
|
|
|
62
72
|
merged.empty? ? EMPTY_OPTIONS : Job::Options.new(**merged).to_h.compact
|
|
@@ -72,6 +82,8 @@ module Async
|
|
|
72
82
|
raise "Async::Background::Queue not configured" unless default_client
|
|
73
83
|
end
|
|
74
84
|
|
|
85
|
+
def normalized_args(args) = args.empty? ? EMPTY_ARGS : args
|
|
86
|
+
|
|
75
87
|
def resolve_class_name(job_class)
|
|
76
88
|
return job_class if job_class.is_a?(String)
|
|
77
89
|
return job_class.name if job_class.respond_to?(:perform_now)
|
|
@@ -80,9 +92,15 @@ module Async
|
|
|
80
92
|
end
|
|
81
93
|
|
|
82
94
|
def resolve_options(job_class)
|
|
83
|
-
return
|
|
95
|
+
return EMPTY_OPTIONS unless job_class.respond_to?(:resolve_options)
|
|
96
|
+
|
|
97
|
+
options = if job_class.respond_to?(:queue_options)
|
|
98
|
+
job_class.queue_options
|
|
99
|
+
else
|
|
100
|
+
job_class.resolve_options
|
|
101
|
+
end
|
|
84
102
|
|
|
85
|
-
|
|
103
|
+
options.empty? ? EMPTY_OPTIONS : options
|
|
86
104
|
end
|
|
87
105
|
end
|
|
88
106
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Queue
|
|
6
|
+
EMPTY_ARGS = [].freeze
|
|
7
|
+
EMPTY_OPTIONS = {}.freeze
|
|
8
|
+
|
|
9
|
+
SYNCHRONOUS_LEVELS = {normal: 'NORMAL', full: 'FULL', extra: 'EXTRA'}.freeze
|
|
10
|
+
WAL_AUTOCHECKPOINT_RANGE = 100..10_000
|
|
11
|
+
DEFAULT_STORE_OPTIONS = {mmap: true, synchronous: :normal, wal_autocheckpoint: 1_000}.freeze
|
|
12
|
+
DEFAULTS = DEFAULT_STORE_OPTIONS
|
|
13
|
+
MMAP_SIZE = 268_435_456
|
|
14
|
+
|
|
15
|
+
StoreOptions = Data.define(:mmap, :synchronous, :wal_autocheckpoint) do
|
|
16
|
+
def self.build(value = {})
|
|
17
|
+
return value if value.is_a?(self)
|
|
18
|
+
|
|
19
|
+
new(**DEFAULT_STORE_OPTIONS, **value)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(mmap:, synchronous:, wal_autocheckpoint:)
|
|
23
|
+
validate_mmap!(mmap)
|
|
24
|
+
validate_synchronous!(synchronous)
|
|
25
|
+
validate_wal_autocheckpoint!(wal_autocheckpoint)
|
|
26
|
+
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def synchronous_pragma = SYNCHRONOUS_LEVELS.fetch(synchronous)
|
|
31
|
+
def mmap_size = mmap ? MMAP_SIZE : 0
|
|
32
|
+
|
|
33
|
+
def pragma_sql
|
|
34
|
+
<<~SQL
|
|
35
|
+
PRAGMA journal_mode = WAL;
|
|
36
|
+
PRAGMA synchronous = #{synchronous_pragma};
|
|
37
|
+
PRAGMA mmap_size = #{mmap_size};
|
|
38
|
+
PRAGMA cache_size = -16000;
|
|
39
|
+
PRAGMA temp_store = MEMORY;
|
|
40
|
+
PRAGMA journal_size_limit = 67108864;
|
|
41
|
+
PRAGMA wal_autocheckpoint = #{wal_autocheckpoint};
|
|
42
|
+
SQL
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def validate_mmap!(value)
|
|
48
|
+
return if value == true || value == false
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, "mmap must be true or false, got #{value.inspect}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_synchronous!(value)
|
|
54
|
+
return if SYNCHRONOUS_LEVELS.key?(value)
|
|
55
|
+
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"synchronous must be one of #{SYNCHRONOUS_LEVELS.keys.inspect}, got #{value.inspect}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_wal_autocheckpoint!(value)
|
|
61
|
+
return if value.is_a?(Integer) && WAL_AUTOCHECKPOINT_RANGE.cover?(value)
|
|
62
|
+
|
|
63
|
+
raise ArgumentError,
|
|
64
|
+
"wal_autocheckpoint must be an Integer in #{WAL_AUTOCHECKPOINT_RANGE}, " \
|
|
65
|
+
"got #{value.inspect}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
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[
|
|
17
|
+
idx_jobs_done_finished_at
|
|
18
|
+
idx_jobs_failed_finished_at
|
|
19
|
+
idx_jobs_executing_started_at
|
|
20
|
+
idx_jobs_claimed_locked_at
|
|
21
|
+
].freeze
|
|
22
|
+
REQUIRED_INDEXES = CORE_INDEXES
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def migrate!(db)
|
|
27
|
+
reject_future_version!(db)
|
|
28
|
+
return if current?(db)
|
|
29
|
+
|
|
30
|
+
enable_incremental_vacuum!(db) unless jobs_table?(db)
|
|
31
|
+
|
|
32
|
+
with_migration_timeout(db) do
|
|
33
|
+
immediate_transaction(db) do
|
|
34
|
+
reject_future_version!(db)
|
|
35
|
+
upgrade!(db) unless current?(db)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def prepare_dashboard!(db)
|
|
41
|
+
migrate!(db)
|
|
42
|
+
return if dashboard_indexes_current?(db)
|
|
43
|
+
|
|
44
|
+
with_migration_timeout(db) do
|
|
45
|
+
immediate_transaction(db) do
|
|
46
|
+
create_dashboard_indexes!(db) unless dashboard_indexes_current?(db)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def current?(db)
|
|
52
|
+
jobs_table?(db) && version(db) == VERSION && core_indexes_current?(db)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def dashboard_indexes_current?(db)
|
|
56
|
+
(DASHBOARD_INDEXES - index_names(db)).empty?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def version(db)
|
|
60
|
+
db.get_first_value(SQL::USER_VERSION).to_i
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def upgrade!(db)
|
|
64
|
+
jobs_table?(db) ? upgrade_existing_database!(db) : create_current_schema!(db)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_current_schema!(db)
|
|
68
|
+
db.execute_batch(SQL::CREATE_SCHEMA)
|
|
69
|
+
set_version!(db, VERSION)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def upgrade_existing_database!(db)
|
|
73
|
+
add_column_unless_exists!(db, 'options', 'TEXT')
|
|
74
|
+
add_lifecycle_columns!(db)
|
|
75
|
+
backfill_finished_at!(db)
|
|
76
|
+
ensure_pending_index!(db)
|
|
77
|
+
set_version!(db, VERSION)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_lifecycle_columns!(db)
|
|
81
|
+
{
|
|
82
|
+
'claim_token' => 'TEXT',
|
|
83
|
+
'started_at' => 'REAL',
|
|
84
|
+
'finished_at' => 'REAL',
|
|
85
|
+
'duration_ms' => 'INTEGER',
|
|
86
|
+
'last_error_class' => 'TEXT',
|
|
87
|
+
'last_error_message' => 'TEXT'
|
|
88
|
+
}.each { |name, type| add_column_unless_exists!(db, name, type) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def backfill_finished_at!(db)
|
|
92
|
+
db.execute(SQL::BACKFILL_FINISHED_AT)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def ensure_pending_index!(db)
|
|
96
|
+
db.execute(SQL::DROP_LEGACY_PENDING_INDEX)
|
|
97
|
+
db.execute(SQL::CREATE_PENDING_INDEX)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def create_dashboard_indexes!(db)
|
|
101
|
+
SQL::CREATE_DASHBOARD_INDEXES.each { |statement| db.execute(statement) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def core_indexes_current?(db)
|
|
105
|
+
(CORE_INDEXES - index_names(db)).empty?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def jobs_table?(db)
|
|
109
|
+
!db.get_first_value(SQL::JOBS_TABLE_EXISTS).nil?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def index_names(db)
|
|
113
|
+
db.execute(SQL::JOB_INDEX_NAMES).map(&:first)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def add_column_unless_exists!(db, name, sql_type)
|
|
117
|
+
return if table_columns(db).include?(name)
|
|
118
|
+
|
|
119
|
+
db.execute(SQL.add_column(name, sql_type))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def table_columns(db)
|
|
123
|
+
db.execute(SQL::TABLE_INFO).map { |row| row[1] }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def reject_future_version!(db)
|
|
127
|
+
return unless version(db) > VERSION
|
|
128
|
+
|
|
129
|
+
raise Store::SchemaError,
|
|
130
|
+
"queue database schema #{version(db)} is newer than supported schema #{VERSION}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def set_version!(db, value)
|
|
134
|
+
db.execute(SQL.user_version(value))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def enable_incremental_vacuum!(db)
|
|
138
|
+
db.execute(SQL::AUTO_VACUUM_INCREMENTAL)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def with_migration_timeout(db)
|
|
142
|
+
original_timeout = db.get_first_value(SQL::BUSY_TIMEOUT).to_i
|
|
143
|
+
db.execute(SQL.busy_timeout(MIGRATION_BUSY_TIMEOUT_MS))
|
|
144
|
+
yield
|
|
145
|
+
ensure
|
|
146
|
+
begin
|
|
147
|
+
db.execute(SQL.busy_timeout(original_timeout)) if defined?(original_timeout)
|
|
148
|
+
rescue StandardError
|
|
149
|
+
# Restoring a connection option must not hide the migration exception.
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def immediate_transaction(db)
|
|
154
|
+
db.execute(SQL::BEGIN_IMMEDIATE)
|
|
155
|
+
result = yield
|
|
156
|
+
db.execute(SQL::COMMIT)
|
|
157
|
+
result
|
|
158
|
+
rescue StandardError
|
|
159
|
+
db.execute(SQL::ROLLBACK) rescue nil
|
|
160
|
+
raise
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
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_EXECUTING_INDEX = <<~SQL.freeze
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_executing_started_at
|
|
197
|
+
ON jobs(started_at)
|
|
198
|
+
WHERE status = 'running' AND started_at IS NOT NULL
|
|
199
|
+
SQL
|
|
200
|
+
|
|
201
|
+
CREATE_CLAIMED_INDEX = <<~SQL.freeze
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_claimed_locked_at
|
|
203
|
+
ON jobs(locked_at)
|
|
204
|
+
WHERE status = 'running' AND started_at IS NULL
|
|
205
|
+
SQL
|
|
206
|
+
|
|
207
|
+
CREATE_DASHBOARD_INDEXES = [
|
|
208
|
+
CREATE_DONE_INDEX,
|
|
209
|
+
CREATE_FAILED_INDEX,
|
|
210
|
+
CREATE_EXECUTING_INDEX,
|
|
211
|
+
CREATE_CLAIMED_INDEX
|
|
212
|
+
].freeze
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|