async-background 1.0.1 → 1.1.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 +153 -0
- data/README.md +36 -15
- data/async-background.gemspec +13 -7
- data/lib/async/background/metrics.rb +1 -3
- data/lib/async/background/queue/schema.rb +14 -14
- data/lib/async/background/queue/socket_notifier.rb +44 -13
- data/lib/async/background/queue/socket_waker.rb +115 -49
- data/lib/async/background/queue/sql.rb +2 -1
- data/lib/async/background/queue/store.rb +98 -75
- data/lib/async/background/runner/queue_execution.rb +32 -8
- data/lib/async/background/runner/schedule.rb +16 -13
- data/lib/async/background/runner.rb +132 -52
- data/lib/async/background/runtime/notification.rb +41 -0
- data/lib/async/background/runtime/semaphore.rb +147 -0
- data/lib/async/background/runtime/task.rb +145 -0
- data/lib/async/background/runtime/task_group.rb +81 -0
- data/lib/async/background/runtime.rb +243 -0
- data/lib/async/background/scheduler.rb +145 -0
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +22 -26
- data/lib/async/background/web/configuration.rb +40 -93
- data/lib/async/background/web/cursor.rb +10 -21
- data/lib/async/background/web/event_hub.rb +12 -7
- data/lib/async/background/web/metrics_reader.rb +29 -35
- 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 +32 -78
- data/lib/async/background/web/sql.rb +58 -64
- data/lib/async/background.rb +1 -2
- metadata +26 -22
|
@@ -20,14 +20,13 @@ module Async
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def current_version
|
|
23
|
-
|
|
23
|
+
with_open {}
|
|
24
24
|
@snapshot.data_version
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def frame_for(version)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return @cached_frame if @cached_version == version && @cached_frame
|
|
28
|
+
with_open do
|
|
29
|
+
next @cached_frame if @cached_version == version && @cached_frame
|
|
31
30
|
|
|
32
31
|
refresh_frame_locked!
|
|
33
32
|
@cached_frame
|
|
@@ -35,9 +34,7 @@ module Async
|
|
|
35
34
|
end
|
|
36
35
|
|
|
37
36
|
def initial_frame
|
|
38
|
-
|
|
39
|
-
raise ClosedError, 'event hub is closed' if @closed
|
|
40
|
-
|
|
37
|
+
with_open do
|
|
41
38
|
refresh_frame_locked!
|
|
42
39
|
[@cached_version, @cached_frame]
|
|
43
40
|
end
|
|
@@ -58,6 +55,14 @@ module Async
|
|
|
58
55
|
|
|
59
56
|
private
|
|
60
57
|
|
|
58
|
+
def with_open
|
|
59
|
+
@mutex.synchronize do
|
|
60
|
+
raise ClosedError, 'event hub is closed' if @closed
|
|
61
|
+
|
|
62
|
+
yield
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
61
66
|
def refresh_frame_locked!
|
|
62
67
|
overview = @snapshot.overview(force: true)
|
|
63
68
|
metrics = @metrics_reader&.aggregated
|
|
@@ -11,16 +11,24 @@ module Async
|
|
|
11
11
|
|
|
12
12
|
DEFAULT_TTL = 1.0
|
|
13
13
|
EMPTY_WORKERS = [].freeze
|
|
14
|
-
|
|
15
|
-
total_runs
|
|
16
|
-
total_successes
|
|
17
|
-
total_failures
|
|
18
|
-
total_timeouts
|
|
19
|
-
total_skips
|
|
20
|
-
active_jobs
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
SUMMED_FIELDS = %i[
|
|
15
|
+
total_runs
|
|
16
|
+
total_successes
|
|
17
|
+
total_failures
|
|
18
|
+
total_timeouts
|
|
19
|
+
total_skips
|
|
20
|
+
active_jobs
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
LATEST_FIELD = :last_run_at
|
|
24
|
+
LATEST_COMPANION = :last_duration_ms
|
|
25
|
+
|
|
26
|
+
EMPTY_TOTALS = (
|
|
27
|
+
SUMMED_FIELDS.to_h { |field| [field, 0] }
|
|
28
|
+
.merge(LATEST_FIELD => 0, LATEST_COMPANION => nil)
|
|
29
|
+
).freeze
|
|
30
|
+
|
|
31
|
+
UNAVAILABLE = {available: false, workers: EMPTY_WORKERS, totals: EMPTY_TOTALS}.freeze
|
|
24
32
|
|
|
25
33
|
def initialize(path:, total_workers:, ttl: DEFAULT_TTL)
|
|
26
34
|
@path = path
|
|
@@ -57,37 +65,23 @@ module Async
|
|
|
57
65
|
unavailable
|
|
58
66
|
end
|
|
59
67
|
|
|
60
|
-
def unavailable
|
|
61
|
-
{available: false, workers: EMPTY_WORKERS, totals: EMPTY_TOTALS}
|
|
62
|
-
end
|
|
68
|
+
def unavailable = UNAVAILABLE
|
|
63
69
|
|
|
64
70
|
def aggregate(workers)
|
|
65
|
-
totals =
|
|
66
|
-
|
|
67
|
-
total_successes: 0,
|
|
68
|
-
total_failures: 0,
|
|
69
|
-
total_timeouts: 0,
|
|
70
|
-
total_skips: 0,
|
|
71
|
-
active_jobs: 0,
|
|
72
|
-
last_run_at: 0,
|
|
73
|
-
last_duration_ms: nil
|
|
74
|
-
}
|
|
71
|
+
totals = EMPTY_TOTALS.dup
|
|
72
|
+
most_recent = nil
|
|
75
73
|
|
|
76
74
|
workers.each do |worker|
|
|
77
|
-
totals[
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
totals[
|
|
83
|
-
|
|
84
|
-
last_run_at = worker[:last_run_at].to_i
|
|
85
|
-
next unless last_run_at > totals[:last_run_at]
|
|
86
|
-
|
|
87
|
-
totals[:last_run_at] = last_run_at
|
|
88
|
-
totals[:last_duration_ms] = worker[:last_duration_ms]
|
|
75
|
+
SUMMED_FIELDS.each { |field| totals[field] += worker[field].to_i }
|
|
76
|
+
|
|
77
|
+
last_run_at = worker[LATEST_FIELD].to_i
|
|
78
|
+
next unless last_run_at > totals[LATEST_FIELD]
|
|
79
|
+
|
|
80
|
+
totals[LATEST_FIELD] = last_run_at
|
|
81
|
+
most_recent = worker
|
|
89
82
|
end
|
|
90
83
|
|
|
84
|
+
totals[LATEST_COMPANION] = most_recent[LATEST_COMPANION] if most_recent
|
|
91
85
|
totals.freeze
|
|
92
86
|
end
|
|
93
87
|
end
|
|
@@ -13,8 +13,12 @@ module Async
|
|
|
13
13
|
TEXT_TYPE = 'text/plain; charset=utf-8'
|
|
14
14
|
JAVASCRIPT_TYPE = 'application/javascript; charset=utf-8'
|
|
15
15
|
CSS_TYPE = 'text/css; charset=utf-8'
|
|
16
|
+
EVENT_STREAM_TYPE = 'text/event-stream; charset=utf-8'
|
|
17
|
+
|
|
16
18
|
NO_STORE = 'no-store'
|
|
17
19
|
ASSET_CACHE = 'public, max-age=31536000, immutable'
|
|
20
|
+
SSE_CACHE = 'no-cache, no-transform'
|
|
21
|
+
|
|
18
22
|
BASE_SECURITY_HEADERS = {
|
|
19
23
|
'x-content-type-options' => 'nosniff',
|
|
20
24
|
'referrer-policy' => 'no-referrer',
|
|
@@ -34,73 +38,57 @@ module Async
|
|
|
34
38
|
"form-action 'none'"
|
|
35
39
|
).freeze
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
ERRORS = {
|
|
42
|
+
unauthorized: [401, 'unauthorized'],
|
|
43
|
+
not_found: [404, 'not_found'],
|
|
44
|
+
bad_request: [400, 'invalid_request'],
|
|
45
|
+
unavailable: [503, 'service_unavailable'],
|
|
46
|
+
internal_error: [500, 'internal_error']
|
|
47
|
+
}.freeze
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
[200, sse_headers, body]
|
|
46
|
-
end
|
|
49
|
+
ERROR_BODY = ERRORS.to_h { |name, (_, code)| [name, JSON.generate(error: code).freeze] }.freeze
|
|
47
50
|
|
|
48
|
-
def
|
|
49
|
-
|
|
51
|
+
def headers(content_type, cache_control, security = BASE_SECURITY_HEADERS)
|
|
52
|
+
{'content-type' => content_type, 'cache-control' => cache_control}.merge(security)
|
|
50
53
|
end
|
|
51
54
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
def no_store_headers(content_type) = headers(content_type, NO_STORE)
|
|
56
|
+
def html_headers = headers(HTML_TYPE, NO_STORE, HTML_SECURITY_HEADERS)
|
|
57
|
+
def asset_headers(content_type) = headers(content_type, ASSET_CACHE)
|
|
55
58
|
|
|
56
|
-
def
|
|
57
|
-
|
|
59
|
+
def sse_headers
|
|
60
|
+
{
|
|
61
|
+
'content-type' => EVENT_STREAM_TYPE,
|
|
62
|
+
'cache-control' => SSE_CACHE,
|
|
63
|
+
'x-accel-buffering' => 'no'
|
|
64
|
+
}.merge(BASE_SECURITY_HEADERS)
|
|
58
65
|
end
|
|
59
66
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
def sse(body) = [200, sse_headers, body]
|
|
68
|
+
def html(body) = [200, html_headers, [body]]
|
|
69
|
+
def javascript(body) = [200, asset_headers(JAVASCRIPT_TYPE), [body]]
|
|
70
|
+
def stylesheet(body) = [200, asset_headers(CSS_TYPE), [body]]
|
|
63
71
|
|
|
64
|
-
def
|
|
65
|
-
[
|
|
72
|
+
def json(payload, status: 200)
|
|
73
|
+
[status, no_store_headers(JSON_TYPE), [JSON.generate(payload)]]
|
|
66
74
|
end
|
|
67
75
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
def unauthorized = error_response(:unauthorized)
|
|
77
|
+
def not_found = error_response(:not_found)
|
|
78
|
+
def unavailable = error_response(:unavailable)
|
|
79
|
+
def internal_error = error_response(:internal_error)
|
|
71
80
|
|
|
72
81
|
def bad_request(message = nil)
|
|
73
|
-
|
|
74
|
-
[400, no_store_headers(JSON_TYPE), [body]]
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def unavailable
|
|
78
|
-
[503, no_store_headers(JSON_TYPE), [UNAVAILABLE_BODY]]
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def internal_error
|
|
82
|
-
[500, no_store_headers(JSON_TYPE), [INTERNAL_ERROR_BODY]]
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def no_store_headers(content_type)
|
|
86
|
-
{'content-type' => content_type, 'cache-control' => NO_STORE}.merge(BASE_SECURITY_HEADERS)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def html_headers
|
|
90
|
-
{'content-type' => HTML_TYPE, 'cache-control' => NO_STORE}.merge(HTML_SECURITY_HEADERS)
|
|
91
|
-
end
|
|
82
|
+
return error_response(:bad_request) if message.nil?
|
|
92
83
|
|
|
93
|
-
|
|
94
|
-
{'content-type' => content_type, 'cache-control' => ASSET_CACHE}.merge(BASE_SECURITY_HEADERS)
|
|
84
|
+
error_response(:bad_request, JSON.generate(error: 'invalid_request', message: message))
|
|
95
85
|
end
|
|
96
86
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
'cache-control' => 'no-cache, no-transform',
|
|
101
|
-
'x-accel-buffering' => 'no'
|
|
102
|
-
}.merge(BASE_SECURITY_HEADERS)
|
|
87
|
+
def error_response(name, body = ERROR_BODY.fetch(name))
|
|
88
|
+
status = ERRORS.fetch(name).first
|
|
89
|
+
[status, no_store_headers(JSON_TYPE), [body]]
|
|
103
90
|
end
|
|
91
|
+
private_class_method :error_response
|
|
104
92
|
end
|
|
105
93
|
end
|
|
106
94
|
end
|
|
@@ -26,97 +26,57 @@ module Async
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def executing(rows)
|
|
29
|
-
rows.map { |row|
|
|
29
|
+
rows.map { |row| item(:executing, row) }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def claimed(rows)
|
|
33
|
-
rows.map { |row|
|
|
33
|
+
rows.map { |row| item(:claimed, row) }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def done(rows)
|
|
37
|
-
page(
|
|
37
|
+
page(:done, rows)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def failed(rows)
|
|
41
|
-
page(
|
|
41
|
+
page(:failed, rows)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def pending(rows)
|
|
45
|
-
page(
|
|
45
|
+
page(:pending, rows)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
EXTRA_FIELDS = {
|
|
51
|
+
executing: %i[started_at locked_by locked_at].freeze,
|
|
52
|
+
claimed: %i[locked_at locked_by].freeze,
|
|
53
|
+
done: %i[finished_at duration_ms].freeze,
|
|
54
|
+
failed: %i[finished_at duration_ms last_error_class last_error_message].freeze,
|
|
55
|
+
pending: %i[created_at run_at].freeze
|
|
56
|
+
}.freeze
|
|
53
57
|
|
|
54
|
-
|
|
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
|
|
58
|
+
CURSOR_KEY = {done: :finished_at, failed: :finished_at, pending: :run_at}.freeze
|
|
67
59
|
|
|
68
|
-
def
|
|
60
|
+
def item(kind, row)
|
|
69
61
|
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
62
|
|
|
81
|
-
|
|
82
|
-
args, args_count = args_for(row[:args_raw])
|
|
83
|
-
{
|
|
63
|
+
fields = {
|
|
84
64
|
id: row[:id],
|
|
85
65
|
class_name: row[:class_name],
|
|
86
66
|
args: args,
|
|
87
67
|
args_count: args_count,
|
|
88
|
-
options: parse_options(row[:options_raw])
|
|
89
|
-
finished_at: row[:finished_at],
|
|
90
|
-
duration_ms: row[:duration_ms]
|
|
68
|
+
options: parse_options(row[:options_raw])
|
|
91
69
|
}
|
|
70
|
+
EXTRA_FIELDS.fetch(kind).each { |name| fields[name] = row[name] }
|
|
71
|
+
fields
|
|
92
72
|
end
|
|
93
73
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
74
|
+
def page(kind, rows)
|
|
75
|
+
items = rows.map { |row| item(kind, row) }
|
|
76
|
+
last = items.last
|
|
77
|
+
cursor = last && Cursor.encode(last[CURSOR_KEY.fetch(kind)], last[:id])
|
|
108
78
|
|
|
109
|
-
|
|
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
|
-
}
|
|
79
|
+
{items: items, next_cursor: cursor}
|
|
120
80
|
end
|
|
121
81
|
|
|
122
82
|
def args_for(raw)
|
|
@@ -13,6 +13,20 @@ module Async
|
|
|
13
13
|
include Clock
|
|
14
14
|
|
|
15
15
|
CacheEntry = Data.define(:value, :created_at)
|
|
16
|
+
RAW_COLUMNS = {'args' => :args_raw, 'options' => :options_raw}.freeze
|
|
17
|
+
|
|
18
|
+
ROW_KEYS = SQL::EXTRA_COLUMNS.to_h do |status, extra|
|
|
19
|
+
columns = SQL::BASE_COLUMNS + extra
|
|
20
|
+
[status, columns.map { |column| RAW_COLUMNS.fetch(column, column.to_sym) }.freeze]
|
|
21
|
+
end.freeze
|
|
22
|
+
|
|
23
|
+
PAGING = {
|
|
24
|
+
executing: [SQL::EXECUTING, nil, nil].freeze,
|
|
25
|
+
claimed: [SQL::CLAIMED, nil, nil].freeze,
|
|
26
|
+
done: [SQL::DONE, SQL::DONE_AFTER, %i[finished_at id].freeze].freeze,
|
|
27
|
+
failed: [SQL::FAILED, SQL::FAILED_AFTER, %i[finished_at id].freeze].freeze,
|
|
28
|
+
pending: [SQL::PENDING, SQL::PENDING_AFTER, %i[run_at id].freeze].freeze
|
|
29
|
+
}.freeze
|
|
16
30
|
|
|
17
31
|
def initialize(path:, counts_cache_ttl:)
|
|
18
32
|
@path = path
|
|
@@ -65,26 +79,23 @@ module Async
|
|
|
65
79
|
end
|
|
66
80
|
|
|
67
81
|
def executing(limit:)
|
|
68
|
-
|
|
82
|
+
list(:executing, limit: limit)
|
|
69
83
|
end
|
|
70
84
|
|
|
71
85
|
def claimed(limit:)
|
|
72
|
-
|
|
86
|
+
list(:claimed, limit: limit)
|
|
73
87
|
end
|
|
74
88
|
|
|
75
89
|
def recent_done(limit:, cursor: nil)
|
|
76
|
-
|
|
77
|
-
read_rows(sql, binds).map { |row| done_row(row) }
|
|
90
|
+
list(:done, limit: limit, cursor: cursor)
|
|
78
91
|
end
|
|
79
92
|
|
|
80
93
|
def recent_failed(limit:, cursor: nil)
|
|
81
|
-
|
|
82
|
-
read_rows(sql, binds).map { |row| failed_row(row) }
|
|
94
|
+
list(:failed, limit: limit, cursor: cursor)
|
|
83
95
|
end
|
|
84
96
|
|
|
85
97
|
def pending(limit:, cursor: nil)
|
|
86
|
-
|
|
87
|
-
read_rows(sql, binds).map { |row| pending_row(row) }
|
|
98
|
+
list(:pending, limit: limit, cursor: cursor)
|
|
88
99
|
end
|
|
89
100
|
|
|
90
101
|
private
|
|
@@ -103,7 +114,7 @@ module Async
|
|
|
103
114
|
end
|
|
104
115
|
|
|
105
116
|
def database_uri
|
|
106
|
-
path = URI::
|
|
117
|
+
path = URI::RFC2396_PARSER.escape(File.expand_path(@path)).gsub('?', '%3F')
|
|
107
118
|
"file:#{path}?mode=ro"
|
|
108
119
|
end
|
|
109
120
|
|
|
@@ -153,10 +164,10 @@ module Async
|
|
|
153
164
|
{
|
|
154
165
|
counts: {
|
|
155
166
|
executing: db.get_first_value(SQL::OVERVIEW_EXECUTING).to_i,
|
|
156
|
-
claimed:
|
|
157
|
-
pending:
|
|
158
|
-
done:
|
|
159
|
-
failed:
|
|
167
|
+
claimed: db.get_first_value(SQL::OVERVIEW_CLAIMED).to_i,
|
|
168
|
+
pending: db.get_first_value(SQL::OVERVIEW_PENDING).to_i,
|
|
169
|
+
done: db.get_first_value(SQL::OVERVIEW_DONE).to_i,
|
|
170
|
+
failed: db.get_first_value(SQL::OVERVIEW_FAILED).to_i
|
|
160
171
|
}.freeze,
|
|
161
172
|
next_pending_run_at: db.get_first_value(SQL::OVERVIEW_NEXT_PENDING),
|
|
162
173
|
data_version: db.get_first_value(Queue::SQL::DATA_VERSION).to_i,
|
|
@@ -164,74 +175,17 @@ module Async
|
|
|
164
175
|
}
|
|
165
176
|
end
|
|
166
177
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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]]
|
|
178
|
+
def list(status, limit:, cursor: nil)
|
|
179
|
+
sql, binds = page_query(status, limit, cursor)
|
|
180
|
+
keys = ROW_KEYS.fetch(status)
|
|
181
|
+
read_rows(sql, binds).map { |row| keys.zip(row).to_h }
|
|
177
182
|
end
|
|
178
183
|
|
|
179
|
-
def
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
184
|
+
def page_query(status, limit, cursor)
|
|
185
|
+
first_sql, seek_sql, cursor_keys = PAGING.fetch(status)
|
|
186
|
+
return [first_sql, [limit]] unless cursor
|
|
190
187
|
|
|
191
|
-
|
|
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
|
-
}
|
|
188
|
+
[seek_sql, cursor_keys.map { |key| cursor.fetch(key) } << limit]
|
|
235
189
|
end
|
|
236
190
|
|
|
237
191
|
def require_sqlite3
|
|
@@ -9,79 +9,73 @@ module Async
|
|
|
9
9
|
ROLLBACK = 'ROLLBACK'.freeze
|
|
10
10
|
QUERY_ONLY = 'PRAGMA query_only = ON'.freeze
|
|
11
11
|
BUSY_TIMEOUT = 'PRAGMA busy_timeout = 2000'.freeze
|
|
12
|
+
BASE_COLUMNS = %w[id class_name args options].freeze
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
STATUS_PREDICATE = {
|
|
15
|
+
executing: "status = 'running' AND started_at IS NOT NULL",
|
|
16
|
+
claimed: "status = 'running' AND started_at IS NULL",
|
|
17
|
+
pending: "status = 'pending'",
|
|
18
|
+
done: "status = 'done'",
|
|
19
|
+
failed: "status = 'failed'"
|
|
20
|
+
}.freeze
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
EXTRA_COLUMNS = {
|
|
23
|
+
executing: %w[started_at locked_by locked_at],
|
|
24
|
+
claimed: %w[locked_at locked_by],
|
|
25
|
+
pending: %w[created_at run_at],
|
|
26
|
+
done: %w[finished_at duration_ms],
|
|
27
|
+
failed: %w[finished_at duration_ms last_error_class last_error_message]
|
|
28
|
+
}.freeze
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
KEYSET = {
|
|
31
|
+
executing: ['started_at', :asc],
|
|
32
|
+
claimed: ['locked_at', :asc],
|
|
33
|
+
pending: ['run_at', :asc],
|
|
34
|
+
done: ['finished_at', :desc],
|
|
35
|
+
failed: ['finished_at', :desc]
|
|
36
|
+
}.freeze
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
FROM jobs
|
|
39
|
-
WHERE status = 'done'
|
|
40
|
-
ORDER BY finished_at DESC, id DESC
|
|
41
|
-
LIMIT ?
|
|
42
|
-
SQL
|
|
38
|
+
ORDER_SQL = {asc: 'ORDER BY %<column>s, id', desc: 'ORDER BY %<column>s DESC, id DESC'}.freeze
|
|
39
|
+
SEEK_SQL = {asc: '(%<column>s, id) > (?, ?)', desc: '(%<column>s, id) < (?, ?)'}.freeze
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
WHERE status = 'done' AND (finished_at, id) < (?, ?)
|
|
48
|
-
ORDER BY finished_at DESC, id DESC
|
|
49
|
-
LIMIT ?
|
|
50
|
-
SQL
|
|
41
|
+
def self.columns_for(status)
|
|
42
|
+
(BASE_COLUMNS + EXTRA_COLUMNS.fetch(status)).join(', ')
|
|
43
|
+
end
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
SELECT
|
|
54
|
-
|
|
55
|
-
FROM jobs
|
|
56
|
-
WHERE status = 'failed'
|
|
57
|
-
ORDER BY finished_at DESC, id DESC
|
|
58
|
-
LIMIT ?
|
|
59
|
-
SQL
|
|
45
|
+
def self.count_query(status)
|
|
46
|
+
"SELECT COUNT(*) FROM jobs WHERE #{STATUS_PREDICATE.fetch(status)}".freeze
|
|
47
|
+
end
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
WHERE status = 'failed' AND (finished_at, id) < (?, ?)
|
|
66
|
-
ORDER BY finished_at DESC, id DESC
|
|
67
|
-
LIMIT ?
|
|
68
|
-
SQL
|
|
49
|
+
def self.list_query(status, seek: false)
|
|
50
|
+
column, direction = KEYSET.fetch(status)
|
|
51
|
+
predicate = STATUS_PREDICATE.fetch(status)
|
|
52
|
+
predicate += " AND #{format(SEEK_SQL.fetch(direction), column: column)}" if seek
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
54
|
+
<<~SQL.freeze
|
|
55
|
+
SELECT #{columns_for(status)}
|
|
56
|
+
FROM jobs
|
|
57
|
+
WHERE #{predicate}
|
|
58
|
+
#{format(ORDER_SQL.fetch(direction), column: column)}
|
|
59
|
+
LIMIT ?
|
|
60
|
+
SQL
|
|
61
|
+
end
|
|
77
62
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
63
|
+
OVERVIEW_EXECUTING = count_query(:executing)
|
|
64
|
+
OVERVIEW_CLAIMED = count_query(:claimed)
|
|
65
|
+
OVERVIEW_PENDING = count_query(:pending)
|
|
66
|
+
OVERVIEW_DONE = count_query(:done)
|
|
67
|
+
OVERVIEW_FAILED = count_query(:failed)
|
|
68
|
+
OVERVIEW_NEXT_PENDING = "SELECT MIN(run_at) FROM jobs WHERE #{STATUS_PREDICATE.fetch(:pending)}".freeze
|
|
69
|
+
|
|
70
|
+
EXECUTING = list_query(:executing)
|
|
71
|
+
CLAIMED = list_query(:claimed)
|
|
72
|
+
DONE = list_query(:done)
|
|
73
|
+
FAILED = list_query(:failed)
|
|
74
|
+
PENDING = list_query(:pending)
|
|
75
|
+
|
|
76
|
+
DONE_AFTER = list_query(:done, seek: true)
|
|
77
|
+
FAILED_AFTER = list_query(:failed, seek: true)
|
|
78
|
+
PENDING_AFTER = list_query(:pending, seek: true)
|
|
85
79
|
end
|
|
86
80
|
end
|
|
87
81
|
end
|