async-background 1.0.2 → 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 +67 -0
- data/README.md +36 -15
- data/async-background.gemspec +13 -7
- data/lib/async/background/queue/schema.rb +14 -14
- data/lib/async/background/queue/socket_waker.rb +115 -52
- data/lib/async/background/queue/store.rb +21 -38
- 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 +128 -38
- 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
|
@@ -8,34 +8,23 @@ module Async
|
|
|
8
8
|
module Cursor
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
end
|
|
11
|
+
def encode(timestamp, id)
|
|
12
|
+
return if timestamp.nil? || id.nil?
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
encode(run_at, id)
|
|
14
|
+
Base64.urlsafe_encode64("#{Float(timestamp)}:#{Integer(id)}", padding: false)
|
|
17
15
|
end
|
|
18
16
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{finished_at: timestamp, id: id}
|
|
24
|
-
end
|
|
17
|
+
def encode_finished(finished_at, id) = encode(finished_at, id)
|
|
18
|
+
def encode_pending(run_at, id) = encode(run_at, id)
|
|
19
|
+
def decode_finished(value) = decode_as(:finished_at, value)
|
|
20
|
+
def decode_pending(value) = decode_as(:run_at, value)
|
|
25
21
|
|
|
26
|
-
def
|
|
22
|
+
def decode_as(key, value)
|
|
27
23
|
timestamp, id = decode(value)
|
|
28
24
|
return unless timestamp
|
|
29
25
|
|
|
30
|
-
{
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def encode(timestamp, id)
|
|
34
|
-
return if timestamp.nil? || id.nil?
|
|
35
|
-
|
|
36
|
-
Base64.urlsafe_encode64("#{Float(timestamp)}:#{Integer(id)}", padding: false)
|
|
26
|
+
{key => timestamp, :id => id}
|
|
37
27
|
end
|
|
38
|
-
private_class_method :encode
|
|
39
28
|
|
|
40
29
|
def decode(value)
|
|
41
30
|
return if value.nil? || value.to_s.empty?
|
|
@@ -51,7 +40,7 @@ module Async
|
|
|
51
40
|
rescue ArgumentError, TypeError
|
|
52
41
|
raise RequestError, 'invalid cursor'
|
|
53
42
|
end
|
|
54
|
-
private_class_method :decode
|
|
43
|
+
private_class_method :decode, :decode_as
|
|
55
44
|
end
|
|
56
45
|
end
|
|
57
46
|
end
|
|
@@ -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
|