async-background 1.0.2 → 1.1.1
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 +81 -0
- data/README.md +36 -15
- data/async-background.gemspec +13 -7
- data/lib/async/background/job.rb +75 -28
- data/lib/async/background/metrics.rb +41 -33
- data/lib/async/background/min_heap.rb +16 -12
- data/lib/async/background/queue/client.rb +11 -8
- data/lib/async/background/queue/options.rb +3 -3
- data/lib/async/background/queue/schema.rb +19 -15
- data/lib/async/background/queue/socket_notifier.rb +14 -4
- data/lib/async/background/queue/socket_waker.rb +139 -56
- data/lib/async/background/queue/sql.rb +31 -31
- data/lib/async/background/queue/store.rb +44 -45
- data/lib/async/background/runner/lifecycle.rb +131 -0
- data/lib/async/background/runner/queue_execution.rb +101 -57
- data/lib/async/background/runner/schedule.rb +20 -17
- data/lib/async/background/runner.rb +98 -77
- data/lib/async/background/runtime/notification.rb +42 -0
- data/lib/async/background/runtime/semaphore.rb +151 -0
- data/lib/async/background/runtime/task.rb +146 -0
- data/lib/async/background/runtime/task_group.rb +81 -0
- data/lib/async/background/runtime.rb +246 -0
- data/lib/async/background/scheduler.rb +145 -0
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +43 -40
- data/lib/async/background/web/assets/app.css +174 -0
- data/lib/async/background/web/assets/app.js +486 -0
- data/lib/async/background/web/assets/index.html +29 -0
- data/lib/async/background/web/assets.rb +4 -697
- data/lib/async/background/web/auth.rb +3 -3
- data/lib/async/background/web/configuration.rb +40 -93
- data/lib/async/background/web/cursor.rb +22 -26
- data/lib/async/background/web/event_hub.rb +12 -7
- data/lib/async/background/web/metrics_reader.rb +28 -36
- 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 +35 -81
- data/lib/async/background/web/sql.rb +63 -69
- data/lib/async/background/web/stream.rb +52 -32
- data/lib/async/background/web.rb +1 -1
- data/lib/async/background.rb +1 -2
- metadata +30 -22
|
@@ -11,10 +11,10 @@ module Async
|
|
|
11
11
|
|
|
12
12
|
def authorized?(env)
|
|
13
13
|
!!@callable.call(env)
|
|
14
|
-
rescue StandardError =>
|
|
14
|
+
rescue StandardError => e
|
|
15
15
|
@logger&.warn(
|
|
16
|
-
|
|
17
|
-
"#{
|
|
16
|
+
'[async-background-web] auth callable raised: ' \
|
|
17
|
+
"#{e.class}: #{e.message}"
|
|
18
18
|
)
|
|
19
19
|
false
|
|
20
20
|
end
|
|
@@ -53,18 +53,47 @@ module Async
|
|
|
53
53
|
@logger = nil
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
RULES = [
|
|
57
|
+
[:queue_path, ->(value, _) { !value.nil? && !value.to_s.empty? }, 'queue_path must be set'],
|
|
58
|
+
[:auth, ->(value, _) { !value.nil? }, 'auth must be configured (gem ships no permissive default)'],
|
|
59
|
+
[:auth, ->(value, _) { value.respond_to?(:call) },
|
|
60
|
+
'auth must respond to #call(env) and return truthy on success'],
|
|
61
|
+
[:list_limit, ->(value, _) { value.is_a?(Integer) && value.between?(1, MAX_LIST_LIMIT) },
|
|
62
|
+
"list_limit must be an Integer between 1 and #{MAX_LIST_LIMIT}"],
|
|
63
|
+
[:counts_cache_ttl, ->(value, _) { value.is_a?(Numeric) && value >= 0 },
|
|
64
|
+
'counts_cache_ttl must be a non-negative Numeric'],
|
|
65
|
+
[:poll_interval_ms, ->(value, _) { value.is_a?(Integer) && value >= 200 },
|
|
66
|
+
'poll_interval_ms must be an Integer >= 200'],
|
|
67
|
+
[:transport, ->(value, _) { TRANSPORTS.include?(value) },
|
|
68
|
+
"transport must be one of #{TRANSPORTS.inspect}"],
|
|
69
|
+
[:stream_poll_seconds, ->(value, _) { value.is_a?(Numeric) && value >= 0.1 },
|
|
70
|
+
'stream_poll_seconds must be a Numeric >= 0.1'],
|
|
71
|
+
[:stream_heartbeat_seconds, ->(value, _) { value.is_a?(Numeric) && value >= 5 },
|
|
72
|
+
'stream_heartbeat_seconds must be a Numeric >= 5'],
|
|
73
|
+
[:stream_retry_ms, ->(value, _) { value.is_a?(Integer) && value >= 500 },
|
|
74
|
+
'stream_retry_ms must be an Integer >= 500'],
|
|
75
|
+
[:redact_args, ->(value, config) { !config.expose_args || value.nil? || value.respond_to?(:call) },
|
|
76
|
+
'redact_args must respond to #call(args)'],
|
|
77
|
+
[:total_workers, lambda { |value, config|
|
|
78
|
+
!config.metrics_enabled? || (value.is_a?(Integer) && value.positive?)
|
|
79
|
+
}, 'metrics_path requires total_workers to be a positive Integer'],
|
|
80
|
+
[:mount_path, ->(value, _) { value.is_a?(String) }, 'mount_path must be a String'],
|
|
81
|
+
[:mount_path, ->(value, _) { value.empty? || value.start_with?('/') },
|
|
82
|
+
'mount_path must start with "/" or be empty'],
|
|
83
|
+
[:mount_path, ->(value, _) { value.empty? || !value.end_with?('/') },
|
|
84
|
+
'mount_path must not end with "/"'],
|
|
85
|
+
[:mount_path, ->(value, _) { value.empty? || !value.match?(/[[:cntrl:]]/) },
|
|
86
|
+
'mount_path must not contain control characters'],
|
|
87
|
+
[:mount_path, ->(value, _) { value.empty? || !value.match?(/\s/) },
|
|
88
|
+
'mount_path must not contain whitespace'],
|
|
89
|
+
[:logger, ->(value, _) { value.nil? || (value.respond_to?(:warn) && value.respond_to?(:error)) },
|
|
90
|
+
'logger must respond to #warn and #error']
|
|
91
|
+
].freeze
|
|
92
|
+
|
|
56
93
|
def validate!
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
validate_cache_ttl!
|
|
61
|
-
validate_poll_interval!
|
|
62
|
-
validate_transport!
|
|
63
|
-
validate_stream!
|
|
64
|
-
validate_redactor!
|
|
65
|
-
validate_metrics!
|
|
66
|
-
validate_mount_path!
|
|
67
|
-
validate_logger!
|
|
94
|
+
RULES.each do |attribute, valid, message|
|
|
95
|
+
raise ConfigurationError, message unless valid.call(public_send(attribute), self)
|
|
96
|
+
end
|
|
68
97
|
self
|
|
69
98
|
end
|
|
70
99
|
|
|
@@ -84,88 +113,6 @@ module Async
|
|
|
84
113
|
def metrics_enabled?
|
|
85
114
|
!metrics_path.nil?
|
|
86
115
|
end
|
|
87
|
-
|
|
88
|
-
private
|
|
89
|
-
|
|
90
|
-
def validate_queue_path!
|
|
91
|
-
raise ConfigurationError, 'queue_path must be set' if queue_path.nil? || queue_path.to_s.empty?
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def validate_auth!
|
|
95
|
-
raise ConfigurationError, 'auth must be configured (gem ships no permissive default)' if auth.nil?
|
|
96
|
-
|
|
97
|
-
return if auth.respond_to?(:call)
|
|
98
|
-
|
|
99
|
-
raise ConfigurationError, 'auth must respond to #call(env) and return truthy on success'
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def validate_list_limit!
|
|
103
|
-
return if list_limit.is_a?(Integer) && list_limit.between?(1, MAX_LIST_LIMIT)
|
|
104
|
-
|
|
105
|
-
raise ConfigurationError, "list_limit must be an Integer between 1 and #{MAX_LIST_LIMIT}"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def validate_cache_ttl!
|
|
109
|
-
return if counts_cache_ttl.is_a?(Numeric) && counts_cache_ttl >= 0
|
|
110
|
-
|
|
111
|
-
raise ConfigurationError, 'counts_cache_ttl must be a non-negative Numeric'
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def validate_poll_interval!
|
|
115
|
-
return if poll_interval_ms.is_a?(Integer) && poll_interval_ms >= 200
|
|
116
|
-
|
|
117
|
-
raise ConfigurationError, 'poll_interval_ms must be an Integer >= 200'
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def validate_transport!
|
|
121
|
-
return if TRANSPORTS.include?(transport)
|
|
122
|
-
|
|
123
|
-
raise ConfigurationError, "transport must be one of #{TRANSPORTS.inspect}"
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def validate_stream!
|
|
127
|
-
unless stream_poll_seconds.is_a?(Numeric) && stream_poll_seconds >= 0.1
|
|
128
|
-
raise ConfigurationError, 'stream_poll_seconds must be a Numeric >= 0.1'
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
unless stream_heartbeat_seconds.is_a?(Numeric) && stream_heartbeat_seconds >= 5
|
|
132
|
-
raise ConfigurationError, 'stream_heartbeat_seconds must be a Numeric >= 5'
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
return if stream_retry_ms.is_a?(Integer) && stream_retry_ms >= 500
|
|
136
|
-
|
|
137
|
-
raise ConfigurationError, 'stream_retry_ms must be an Integer >= 500'
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def validate_redactor!
|
|
141
|
-
return unless expose_args && redact_args && !redact_args.respond_to?(:call)
|
|
142
|
-
|
|
143
|
-
raise ConfigurationError, 'redact_args must respond to #call(args)'
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def validate_metrics!
|
|
147
|
-
return unless metrics_enabled?
|
|
148
|
-
return if total_workers.is_a?(Integer) && total_workers.positive?
|
|
149
|
-
|
|
150
|
-
raise ConfigurationError, 'metrics_path requires total_workers to be a positive Integer'
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def validate_mount_path!
|
|
154
|
-
raise ConfigurationError, 'mount_path must be a String' unless mount_path.is_a?(String)
|
|
155
|
-
return if mount_path.empty?
|
|
156
|
-
|
|
157
|
-
raise ConfigurationError, 'mount_path must start with "/" or be empty' unless mount_path.start_with?('/')
|
|
158
|
-
raise ConfigurationError, 'mount_path must not end with "/"' if mount_path.end_with?('/')
|
|
159
|
-
raise ConfigurationError, 'mount_path must not contain control characters' if mount_path.match?(/[[:cntrl:]]/)
|
|
160
|
-
raise ConfigurationError, 'mount_path must not contain whitespace' if mount_path.match?(/\s/)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def validate_logger!
|
|
164
|
-
return if logger.nil?
|
|
165
|
-
return if logger.respond_to?(:warn) && logger.respond_to?(:error)
|
|
166
|
-
|
|
167
|
-
raise ConfigurationError, 'logger must respond to #warn and #error'
|
|
168
|
-
end
|
|
169
116
|
end
|
|
170
117
|
end
|
|
171
118
|
end
|
|
@@ -8,50 +8,46 @@ 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
|
-
|
|
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)
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def decode_pending(value)
|
|
22
|
+
def decode_as(key, value)
|
|
27
23
|
timestamp, id = decode(value)
|
|
28
24
|
return unless timestamp
|
|
29
25
|
|
|
30
|
-
{
|
|
26
|
+
{ key => timestamp, :id => id }
|
|
31
27
|
end
|
|
32
28
|
|
|
33
|
-
def encode(timestamp, id)
|
|
34
|
-
return if timestamp.nil? || id.nil?
|
|
35
|
-
|
|
36
|
-
Base64.urlsafe_encode64("#{Float(timestamp)}:#{Integer(id)}", padding: false)
|
|
37
|
-
end
|
|
38
|
-
private_class_method :encode
|
|
39
|
-
|
|
40
29
|
def decode(value)
|
|
41
30
|
return if value.nil? || value.to_s.empty?
|
|
42
31
|
|
|
43
|
-
|
|
32
|
+
decode_fields(Base64.urlsafe_decode64(value.to_s))
|
|
33
|
+
rescue ArgumentError, TypeError
|
|
34
|
+
raise RequestError, 'invalid cursor'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def decode_fields(payload)
|
|
38
|
+
timestamp_raw, id_raw, extra = payload.split(':', 3)
|
|
44
39
|
raise RequestError, 'invalid cursor' if timestamp_raw.nil? || id_raw.nil? || extra
|
|
45
40
|
|
|
46
41
|
timestamp = Float(timestamp_raw)
|
|
47
42
|
id = Integer(id_raw)
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
validate_fields!(timestamp, id)
|
|
50
44
|
[timestamp, id]
|
|
51
|
-
rescue ArgumentError, TypeError
|
|
52
|
-
raise RequestError, 'invalid cursor'
|
|
53
45
|
end
|
|
54
|
-
|
|
46
|
+
|
|
47
|
+
def validate_fields!(timestamp, id)
|
|
48
|
+
raise RequestError, 'invalid cursor' unless timestamp.finite? && id.positive?
|
|
49
|
+
end
|
|
50
|
+
private_class_method :decode, :decode_as, :decode_fields, :validate_fields!
|
|
55
51
|
end
|
|
56
52
|
end
|
|
57
53
|
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,22 @@ 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 = SUMMED_FIELDS.to_h { |field| [field, 0] }
|
|
27
|
+
.merge(LATEST_FIELD => 0, LATEST_COMPANION => nil).freeze
|
|
28
|
+
|
|
29
|
+
UNAVAILABLE = { available: false, workers: EMPTY_WORKERS, totals: EMPTY_TOTALS }.freeze
|
|
24
30
|
|
|
25
31
|
def initialize(path:, total_workers:, ttl: DEFAULT_TTL)
|
|
26
32
|
@path = path
|
|
@@ -52,42 +58,28 @@ module Async
|
|
|
52
58
|
return unavailable unless Metrics.available? && File.file?(@path)
|
|
53
59
|
|
|
54
60
|
workers = Metrics.read_all(total_workers: @total_workers, path: @path)
|
|
55
|
-
{available: true, workers: workers, totals: aggregate(workers)}
|
|
61
|
+
{ available: true, workers: workers, totals: aggregate(workers) }
|
|
56
62
|
rescue StandardError
|
|
57
63
|
unavailable
|
|
58
64
|
end
|
|
59
65
|
|
|
60
|
-
def unavailable
|
|
61
|
-
{available: false, workers: EMPTY_WORKERS, totals: EMPTY_TOTALS}
|
|
62
|
-
end
|
|
66
|
+
def unavailable = UNAVAILABLE
|
|
63
67
|
|
|
64
68
|
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
|
-
}
|
|
69
|
+
totals = EMPTY_TOTALS.dup
|
|
70
|
+
most_recent = nil
|
|
75
71
|
|
|
76
72
|
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]
|
|
73
|
+
SUMMED_FIELDS.each { |field| totals[field] += worker[field].to_i }
|
|
74
|
+
|
|
75
|
+
last_run_at = worker[LATEST_FIELD].to_i
|
|
76
|
+
next unless last_run_at > totals[LATEST_FIELD]
|
|
77
|
+
|
|
78
|
+
totals[LATEST_FIELD] = last_run_at
|
|
79
|
+
most_recent = worker
|
|
89
80
|
end
|
|
90
81
|
|
|
82
|
+
totals[LATEST_COMPANION] = most_recent[LATEST_COMPANION] if most_recent
|
|
91
83
|
totals.freeze
|
|
92
84
|
end
|
|
93
85
|
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)
|