async-background 1.1.0 → 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 +14 -0
- 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 +5 -1
- data/lib/async/background/queue/socket_notifier.rb +14 -4
- data/lib/async/background/queue/socket_waker.rb +27 -7
- data/lib/async/background/queue/sql.rb +31 -31
- data/lib/async/background/queue/store.rb +23 -7
- data/lib/async/background/runner/lifecycle.rb +131 -0
- data/lib/async/background/runner/queue_execution.rb +72 -52
- data/lib/async/background/runner/schedule.rb +4 -4
- data/lib/async/background/runner.rb +57 -126
- data/lib/async/background/runtime/notification.rb +2 -1
- data/lib/async/background/runtime/semaphore.rb +15 -11
- data/lib/async/background/runtime/task.rb +18 -17
- data/lib/async/background/runtime/task_group.rb +2 -2
- data/lib/async/background/runtime.rb +56 -53
- data/lib/async/background/scheduler.rb +4 -4
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +26 -19
- 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 +15 -15
- data/lib/async/background/web/cursor.rb +14 -7
- data/lib/async/background/web/metrics_reader.rb +4 -6
- data/lib/async/background/web/response.rb +1 -1
- data/lib/async/background/web/serializer.rb +2 -2
- data/lib/async/background/web/snapshot.rb +5 -5
- data/lib/async/background/web/sql.rb +7 -7
- data/lib/async/background/web/stream.rb +52 -32
- data/lib/async/background/web.rb +1 -1
- metadata +5 -1
|
@@ -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
|
|
@@ -57,37 +57,37 @@ module Async
|
|
|
57
57
|
[:queue_path, ->(value, _) { !value.nil? && !value.to_s.empty? }, 'queue_path must be set'],
|
|
58
58
|
[:auth, ->(value, _) { !value.nil? }, 'auth must be configured (gem ships no permissive default)'],
|
|
59
59
|
[:auth, ->(value, _) { value.respond_to?(:call) },
|
|
60
|
-
|
|
60
|
+
'auth must respond to #call(env) and return truthy on success'],
|
|
61
61
|
[:list_limit, ->(value, _) { value.is_a?(Integer) && value.between?(1, MAX_LIST_LIMIT) },
|
|
62
|
-
|
|
62
|
+
"list_limit must be an Integer between 1 and #{MAX_LIST_LIMIT}"],
|
|
63
63
|
[:counts_cache_ttl, ->(value, _) { value.is_a?(Numeric) && value >= 0 },
|
|
64
|
-
|
|
64
|
+
'counts_cache_ttl must be a non-negative Numeric'],
|
|
65
65
|
[:poll_interval_ms, ->(value, _) { value.is_a?(Integer) && value >= 200 },
|
|
66
|
-
|
|
66
|
+
'poll_interval_ms must be an Integer >= 200'],
|
|
67
67
|
[:transport, ->(value, _) { TRANSPORTS.include?(value) },
|
|
68
|
-
|
|
68
|
+
"transport must be one of #{TRANSPORTS.inspect}"],
|
|
69
69
|
[:stream_poll_seconds, ->(value, _) { value.is_a?(Numeric) && value >= 0.1 },
|
|
70
|
-
|
|
70
|
+
'stream_poll_seconds must be a Numeric >= 0.1'],
|
|
71
71
|
[:stream_heartbeat_seconds, ->(value, _) { value.is_a?(Numeric) && value >= 5 },
|
|
72
|
-
|
|
72
|
+
'stream_heartbeat_seconds must be a Numeric >= 5'],
|
|
73
73
|
[:stream_retry_ms, ->(value, _) { value.is_a?(Integer) && value >= 500 },
|
|
74
|
-
|
|
74
|
+
'stream_retry_ms must be an Integer >= 500'],
|
|
75
75
|
[:redact_args, ->(value, config) { !config.expose_args || value.nil? || value.respond_to?(:call) },
|
|
76
|
-
|
|
77
|
-
[:total_workers,
|
|
76
|
+
'redact_args must respond to #call(args)'],
|
|
77
|
+
[:total_workers, lambda { |value, config|
|
|
78
78
|
!config.metrics_enabled? || (value.is_a?(Integer) && value.positive?)
|
|
79
79
|
}, 'metrics_path requires total_workers to be a positive Integer'],
|
|
80
80
|
[:mount_path, ->(value, _) { value.is_a?(String) }, 'mount_path must be a String'],
|
|
81
81
|
[:mount_path, ->(value, _) { value.empty? || value.start_with?('/') },
|
|
82
|
-
|
|
82
|
+
'mount_path must start with "/" or be empty'],
|
|
83
83
|
[:mount_path, ->(value, _) { value.empty? || !value.end_with?('/') },
|
|
84
|
-
|
|
84
|
+
'mount_path must not end with "/"'],
|
|
85
85
|
[:mount_path, ->(value, _) { value.empty? || !value.match?(/[[:cntrl:]]/) },
|
|
86
|
-
|
|
86
|
+
'mount_path must not contain control characters'],
|
|
87
87
|
[:mount_path, ->(value, _) { value.empty? || !value.match?(/\s/) },
|
|
88
|
-
|
|
88
|
+
'mount_path must not contain whitespace'],
|
|
89
89
|
[:logger, ->(value, _) { value.nil? || (value.respond_to?(:warn) && value.respond_to?(:error)) },
|
|
90
|
-
|
|
90
|
+
'logger must respond to #warn and #error']
|
|
91
91
|
].freeze
|
|
92
92
|
|
|
93
93
|
def validate!
|
|
@@ -23,24 +23,31 @@ module Async
|
|
|
23
23
|
timestamp, id = decode(value)
|
|
24
24
|
return unless timestamp
|
|
25
25
|
|
|
26
|
-
{key => timestamp, :id => id}
|
|
26
|
+
{ key => timestamp, :id => id }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def decode(value)
|
|
30
30
|
return if value.nil? || value.to_s.empty?
|
|
31
31
|
|
|
32
|
-
|
|
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)
|
|
33
39
|
raise RequestError, 'invalid cursor' if timestamp_raw.nil? || id_raw.nil? || extra
|
|
34
40
|
|
|
35
41
|
timestamp = Float(timestamp_raw)
|
|
36
42
|
id = Integer(id_raw)
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
validate_fields!(timestamp, id)
|
|
39
44
|
[timestamp, id]
|
|
40
|
-
rescue ArgumentError, TypeError
|
|
41
|
-
raise RequestError, 'invalid cursor'
|
|
42
45
|
end
|
|
43
|
-
|
|
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!
|
|
44
51
|
end
|
|
45
52
|
end
|
|
46
53
|
end
|
|
@@ -23,12 +23,10 @@ module Async
|
|
|
23
23
|
LATEST_FIELD = :last_run_at
|
|
24
24
|
LATEST_COMPANION = :last_duration_ms
|
|
25
25
|
|
|
26
|
-
EMPTY_TOTALS =
|
|
27
|
-
|
|
28
|
-
.merge(LATEST_FIELD => 0, LATEST_COMPANION => nil)
|
|
29
|
-
).freeze
|
|
26
|
+
EMPTY_TOTALS = SUMMED_FIELDS.to_h { |field| [field, 0] }
|
|
27
|
+
.merge(LATEST_FIELD => 0, LATEST_COMPANION => nil).freeze
|
|
30
28
|
|
|
31
|
-
UNAVAILABLE = {available: false, workers: EMPTY_WORKERS, totals: EMPTY_TOTALS}.freeze
|
|
29
|
+
UNAVAILABLE = { available: false, workers: EMPTY_WORKERS, totals: EMPTY_TOTALS }.freeze
|
|
32
30
|
|
|
33
31
|
def initialize(path:, total_workers:, ttl: DEFAULT_TTL)
|
|
34
32
|
@path = path
|
|
@@ -60,7 +58,7 @@ module Async
|
|
|
60
58
|
return unavailable unless Metrics.available? && File.file?(@path)
|
|
61
59
|
|
|
62
60
|
workers = Metrics.read_all(total_workers: @total_workers, path: @path)
|
|
63
|
-
{available: true, workers: workers, totals: aggregate(workers)}
|
|
61
|
+
{ available: true, workers: workers, totals: aggregate(workers) }
|
|
64
62
|
rescue StandardError
|
|
65
63
|
unavailable
|
|
66
64
|
end
|
|
@@ -49,7 +49,7 @@ module Async
|
|
|
49
49
|
ERROR_BODY = ERRORS.to_h { |name, (_, code)| [name, JSON.generate(error: code).freeze] }.freeze
|
|
50
50
|
|
|
51
51
|
def headers(content_type, cache_control, security = BASE_SECURITY_HEADERS)
|
|
52
|
-
{'content-type' => content_type, 'cache-control' => cache_control}.merge(security)
|
|
52
|
+
{ 'content-type' => content_type, 'cache-control' => cache_control }.merge(security)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def no_store_headers(content_type) = headers(content_type, NO_STORE)
|
|
@@ -55,7 +55,7 @@ module Async
|
|
|
55
55
|
pending: %i[created_at run_at].freeze
|
|
56
56
|
}.freeze
|
|
57
57
|
|
|
58
|
-
CURSOR_KEY = {done: :finished_at, failed: :finished_at, pending: :run_at}.freeze
|
|
58
|
+
CURSOR_KEY = { done: :finished_at, failed: :finished_at, pending: :run_at }.freeze
|
|
59
59
|
|
|
60
60
|
def item(kind, row)
|
|
61
61
|
args, args_count = args_for(row[:args_raw])
|
|
@@ -76,7 +76,7 @@ module Async
|
|
|
76
76
|
last = items.last
|
|
77
77
|
cursor = last && Cursor.encode(last[CURSOR_KEY.fetch(kind)], last[:id])
|
|
78
78
|
|
|
79
|
-
{items: items, next_cursor: cursor}
|
|
79
|
+
{ items: items, next_cursor: cursor }
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def args_for(raw)
|
|
@@ -13,11 +13,11 @@ 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
|
|
16
|
+
RAW_COLUMNS = { 'args' => :args_raw, 'options' => :options_raw }.freeze
|
|
17
17
|
|
|
18
18
|
ROW_KEYS = SQL::EXTRA_COLUMNS.to_h do |status, extra|
|
|
19
19
|
columns = SQL::BASE_COLUMNS + extra
|
|
20
|
-
|
|
20
|
+
[status, columns.map { |column| RAW_COLUMNS.fetch(column) { column.to_sym } }.freeze]
|
|
21
21
|
end.freeze
|
|
22
22
|
|
|
23
23
|
PAGING = {
|
|
@@ -109,8 +109,8 @@ module Async
|
|
|
109
109
|
SQLite3::Database.new(database_uri, uri: true)
|
|
110
110
|
rescue LoadError
|
|
111
111
|
raise
|
|
112
|
-
rescue StandardError =>
|
|
113
|
-
raise UnavailableError, "cannot open queue database: #{
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
raise UnavailableError, "cannot open queue database: #{e.message}"
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def database_uri
|
|
@@ -192,7 +192,7 @@ module Async
|
|
|
192
192
|
require 'sqlite3'
|
|
193
193
|
rescue LoadError
|
|
194
194
|
raise LoadError,
|
|
195
|
-
|
|
195
|
+
'sqlite3 gem is required for Async::Background::Web. ' \
|
|
196
196
|
"Add `gem 'sqlite3', '~> 2.0'` to your Gemfile."
|
|
197
197
|
end
|
|
198
198
|
end
|
|
@@ -4,11 +4,11 @@ module Async
|
|
|
4
4
|
module Background
|
|
5
5
|
module Web
|
|
6
6
|
module SQL
|
|
7
|
-
BEGIN_READ_TRANSACTION = 'BEGIN'
|
|
8
|
-
COMMIT = 'COMMIT'
|
|
9
|
-
ROLLBACK = 'ROLLBACK'
|
|
10
|
-
QUERY_ONLY = 'PRAGMA query_only = ON'
|
|
11
|
-
BUSY_TIMEOUT = 'PRAGMA busy_timeout = 2000'
|
|
7
|
+
BEGIN_READ_TRANSACTION = 'BEGIN'
|
|
8
|
+
COMMIT = 'COMMIT'
|
|
9
|
+
ROLLBACK = 'ROLLBACK'
|
|
10
|
+
QUERY_ONLY = 'PRAGMA query_only = ON'
|
|
11
|
+
BUSY_TIMEOUT = 'PRAGMA busy_timeout = 2000'
|
|
12
12
|
BASE_COLUMNS = %w[id class_name args options].freeze
|
|
13
13
|
|
|
14
14
|
STATUS_PREDICATE = {
|
|
@@ -35,8 +35,8 @@ module Async
|
|
|
35
35
|
failed: ['finished_at', :desc]
|
|
36
36
|
}.freeze
|
|
37
37
|
|
|
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
|
|
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
|
|
40
40
|
|
|
41
41
|
def self.columns_for(status)
|
|
42
42
|
(BASE_COLUMNS + EXTRA_COLUMNS.fetch(status)).join(', ')
|
|
@@ -26,41 +26,13 @@ module Async
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
yield frame
|
|
29
|
-
|
|
30
|
-
unavailable_announced = false
|
|
31
|
-
|
|
32
|
-
loop do
|
|
33
|
-
sleep_for_poll
|
|
34
|
-
|
|
35
|
-
begin
|
|
36
|
-
new_version = @hub.current_version
|
|
37
|
-
|
|
38
|
-
if new_version != version
|
|
39
|
-
version = new_version
|
|
40
|
-
yield @hub.frame_for(version)
|
|
41
|
-
last_yield = monotonic_now
|
|
42
|
-
elsif (monotonic_now - last_yield) >= @heartbeat_seconds
|
|
43
|
-
yield EventHub::HEARTBEAT_FRAME
|
|
44
|
-
last_yield = monotonic_now
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
unavailable_announced = false
|
|
48
|
-
rescue ClosedError
|
|
49
|
-
break
|
|
50
|
-
rescue UnavailableError
|
|
51
|
-
unless unavailable_announced
|
|
52
|
-
yield EventHub::UNAVAILABLE_FRAME
|
|
53
|
-
unavailable_announced = true
|
|
54
|
-
last_yield = monotonic_now
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
29
|
+
stream_updates(version, monotonic_now) { |event| yield event }
|
|
58
30
|
rescue Errno::EPIPE, Errno::ECONNRESET, IOError
|
|
59
31
|
nil
|
|
60
|
-
rescue StandardError =>
|
|
32
|
+
rescue StandardError => e
|
|
61
33
|
@logger&.error(
|
|
62
|
-
|
|
63
|
-
"#{
|
|
34
|
+
'[async-background-web] SSE stream terminated: ' \
|
|
35
|
+
"#{e.class}: #{e.message}"
|
|
64
36
|
)
|
|
65
37
|
nil
|
|
66
38
|
end
|
|
@@ -76,6 +48,54 @@ module Async
|
|
|
76
48
|
def sleep_for_poll
|
|
77
49
|
sleep(@poll_seconds)
|
|
78
50
|
end
|
|
51
|
+
|
|
52
|
+
def stream_updates(version, last_yield)
|
|
53
|
+
unavailable_announced = false
|
|
54
|
+
loop do
|
|
55
|
+
sleep_for_poll
|
|
56
|
+
state = next_state(version, last_yield, unavailable_announced) { |event| yield event }
|
|
57
|
+
break unless state
|
|
58
|
+
|
|
59
|
+
version, last_yield, unavailable_announced = state
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def next_state(version, last_yield, unavailable_announced)
|
|
64
|
+
current_version = @hub.current_version
|
|
65
|
+
return updated_state(current_version) { |event| yield event } if current_version != version
|
|
66
|
+
return heartbeat_state(version) { |event| yield event } if heartbeat_due?(last_yield)
|
|
67
|
+
|
|
68
|
+
[version, last_yield, false]
|
|
69
|
+
rescue ClosedError
|
|
70
|
+
nil
|
|
71
|
+
rescue UnavailableError
|
|
72
|
+
announce_unavailable(version, last_yield, unavailable_announced) { |event| yield event }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def updated_state(version)
|
|
76
|
+
yield @hub.frame_for(version)
|
|
77
|
+
[version, monotonic_now, false]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def heartbeat_state(version)
|
|
81
|
+
yield EventHub::HEARTBEAT_FRAME
|
|
82
|
+
[version, monotonic_now, false]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def heartbeat_due?(last_yield)
|
|
86
|
+
(monotonic_now - last_yield) >= @heartbeat_seconds
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def announce_unavailable(version, last_yield, announced)
|
|
90
|
+
return [version, last_yield, true] if announced
|
|
91
|
+
|
|
92
|
+
yield_unavailable { |event| yield event }
|
|
93
|
+
[version, monotonic_now, true]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def yield_unavailable
|
|
97
|
+
yield EventHub::UNAVAILABLE_FRAME
|
|
98
|
+
end
|
|
79
99
|
end
|
|
80
100
|
end
|
|
81
101
|
end
|
data/lib/async/background/web.rb
CHANGED
|
@@ -37,7 +37,7 @@ module Async
|
|
|
37
37
|
|
|
38
38
|
def configuration
|
|
39
39
|
@configuration or raise NotConfiguredError,
|
|
40
|
-
|
|
40
|
+
'Async::Background::Web is not configured. Call Async::Background::Web.configure.'
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def reset!
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-background
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Hajdarov
|
|
@@ -157,6 +157,7 @@ files:
|
|
|
157
157
|
- lib/async/background/queue/sql.rb
|
|
158
158
|
- lib/async/background/queue/store.rb
|
|
159
159
|
- lib/async/background/runner.rb
|
|
160
|
+
- lib/async/background/runner/lifecycle.rb
|
|
160
161
|
- lib/async/background/runner/queue_execution.rb
|
|
161
162
|
- lib/async/background/runner/schedule.rb
|
|
162
163
|
- lib/async/background/runtime.rb
|
|
@@ -169,6 +170,9 @@ files:
|
|
|
169
170
|
- lib/async/background/web.rb
|
|
170
171
|
- lib/async/background/web/app.rb
|
|
171
172
|
- lib/async/background/web/assets.rb
|
|
173
|
+
- lib/async/background/web/assets/app.css
|
|
174
|
+
- lib/async/background/web/assets/app.js
|
|
175
|
+
- lib/async/background/web/assets/index.html
|
|
172
176
|
- lib/async/background/web/auth.rb
|
|
173
177
|
- lib/async/background/web/configuration.rb
|
|
174
178
|
- lib/async/background/web/cursor.rb
|