async-background 0.7.0 → 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 +18 -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 +275 -122
- 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 +31 -3
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
4
6
|
require_relative '../clock'
|
|
7
|
+
require_relative 'options'
|
|
8
|
+
require_relative 'schema'
|
|
9
|
+
require_relative 'sql'
|
|
5
10
|
|
|
6
11
|
module Async
|
|
7
12
|
module Background
|
|
@@ -9,101 +14,133 @@ module Async
|
|
|
9
14
|
class Store
|
|
10
15
|
include Clock
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class_name TEXT NOT NULL,
|
|
17
|
-
args TEXT NOT NULL DEFAULT '[]',
|
|
18
|
-
options TEXT,
|
|
19
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
20
|
-
created_at REAL NOT NULL,
|
|
21
|
-
run_at REAL NOT NULL,
|
|
22
|
-
locked_by INTEGER,
|
|
23
|
-
locked_at REAL
|
|
24
|
-
);
|
|
25
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_pending ON jobs(run_at, id) WHERE status = 'pending';
|
|
26
|
-
SQL
|
|
27
|
-
|
|
28
|
-
MMAP_SIZE = 268_435_456
|
|
29
|
-
PRAGMAS = ->(mmap_size) {
|
|
30
|
-
<<~SQL
|
|
31
|
-
PRAGMA journal_mode = WAL;
|
|
32
|
-
PRAGMA synchronous = NORMAL;
|
|
33
|
-
PRAGMA mmap_size = #{mmap_size};
|
|
34
|
-
PRAGMA cache_size = -16000;
|
|
35
|
-
PRAGMA temp_store = MEMORY;
|
|
36
|
-
PRAGMA busy_timeout = 5000;
|
|
37
|
-
PRAGMA journal_size_limit = 67108864;
|
|
38
|
-
SQL
|
|
39
|
-
}.freeze
|
|
17
|
+
SCHEMA_VERSION = Schema::VERSION
|
|
18
|
+
MIGRATION_BUSY_TIMEOUT_MS = Schema::MIGRATION_BUSY_TIMEOUT_MS
|
|
19
|
+
REQUIRED_INDEXES = Schema::REQUIRED_INDEXES
|
|
20
|
+
SCHEMA = SQL::CREATE_SCHEMA
|
|
40
21
|
|
|
41
22
|
CLEANUP_INTERVAL = 300
|
|
42
|
-
CLEANUP_AGE
|
|
23
|
+
CLEANUP_AGE = 3600
|
|
24
|
+
FAILED_RETENTION_AGE = 7 * 24 * 3600
|
|
25
|
+
ERROR_MESSAGE_MAX_LEN = 2_000
|
|
26
|
+
EMPTY_ARGS_JSON = '[]'.freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :path, :options
|
|
29
|
+
|
|
30
|
+
def self.migrate!(path: default_path, options: {})
|
|
31
|
+
store = new(path: path, options: options)
|
|
32
|
+
store.migrate!
|
|
33
|
+
SCHEMA_VERSION
|
|
34
|
+
ensure
|
|
35
|
+
store&.close
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.prepare_dashboard!(path: default_path, options: {})
|
|
39
|
+
store = new(path: path, options: options)
|
|
40
|
+
store.prepare_dashboard!
|
|
41
|
+
SCHEMA_VERSION
|
|
42
|
+
ensure
|
|
43
|
+
store&.close
|
|
44
|
+
end
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
def self.default_path = 'async_background_queue.db'
|
|
45
47
|
|
|
46
|
-
def initialize(path: self.class.default_path,
|
|
47
|
-
@path
|
|
48
|
-
@
|
|
49
|
-
@
|
|
50
|
-
@
|
|
48
|
+
def initialize(path: self.class.default_path, options: {})
|
|
49
|
+
@path = path
|
|
50
|
+
@options = StoreOptions.build(options)
|
|
51
|
+
@pragma_sql = @options.pragma_sql.freeze
|
|
52
|
+
@db = nil
|
|
53
|
+
@schema_checked = false
|
|
51
54
|
@last_cleanup_at = nil
|
|
52
55
|
end
|
|
53
56
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
def migrate!
|
|
58
|
+
raise SchemaError, 'close the Store before calling migrate!' if connected?
|
|
59
|
+
|
|
60
|
+
with_database { |db| migrate_database!(db) }
|
|
61
|
+
@schema_checked = true
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
alias ensure_database! migrate!
|
|
66
|
+
|
|
67
|
+
def prepare_dashboard!
|
|
68
|
+
raise SchemaError, 'close the Store before calling prepare_dashboard!' if connected?
|
|
69
|
+
|
|
70
|
+
with_database { |db| Schema.prepare_dashboard!(db) }
|
|
61
71
|
@schema_checked = true
|
|
72
|
+
self
|
|
62
73
|
end
|
|
63
74
|
|
|
64
|
-
def
|
|
75
|
+
def schema_version
|
|
76
|
+
ensure_connection
|
|
77
|
+
@db.get_first_value(SQL::USER_VERSION).to_i
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def enqueue(class_name, args = EMPTY_ARGS, run_at = nil, options: EMPTY_OPTIONS)
|
|
65
81
|
ensure_connection
|
|
66
82
|
now = realtime_now
|
|
67
|
-
@enqueue_stmt.execute(class_name,
|
|
83
|
+
@enqueue_stmt.execute(class_name, dump_args(args), dump_options(options), now, run_at || now)
|
|
68
84
|
@db.last_insert_row_id
|
|
69
85
|
end
|
|
70
86
|
|
|
71
87
|
def fetch(worker_id)
|
|
72
88
|
ensure_connection
|
|
89
|
+
token = generate_claim_token
|
|
73
90
|
now = realtime_now
|
|
74
91
|
|
|
75
|
-
row = transaction
|
|
92
|
+
row = transaction do
|
|
93
|
+
with_statement(@fetch_stmt) { |statement| statement.execute(worker_id, now, token, now).first }
|
|
94
|
+
end
|
|
76
95
|
return unless row
|
|
77
96
|
|
|
78
97
|
maybe_cleanup
|
|
79
|
-
|
|
98
|
+
job_from_row(row, token)
|
|
80
99
|
end
|
|
81
100
|
|
|
82
|
-
def
|
|
101
|
+
def mark_started!(job_id, claim_token:, started_at: realtime_now)
|
|
83
102
|
ensure_connection
|
|
84
|
-
@
|
|
103
|
+
@mark_started_stmt.execute(started_at, job_id, claim_token)
|
|
104
|
+
@db.changes.positive?
|
|
85
105
|
end
|
|
86
106
|
|
|
87
|
-
def
|
|
107
|
+
def complete(job_id, claim_token:, finished_at: realtime_now, duration_ms: nil)
|
|
88
108
|
ensure_connection
|
|
89
|
-
@
|
|
109
|
+
@complete_stmt.execute(finished_at, duration_ms, job_id, claim_token)
|
|
110
|
+
@db.changes.positive?
|
|
90
111
|
end
|
|
91
112
|
|
|
92
|
-
def
|
|
113
|
+
def fail(job_id, claim_token:, error_class: nil, error_message: nil, finished_at: realtime_now, duration_ms: nil)
|
|
114
|
+
ensure_connection
|
|
115
|
+
@fail_stmt.execute(
|
|
116
|
+
finished_at,
|
|
117
|
+
duration_ms,
|
|
118
|
+
error_class&.to_s,
|
|
119
|
+
truncate_message(error_message),
|
|
120
|
+
job_id,
|
|
121
|
+
claim_token
|
|
122
|
+
)
|
|
123
|
+
@db.changes.positive?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def retry_or_fail(
|
|
127
|
+
job_id,
|
|
128
|
+
claim_token:,
|
|
129
|
+
error_class: nil,
|
|
130
|
+
error_message: nil,
|
|
131
|
+
fallback_options: nil,
|
|
132
|
+
finished_at: realtime_now,
|
|
133
|
+
duration_ms: nil
|
|
134
|
+
)
|
|
93
135
|
ensure_connection
|
|
94
136
|
|
|
95
137
|
transaction do
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
:retried
|
|
103
|
-
else
|
|
104
|
-
@fail_stmt.execute(job_id)
|
|
105
|
-
:failed
|
|
106
|
-
end
|
|
138
|
+
stored_options = stored_options_for(job_id, claim_token)
|
|
139
|
+
next unless lease_alive?(job_id, claim_token)
|
|
140
|
+
|
|
141
|
+
policy = retry_policy(stored_options, fallback_options)
|
|
142
|
+
policy_retries?(policy) ? retry_job!(job_id, claim_token, policy, error_class, error_message) :
|
|
143
|
+
fail_job!(job_id, claim_token, error_class, error_message, finished_at, duration_ms)
|
|
107
144
|
end
|
|
108
145
|
end
|
|
109
146
|
|
|
@@ -113,66 +150,172 @@ module Async
|
|
|
113
150
|
@db.changes
|
|
114
151
|
end
|
|
115
152
|
|
|
153
|
+
def next_pending_run_at
|
|
154
|
+
ensure_connection
|
|
155
|
+
with_statement(@next_pending_stmt) { |statement| statement.execute.first&.first }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def data_version
|
|
159
|
+
ensure_connection
|
|
160
|
+
@db.get_first_value(SQL::DATA_VERSION).to_i
|
|
161
|
+
end
|
|
162
|
+
|
|
116
163
|
def close
|
|
117
|
-
return unless
|
|
164
|
+
return unless connected?
|
|
118
165
|
|
|
119
166
|
finalize_statements
|
|
120
|
-
@db.execute(
|
|
167
|
+
@db.execute(SQL::OPTIMIZE) rescue nil
|
|
121
168
|
@db.close
|
|
122
169
|
@db = nil
|
|
170
|
+
@schema_checked = false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def connected?
|
|
176
|
+
@db && !@db.closed?
|
|
123
177
|
end
|
|
124
178
|
|
|
125
|
-
def
|
|
126
|
-
|
|
179
|
+
def open_database
|
|
180
|
+
require_sqlite3
|
|
181
|
+
db = SQLite3::Database.new(@path)
|
|
182
|
+
configure_database(db)
|
|
183
|
+
db
|
|
184
|
+
rescue StandardError
|
|
185
|
+
db&.close unless db&.closed?
|
|
186
|
+
raise
|
|
127
187
|
end
|
|
128
188
|
|
|
129
|
-
|
|
189
|
+
def with_database
|
|
190
|
+
db = open_database
|
|
191
|
+
yield db
|
|
192
|
+
ensure
|
|
193
|
+
db&.close unless db&.closed?
|
|
194
|
+
end
|
|
130
195
|
|
|
131
196
|
def require_sqlite3
|
|
132
197
|
require 'sqlite3'
|
|
133
198
|
rescue LoadError
|
|
134
199
|
raise LoadError,
|
|
135
|
-
|
|
136
|
-
|
|
200
|
+
"sqlite3 gem is required for Async::Background::Queue. " \
|
|
201
|
+
"Add `gem 'sqlite3', '~> 2.0'` to your Gemfile."
|
|
137
202
|
end
|
|
138
203
|
|
|
139
204
|
def ensure_connection
|
|
140
205
|
return if @db && !@db.closed?
|
|
141
206
|
|
|
142
|
-
require_sqlite3
|
|
143
207
|
finalize_statements
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
@db.execute_batch(SCHEMA)
|
|
149
|
-
@db.execute("ALTER TABLE jobs ADD COLUMN options TEXT") rescue nil
|
|
150
|
-
@schema_checked = true
|
|
151
|
-
end
|
|
152
|
-
|
|
208
|
+
db = open_database
|
|
209
|
+
migrate_database!(db) unless @schema_checked
|
|
210
|
+
@schema_checked = true
|
|
211
|
+
@db = db
|
|
153
212
|
prepare_statements
|
|
154
213
|
@last_cleanup_at = monotonic_now
|
|
214
|
+
rescue StandardError
|
|
215
|
+
db&.close unless db&.equal?(@db) || db&.closed?
|
|
216
|
+
reset_connection!
|
|
217
|
+
raise
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def reset_connection!
|
|
221
|
+
@schema_checked = false
|
|
222
|
+
@db&.close unless @db&.closed?
|
|
223
|
+
@db = nil
|
|
155
224
|
end
|
|
156
225
|
|
|
157
226
|
def configure_database(db)
|
|
158
|
-
db.execute(
|
|
227
|
+
db.execute(SQL.busy_timeout(5000))
|
|
159
228
|
db.execute_batch(@pragma_sql)
|
|
160
229
|
end
|
|
161
230
|
|
|
231
|
+
def migrate_database!(db)
|
|
232
|
+
Schema.migrate!(db)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def job_from_row(row, claim_token)
|
|
236
|
+
{
|
|
237
|
+
id: row[0],
|
|
238
|
+
class_name: row[1],
|
|
239
|
+
args: JSON.parse(row[2]),
|
|
240
|
+
options: load_options(row[3]),
|
|
241
|
+
claim_token: claim_token
|
|
242
|
+
}
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def stored_options_for(job_id, claim_token)
|
|
246
|
+
with_statement(@retry_state_stmt) do |statement|
|
|
247
|
+
load_options(statement.execute(job_id, claim_token).first&.first)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def retry_policy(stored_options, fallback_options)
|
|
252
|
+
return Job::Options.new(**stored_options) unless stored_options.empty?
|
|
253
|
+
|
|
254
|
+
normalize_options(fallback_options)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def policy_retries?(policy)
|
|
258
|
+
policy&.retry? && policy.next_attempt <= policy.retry
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def retry_job!(job_id, claim_token, policy, error_class, error_message)
|
|
262
|
+
advanced = policy.with_attempt(policy.next_attempt)
|
|
263
|
+
@retry_stmt.execute(
|
|
264
|
+
realtime_now + advanced.next_retry_delay(advanced.attempt),
|
|
265
|
+
dump_options(advanced.to_h.compact),
|
|
266
|
+
error_class&.to_s,
|
|
267
|
+
truncate_message(error_message),
|
|
268
|
+
job_id,
|
|
269
|
+
claim_token
|
|
270
|
+
)
|
|
271
|
+
@db.changes.positive? ? :retried : nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def fail_job!(job_id, claim_token, error_class, error_message, finished_at, duration_ms)
|
|
275
|
+
@fail_stmt.execute(
|
|
276
|
+
finished_at,
|
|
277
|
+
duration_ms,
|
|
278
|
+
error_class&.to_s,
|
|
279
|
+
truncate_message(error_message),
|
|
280
|
+
job_id,
|
|
281
|
+
claim_token
|
|
282
|
+
)
|
|
283
|
+
@db.changes.positive? ? :failed : nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def generate_claim_token = SecureRandom.hex(16)
|
|
287
|
+
|
|
288
|
+
def truncate_message(message)
|
|
289
|
+
return if message.nil?
|
|
290
|
+
|
|
291
|
+
string = message.to_s
|
|
292
|
+
string.length > ERROR_MESSAGE_MAX_LEN ? string.byteslice(0, ERROR_MESSAGE_MAX_LEN) : string
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def lease_alive?(job_id, claim_token)
|
|
296
|
+
with_statement(@lease_check_stmt) do |statement|
|
|
297
|
+
!statement.execute(job_id, claim_token).first.nil?
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
162
301
|
def transaction
|
|
163
|
-
@db.execute(
|
|
302
|
+
@db.execute(SQL::BEGIN_IMMEDIATE)
|
|
164
303
|
result = yield
|
|
165
|
-
@db.execute(
|
|
304
|
+
@db.execute(SQL::COMMIT)
|
|
166
305
|
result
|
|
167
|
-
rescue
|
|
168
|
-
@db.execute(
|
|
306
|
+
rescue StandardError
|
|
307
|
+
@db.execute(SQL::ROLLBACK) rescue nil
|
|
169
308
|
raise
|
|
170
309
|
end
|
|
171
310
|
|
|
172
|
-
def
|
|
173
|
-
yield
|
|
311
|
+
def with_statement(statement)
|
|
312
|
+
yield statement
|
|
174
313
|
ensure
|
|
175
|
-
|
|
314
|
+
statement.reset! rescue nil
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def dump_args(args)
|
|
318
|
+
args.equal?(EMPTY_ARGS) ? EMPTY_ARGS_JSON : JSON.generate(args)
|
|
176
319
|
end
|
|
177
320
|
|
|
178
321
|
def dump_options(options)
|
|
@@ -191,43 +334,48 @@ module Async
|
|
|
191
334
|
end
|
|
192
335
|
|
|
193
336
|
def prepare_statements
|
|
194
|
-
@enqueue_stmt = @db.prepare(
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
@
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
207
|
-
RETURNING id, class_name, args, options
|
|
208
|
-
SQL
|
|
209
|
-
|
|
210
|
-
@complete_stmt = @db.prepare("UPDATE jobs SET status = 'done', locked_by = NULL, locked_at = NULL WHERE id = ?")
|
|
211
|
-
@fail_stmt = @db.prepare("UPDATE jobs SET status = 'failed', locked_by = NULL, locked_at = NULL WHERE id = ?")
|
|
212
|
-
@retry_state_stmt = @db.prepare("SELECT options FROM jobs WHERE id = ?")
|
|
213
|
-
@retry_stmt = @db.prepare(
|
|
214
|
-
"UPDATE jobs SET status = 'pending', locked_by = NULL, locked_at = NULL, run_at = ?, options = ? WHERE id = ?"
|
|
215
|
-
)
|
|
216
|
-
@requeue_stmt = @db.prepare(
|
|
217
|
-
"UPDATE jobs SET status = 'pending', locked_by = NULL, locked_at = NULL " \
|
|
218
|
-
"WHERE status = 'running' AND locked_by = ?"
|
|
219
|
-
)
|
|
220
|
-
@cleanup_stmt = @db.prepare("DELETE FROM jobs WHERE status = 'done' AND created_at < ?")
|
|
337
|
+
@enqueue_stmt = @db.prepare(SQL::INSERT_JOB)
|
|
338
|
+
@fetch_stmt = @db.prepare(SQL::FETCH_NEXT_JOB)
|
|
339
|
+
@mark_started_stmt = @db.prepare(SQL::MARK_STARTED)
|
|
340
|
+
@complete_stmt = @db.prepare(SQL::COMPLETE_JOB)
|
|
341
|
+
@fail_stmt = @db.prepare(SQL::FAIL_JOB)
|
|
342
|
+
@retry_state_stmt = @db.prepare(SQL::RETRY_STATE)
|
|
343
|
+
@lease_check_stmt = @db.prepare(SQL::LEASE_ALIVE)
|
|
344
|
+
@retry_stmt = @db.prepare(SQL::RETRY_JOB)
|
|
345
|
+
@requeue_stmt = @db.prepare(SQL::RECOVER_WORKER)
|
|
346
|
+
@cleanup_done_stmt = @db.prepare(SQL::CLEANUP_DONE)
|
|
347
|
+
@cleanup_failed_stmt = @db.prepare(SQL::CLEANUP_FAILED)
|
|
348
|
+
@next_pending_stmt = @db.prepare(SQL::NEXT_PENDING_RUN_AT)
|
|
221
349
|
end
|
|
222
350
|
|
|
223
351
|
def finalize_statements
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
352
|
+
statements.each { |statement| statement&.close rescue nil }
|
|
353
|
+
clear_statements
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def statements
|
|
357
|
+
[
|
|
358
|
+
@enqueue_stmt,
|
|
359
|
+
@fetch_stmt,
|
|
360
|
+
@mark_started_stmt,
|
|
361
|
+
@complete_stmt,
|
|
362
|
+
@fail_stmt,
|
|
363
|
+
@retry_state_stmt,
|
|
364
|
+
@lease_check_stmt,
|
|
365
|
+
@retry_stmt,
|
|
366
|
+
@requeue_stmt,
|
|
367
|
+
@cleanup_done_stmt,
|
|
368
|
+
@cleanup_failed_stmt,
|
|
369
|
+
@next_pending_stmt
|
|
370
|
+
]
|
|
371
|
+
end
|
|
228
372
|
|
|
229
|
-
|
|
230
|
-
@
|
|
373
|
+
def clear_statements
|
|
374
|
+
@enqueue_stmt = @fetch_stmt = @mark_started_stmt = nil
|
|
375
|
+
@complete_stmt = @fail_stmt = @retry_state_stmt = @lease_check_stmt = nil
|
|
376
|
+
@retry_stmt = @requeue_stmt = nil
|
|
377
|
+
@cleanup_done_stmt = @cleanup_failed_stmt = nil
|
|
378
|
+
@next_pending_stmt = nil
|
|
231
379
|
end
|
|
232
380
|
|
|
233
381
|
def maybe_cleanup
|
|
@@ -235,8 +383,13 @@ module Async
|
|
|
235
383
|
return if (now - @last_cleanup_at) < CLEANUP_INTERVAL
|
|
236
384
|
|
|
237
385
|
@last_cleanup_at = now
|
|
238
|
-
|
|
239
|
-
|
|
386
|
+
cleanup_finished_jobs(realtime_now)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def cleanup_finished_jobs(now)
|
|
390
|
+
@cleanup_done_stmt.execute(now - CLEANUP_AGE)
|
|
391
|
+
@cleanup_failed_stmt.execute(now - FAILED_RETENTION_AGE)
|
|
392
|
+
@db.execute(SQL::INCREMENTAL_VACUUM) if @db.changes > 100
|
|
240
393
|
end
|
|
241
394
|
end
|
|
242
395
|
end
|