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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9bd77f8af5601609ef43abb3b108ce225e0b20357e712023e2118a06c7e3496
|
|
4
|
+
data.tar.gz: a0e71bfec3a834da9d7a2df0fbd161ee42bbedff00ea1e90f62bee89d9c1edb3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3f7d7bd4011648a53092257c35906face2cb05bb46c24233cd9739fd37fa07b19d6476e1ad3a433891abce6146fde4c6fcbaf7fff1a73721fe2fde0dcdde1243
|
|
7
|
+
data.tar.gz: 25070911a4a50ed9142f582bffe6f385aeb236885e417409324804b1525cdea5e30889dc92d14f15c0480386a790732cf84aab54858d73c1b0177eff65af8f9c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Refactored runner lifecycle and queue execution, runtime task coordination,
|
|
8
|
+
job option normalization, metrics access, and web request handling into
|
|
9
|
+
smaller focused components.
|
|
10
|
+
- Moved dashboard HTML, CSS, and JavaScript into separate packaged assets.
|
|
11
|
+
- No public API or queue schema changes are intended.
|
|
12
|
+
|
|
13
|
+
### Validation
|
|
14
|
+
|
|
15
|
+
- Full spec suite: 431 examples, 0 failures.
|
|
16
|
+
|
|
17
|
+
## 1.1.0
|
|
18
|
+
|
|
19
|
+
The gem no longer depends on `async`. The same worker runs under any
|
|
20
|
+
`Fiber::Scheduler` the host installs — Falcon/Async, Itsi, or another
|
|
21
|
+
implementation.
|
|
22
|
+
|
|
23
|
+
### Breaking
|
|
24
|
+
|
|
25
|
+
- `Runner#run` no longer starts a reactor. It needs an active
|
|
26
|
+
`Fiber.scheduler` and raises `Runtime::SchedulerRequired` without one:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
Async { runner.run } # Falcon / Async
|
|
30
|
+
Async::Background::Scheduler.run { runner.run } # async or itsi via ENV
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- Job timeouts raise `Async::Background::Runtime::TimeoutError` instead of
|
|
34
|
+
`Async::TimeoutError`. Update rescue sites and any stored `error_class`.
|
|
35
|
+
- `Runner#drain_jobs` is bounded (`drain_timeout:` default 30s). In-flight jobs
|
|
36
|
+
that outlive it are cancelled. Pass `drain_timeout: nil` for an unbounded wait.
|
|
37
|
+
- `TaskGroup#stop_all(grace = nil)` returns whether the group drained, not a
|
|
38
|
+
count of cancelled tasks.
|
|
39
|
+
- `Async::Background::Error` is the base class for `Runtime::Error` and
|
|
40
|
+
`ConfigError`. Both are still `StandardError`.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- `Async::Background::Runtime` — `Task`, `TaskGroup`, `Semaphore`,
|
|
45
|
+
`Notification`, `with_timeout`, `native_timeouts?`, `with_error_handler`.
|
|
46
|
+
- `Async::Background::Scheduler` — optional bootstrap
|
|
47
|
+
(`require "async/background/scheduler"`) that installs `async` or
|
|
48
|
+
`itsi-scheduler` from `ASYNC_BACKGROUND_SCHEDULER`. `Scheduler.preload!`
|
|
49
|
+
loads the gem before fork. `Scheduler.run` on Itsi uses the current thread;
|
|
50
|
+
`ASYNC_BACKGROUND_SCHEDULER_THREAD=1` restores a dedicated thread.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- The saturated-queue wake-up fires from `TaskGroup#on_release`, after the job
|
|
55
|
+
has left `@jobs`. Signalling from the task's `ensure` left the listener
|
|
56
|
+
seeing a full group on any scheduler that resumes `unblock` immediately.
|
|
57
|
+
- `Runtime.with_timeout` calls `scheduler.timeout_after` when the hook exists.
|
|
58
|
+
The `::Timeout.timeout` fallback uses `Thread#raise` and can hit the wrong
|
|
59
|
+
fiber; a missing hook now warns once per scheduler class.
|
|
60
|
+
- `SocketWaker#close` stops the accept loop before the self-connect that
|
|
61
|
+
unblocks it, and hangs up tracked client sockets. A fiber parked in
|
|
62
|
+
`IO#wait_readable` is not cancellable via `Task#stop` on a scheduler without
|
|
63
|
+
`#fiber_interrupt`.
|
|
64
|
+
- `Runtime.error_handler` is scoped per runner (`on_error:` /
|
|
65
|
+
`with_error_handler`). A process-global handler was overwritten by the next
|
|
66
|
+
runner and never restored.
|
|
67
|
+
- A failure awaited via `Task#wait` is no longer also sent to the error handler.
|
|
68
|
+
- `Semaphore.new(0)` raises `ArgumentError` instead of deadlocking on acquire.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- `async` is a development dependency. Runtime gems are `console`, `fugit`
|
|
73
|
+
and `base64`.
|
|
74
|
+
- `SocketWaker#start_accept_loop` no longer needs a parent task (the
|
|
75
|
+
argument is still accepted and ignored).
|
|
76
|
+
- Shutdown closes the waker before the store, so the listener cannot be inside
|
|
77
|
+
`Store#fetch` when the connection disappears.
|
|
78
|
+
|
|
79
|
+
### Unchanged
|
|
80
|
+
|
|
81
|
+
- Dashboard, `perform_async` / `perform_in` / `perform_at`, SQLite schema,
|
|
82
|
+
and the cross-process wake protocol.
|
|
83
|
+
|
|
3
84
|
## 1.0.2
|
|
4
85
|
|
|
5
86
|
Queue maintenance bug fix plus profiler-driven work on the hot paths that
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Async::Background
|
|
2
2
|
|
|
3
|
-
A lightweight cron, interval, and job-queue scheduler for Ruby
|
|
4
|
-
[
|
|
5
|
-
[
|
|
3
|
+
A lightweight cron, interval, and job-queue scheduler for Ruby fibers.
|
|
4
|
+
Works with any `Fiber::Scheduler` — [Falcon](https://github.com/socketry/falcon) /
|
|
5
|
+
[Async](https://github.com/socketry/async), [Itsi](https://itsi.fyi), or your own.
|
|
6
6
|
|
|
7
7
|
- **Cron & interval scheduling** on a single event loop with a min-heap.
|
|
8
8
|
- **Dynamic job queue** backed by SQLite, with delayed jobs
|
|
@@ -15,11 +15,11 @@ A lightweight cron, interval, and job-queue scheduler for Ruby's
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
## Why
|
|
18
|
+
## Why fibers?
|
|
19
19
|
|
|
20
|
-
The whole gem is built around the assumption that
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
The whole gem is built around the assumption that the host schedules many
|
|
21
|
+
fibers on top of one OS thread per process — so the dashboard's SSE stream,
|
|
22
|
+
the cron scheduler, and the queue worker all share that one thread
|
|
23
23
|
cooperatively. A blocked fiber yields; a blocked thread doesn't.
|
|
24
24
|
|
|
25
25
|

|
|
@@ -30,13 +30,31 @@ open tab.
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## Fiber scheduler
|
|
34
|
+
|
|
35
|
+
`Runner#run` does not start a reactor. The host must install a
|
|
36
|
+
`Fiber.scheduler` first:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Async { runner.run } # Falcon / Async
|
|
40
|
+
|
|
41
|
+
require "async/background/scheduler"
|
|
42
|
+
Async::Background::Scheduler.run { runner.run } # ASYNC_BACKGROUND_SCHEDULER=async|itsi
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Without a scheduler it raises `Runtime::SchedulerRequired`.
|
|
46
|
+
Worker setup for Falcon and Itsi is in [Get started](docs/GET_STARTED.md).
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
33
50
|
## Requirements
|
|
34
51
|
|
|
35
52
|
| Dependency | Version | Required? |
|
|
36
53
|
| -------------------- | ---------------- | -------------------- |
|
|
37
54
|
| Ruby | `>= 3.3` | yes |
|
|
38
|
-
| `
|
|
55
|
+
| a `Fiber::Scheduler` | `async ~> 2.0`, `itsi-scheduler`, … | yes — installed by the host, not by this gem |
|
|
39
56
|
| `fugit` | `~> 1.0` | yes |
|
|
57
|
+
| `console` | `~> 1.0` | yes |
|
|
40
58
|
| `sqlite3` | `~> 2.0` | for the queue & dashboard |
|
|
41
59
|
| `async-utilization` | `>= 0.3, < 0.5` | for metrics |
|
|
42
60
|
|
|
@@ -48,16 +66,16 @@ open tab.
|
|
|
48
66
|
# Gemfile
|
|
49
67
|
gem "async-background"
|
|
50
68
|
|
|
51
|
-
gem "
|
|
52
|
-
gem "
|
|
69
|
+
gem "async", "~> 2.0" # or `itsi-scheduler`
|
|
70
|
+
gem "sqlite3", "~> 2.0" # queue / dashboard
|
|
71
|
+
gem "async-utilization", ">= 0.3", "< 0.5" # optional metrics
|
|
53
72
|
```
|
|
54
73
|
|
|
55
74
|
---
|
|
56
75
|
|
|
57
76
|
## ➡️ [Get started](docs/GET_STARTED.md)
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
delayed jobs.
|
|
78
|
+
Schedule config, Falcon or Itsi workers, Docker, queue, delayed jobs.
|
|
61
79
|
|
|
62
80
|
---
|
|
63
81
|
|
|
@@ -120,9 +138,12 @@ jobs use wall-clock time, because "every day at 3am" needs to mean 3am.
|
|
|
120
138
|
|
|
121
139
|
## How it works
|
|
122
140
|
|
|
123
|
-
A single
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
A single fiber sleeps until the next entry is due, then dispatches it under a
|
|
142
|
+
semaphore that caps concurrency. Overlapping ticks are skipped and rescheduled.
|
|
143
|
+
Every dispatched job is tracked in a `Runtime::TaskGroup`, so shutdown drains
|
|
144
|
+
in-flight work before the SQLite store and the wake-up socket are closed. The
|
|
145
|
+
drain is bounded by `drain_timeout:` (30s by default; pass `nil` for the old
|
|
146
|
+
unbounded wait) so one wedged job cannot turn SIGTERM into SIGKILL.
|
|
126
147
|
|
|
127
148
|
```
|
|
128
149
|
schedule.yml → build_heap → MinHeap<Entry> → scheduler loop → Semaphore → run_job
|
data/async-background.gemspec
CHANGED
|
@@ -8,12 +8,13 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ['Roman Hajdarov']
|
|
9
9
|
spec.email = ['romnhajdarov@gmail.com']
|
|
10
10
|
|
|
11
|
-
spec.summary = 'Lightweight heap-based cron/interval scheduler for
|
|
12
|
-
spec.description = 'A production-grade lightweight scheduler built on
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
11
|
+
spec.summary = 'Lightweight heap-based cron/interval scheduler for any Fiber scheduler.'
|
|
12
|
+
spec.description = 'A production-grade lightweight background job scheduler built on the ' \
|
|
13
|
+
'Fiber::Scheduler interface itself, not on any one implementation: it ' \
|
|
14
|
+
'runs under Async/Falcon, Itsi, or anything else that installs a ' \
|
|
15
|
+
'scheduler. Single event loop with min-heap timer, skip-overlapping ' \
|
|
16
|
+
'execution, jitter, monotonic clock intervals, semaphore concurrency ' \
|
|
17
|
+
'control, and deterministic worker sharding.'
|
|
17
18
|
|
|
18
19
|
spec.homepage = 'https://github.com/roman-haidarov/async-background'
|
|
19
20
|
spec.license = 'MIT'
|
|
@@ -30,15 +31,20 @@ Gem::Specification.new do |spec|
|
|
|
30
31
|
|
|
31
32
|
spec.required_ruby_version = '>= 3.3'
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
# No scheduler dependency on purpose. The gem uses the Fiber::Scheduler
|
|
35
|
+
# interface (Fiber.schedule, #block/#unblock, Timeout) and requires the host
|
|
36
|
+
# to install an implementation.
|
|
34
37
|
spec.add_dependency 'console', '~> 1.0'
|
|
35
38
|
spec.add_dependency 'fugit', '~> 1.0'
|
|
36
39
|
spec.add_dependency 'base64', '~> 0.2'
|
|
37
40
|
|
|
38
41
|
# Optional: add to your own Gemfile if you need these features
|
|
42
|
+
# gem 'async', '~> 2.0' # or any other Fiber scheduler
|
|
43
|
+
# gem 'itsi-scheduler' # ... such as this one
|
|
39
44
|
# gem 'sqlite3', '~> 2.0'
|
|
40
45
|
# gem 'async-utilization', '>= 0.3', '< 0.5' # shared-memory worker metrics
|
|
41
46
|
|
|
47
|
+
spec.add_development_dependency 'async', '~> 2.0'
|
|
42
48
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
43
49
|
spec.add_development_dependency 'rspec', '~> 3.12'
|
|
44
50
|
spec.add_development_dependency 'rack', '~> 3.0'
|
data/lib/async/background/job.rb
CHANGED
|
@@ -8,42 +8,81 @@ module Async
|
|
|
8
8
|
BACKOFFS = %i[fixed linear exponential].freeze
|
|
9
9
|
DEFAULT_JITTER_FOR = { fixed: 0.0, linear: 0.0, exponential: 0.5 }.freeze
|
|
10
10
|
|
|
11
|
+
NormalizedOptions = Data.define(:timeout, :retries, :retry_delay, :backoff, :attempt, :jitter)
|
|
12
|
+
private_constant :NormalizedOptions
|
|
13
|
+
|
|
11
14
|
Options = Data.define(:timeout, :retry, :retry_delay, :backoff, :attempt, :jitter) do
|
|
12
15
|
def initialize(timeout: DEFAULT_TIMEOUT, retry: 0, retry_delay: nil, backoff: :fixed, attempt: nil, jitter: nil)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
raise ArgumentError, "retry must be >= 0" if retries.negative?
|
|
22
|
-
raise ArgumentError, "attempt must be >= 0" if attempt && attempt.negative?
|
|
23
|
-
raise ArgumentError, "backoff must be one of #{BACKOFFS.inspect}" unless BACKOFFS.include?(backoff)
|
|
24
|
-
if retries.positive?
|
|
25
|
-
raise ArgumentError, "retry_delay is required when retry > 0" if retry_delay.nil?
|
|
26
|
-
raise ArgumentError, "retry_delay must be > 0" unless retry_delay.positive?
|
|
27
|
-
end
|
|
28
|
-
raise ArgumentError, "jitter must be in [0, 1]" if jitter && !(0.0..1.0).cover?(jitter)
|
|
29
|
-
|
|
30
|
-
super(timeout:, retry: retries, retry_delay:, backoff:, attempt:, jitter:)
|
|
16
|
+
retries = Integer(binding.local_variable_get(:retry) || 0)
|
|
17
|
+
settings = NormalizedOptions.new(
|
|
18
|
+
Integer(timeout), retries, retry_delay.nil? ? nil : Float(retry_delay), backoff.to_sym,
|
|
19
|
+
attempt.nil? ? nil : Integer(attempt), jitter.nil? ? nil : Float(jitter)
|
|
20
|
+
)
|
|
21
|
+
validate_options!(settings)
|
|
22
|
+
super(timeout: settings.timeout, retry: settings.retries, retry_delay: settings.retry_delay,
|
|
23
|
+
backoff: settings.backoff, attempt: settings.attempt, jitter: settings.jitter)
|
|
31
24
|
end
|
|
32
25
|
|
|
33
26
|
def retry? = self.retry.positive?
|
|
34
27
|
def attempts_made = attempt || 0
|
|
35
28
|
def next_attempt = attempts_made + 1
|
|
36
29
|
def with_attempt(n) = with(attempt: Integer(n))
|
|
30
|
+
|
|
37
31
|
def next_retry_delay(for_attempt = next_attempt, rng: Random)
|
|
38
|
-
raise ArgumentError,
|
|
39
|
-
raise ArgumentError,
|
|
32
|
+
raise ArgumentError, 'attempt must be > 0' unless for_attempt.positive?
|
|
33
|
+
raise ArgumentError, 'retry_delay must be configured' if retry_delay.nil?
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
apply_jitter(base_retry_delay(for_attempt), rng)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def validate_options!(settings)
|
|
41
|
+
validate_timeout!(settings.timeout)
|
|
42
|
+
validate_retry_count!(settings.retries)
|
|
43
|
+
validate_attempt!(settings.attempt)
|
|
44
|
+
validate_backoff!(settings.backoff)
|
|
45
|
+
validate_retry_delay!(settings)
|
|
46
|
+
validate_jitter!(settings.jitter)
|
|
47
|
+
end
|
|
46
48
|
|
|
49
|
+
def validate_timeout!(timeout)
|
|
50
|
+
raise ArgumentError, 'timeout must be > 0' unless timeout.positive?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_retry_count!(retries)
|
|
54
|
+
raise ArgumentError, 'retry must be >= 0' if retries.negative?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_attempt!(attempt)
|
|
58
|
+
raise ArgumentError, 'attempt must be >= 0' if attempt&.negative?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_backoff!(backoff)
|
|
62
|
+
raise ArgumentError, "backoff must be one of #{BACKOFFS.inspect}" unless BACKOFFS.include?(backoff)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_retry_delay!(settings)
|
|
66
|
+
return unless settings.retries.positive?
|
|
67
|
+
|
|
68
|
+
raise ArgumentError, 'retry_delay is required when retry > 0' if settings.retry_delay.nil?
|
|
69
|
+
raise ArgumentError, 'retry_delay must be > 0' unless settings.retry_delay.positive?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_jitter!(jitter)
|
|
73
|
+
raise ArgumentError, 'jitter must be in [0, 1]' if jitter && !(0.0..1.0).cover?(jitter)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def base_retry_delay(for_attempt)
|
|
77
|
+
multiplier = case backoff
|
|
78
|
+
when :fixed then 1
|
|
79
|
+
when :linear then for_attempt
|
|
80
|
+
when :exponential then 2**(for_attempt - 1)
|
|
81
|
+
end
|
|
82
|
+
retry_delay * multiplier
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def apply_jitter(base, rng)
|
|
47
86
|
factor = jitter || DEFAULT_JITTER_FOR[backoff]
|
|
48
87
|
factor.zero? ? base : base * (1 + rng.rand * factor)
|
|
49
88
|
end
|
|
@@ -56,9 +95,17 @@ module Async
|
|
|
56
95
|
module ClassMethods
|
|
57
96
|
def perform_now(*args) = new.perform(*args)
|
|
58
97
|
|
|
59
|
-
def perform_async(*args, options: EMPTY_OPTIONS)
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
def perform_async(*args, options: EMPTY_OPTIONS)
|
|
99
|
+
Async::Background::Queue.enqueue(self, *args, options: options)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def perform_in(delay, *args, options: EMPTY_OPTIONS)
|
|
103
|
+
Async::Background::Queue.enqueue_in(delay, self, *args, options: options)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def perform_at(time, *args, options: EMPTY_OPTIONS)
|
|
107
|
+
Async::Background::Queue.enqueue_at(time, self, *args, options: options)
|
|
108
|
+
end
|
|
62
109
|
|
|
63
110
|
def options(**values)
|
|
64
111
|
@options = Options.new(**values).to_h.compact
|
|
@@ -29,53 +29,57 @@ module Async
|
|
|
29
29
|
|
|
30
30
|
validate_worker!(worker_index, total_workers)
|
|
31
31
|
initialize_registry!(worker_index, total_workers, shm_path)
|
|
32
|
-
rescue LoadError =>
|
|
33
|
-
mark_unavailable!(
|
|
32
|
+
rescue LoadError => e
|
|
33
|
+
mark_unavailable!(e)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def enabled? = @enabled
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
module JobEvents
|
|
39
|
+
def job_started(_entry)
|
|
40
|
+
return unless enabled?
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
increment(:total_runs)
|
|
43
|
+
increment(:active_jobs)
|
|
44
|
+
set(:last_run_at, Process.clock_gettime(Process::CLOCK_REALTIME).to_i)
|
|
45
|
+
end
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
def job_succeeded(_entry, duration)
|
|
48
|
+
return unless enabled?
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
increment(:total_successes)
|
|
51
|
+
set(:last_duration_ms, duration_to_milliseconds(duration))
|
|
52
|
+
end
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
def job_finished(entry, duration)
|
|
55
|
+
job_succeeded(entry, duration)
|
|
56
|
+
job_stopped(entry)
|
|
57
|
+
end
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
def job_failed(_entry, _error)
|
|
60
|
+
increment(:total_failures) if enabled?
|
|
61
|
+
end
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
def job_timed_out(_entry)
|
|
64
|
+
increment(:total_timeouts) if enabled?
|
|
65
|
+
end
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
def job_stopped(_entry)
|
|
68
|
+
decrement(:active_jobs) if enabled?
|
|
69
|
+
end
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
def job_skipped(_entry)
|
|
72
|
+
increment(:total_skips) if enabled?
|
|
73
|
+
end
|
|
72
74
|
end
|
|
75
|
+
private_constant :JobEvents
|
|
76
|
+
include JobEvents
|
|
73
77
|
|
|
74
78
|
def values
|
|
75
79
|
enabled? ? registry.values : {}
|
|
76
80
|
end
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
module Reader
|
|
79
83
|
def available?
|
|
80
84
|
load_utilization!
|
|
81
85
|
true
|
|
@@ -138,6 +142,8 @@ module Async
|
|
|
138
142
|
end
|
|
139
143
|
end
|
|
140
144
|
|
|
145
|
+
extend Reader
|
|
146
|
+
|
|
141
147
|
private
|
|
142
148
|
|
|
143
149
|
def initialize_registry!(worker_index, total_workers, path)
|
|
@@ -145,9 +151,7 @@ module Async
|
|
|
145
151
|
ensure_shm!(total_workers, path)
|
|
146
152
|
|
|
147
153
|
@registry = ::Async::Utilization::Registry.new
|
|
148
|
-
unless @registry.respond_to?(:metric)
|
|
149
|
-
raise LoadError, 'async-utilization >= 0.3 is required for metrics'
|
|
150
|
-
end
|
|
154
|
+
raise LoadError, 'async-utilization >= 0.3 is required for metrics' unless @registry.respond_to?(:metric)
|
|
151
155
|
|
|
152
156
|
attach_observer!(worker_index, path)
|
|
153
157
|
@metric_handles = SCHEMA_FIELDS.keys.to_h { |name| [name, @registry.metric(name)] }.freeze
|
|
@@ -196,7 +200,11 @@ module Async
|
|
|
196
200
|
file.flock(File::LOCK_EX)
|
|
197
201
|
file.truncate(mapped_size) if file.size < mapped_size
|
|
198
202
|
ensure
|
|
199
|
-
|
|
203
|
+
begin
|
|
204
|
+
file.flock(File::LOCK_UN)
|
|
205
|
+
rescue StandardError
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
200
208
|
end
|
|
201
209
|
end
|
|
202
210
|
|
|
@@ -41,7 +41,7 @@ module Async
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
43
|
def sift_up(i)
|
|
44
|
-
while i
|
|
44
|
+
while i.positive?
|
|
45
45
|
parent = (i - 1) >> 1
|
|
46
46
|
break if @data[parent].next_run_at <= @data[i].next_run_at
|
|
47
47
|
|
|
@@ -50,20 +50,24 @@ module Async
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def sift_down(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
right = left + 1
|
|
53
|
+
def sift_down(index)
|
|
54
|
+
loop do
|
|
55
|
+
child = smallest_child(index)
|
|
56
|
+
break if child == index
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
swap(index, child)
|
|
59
|
+
index = child
|
|
60
|
+
end
|
|
61
|
+
end
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
def smallest_child(index)
|
|
64
|
+
left = (index << 1) + 1
|
|
65
|
+
right = left + 1
|
|
66
|
+
smallest = index
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
smallest = left if left < @data.size && @data[left].next_run_at < @data[smallest].next_run_at
|
|
69
|
+
smallest = right if right < @data.size && @data[right].next_run_at < @data[smallest].next_run_at
|
|
70
|
+
smallest
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
def swap(a, b)
|
|
@@ -43,17 +43,20 @@ module Async
|
|
|
43
43
|
|
|
44
44
|
def enqueue(job_class, *args, options: EMPTY_OPTIONS)
|
|
45
45
|
ensure_configured!
|
|
46
|
-
default_client.push(resolve_class_name(job_class), normalized_args(args), nil,
|
|
46
|
+
default_client.push(resolve_class_name(job_class), normalized_args(args), nil,
|
|
47
|
+
options: build_options(job_class, options))
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def enqueue_in(delay, job_class, *args, options: EMPTY_OPTIONS)
|
|
50
51
|
ensure_configured!
|
|
51
|
-
default_client.push_in(delay, resolve_class_name(job_class), normalized_args(args),
|
|
52
|
+
default_client.push_in(delay, resolve_class_name(job_class), normalized_args(args),
|
|
53
|
+
options: build_options(job_class, options))
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def enqueue_at(time, job_class, *args, options: EMPTY_OPTIONS)
|
|
55
57
|
ensure_configured!
|
|
56
|
-
default_client.push_at(time, resolve_class_name(job_class), normalized_args(args),
|
|
58
|
+
default_client.push_at(time, resolve_class_name(job_class), normalized_args(args),
|
|
59
|
+
options: build_options(job_class, options))
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
private
|
|
@@ -79,7 +82,7 @@ module Async
|
|
|
79
82
|
end
|
|
80
83
|
|
|
81
84
|
def ensure_configured!
|
|
82
|
-
raise
|
|
85
|
+
raise 'Async::Background::Queue not configured' unless default_client
|
|
83
86
|
end
|
|
84
87
|
|
|
85
88
|
def normalized_args(args) = args.empty? ? EMPTY_ARGS : args
|
|
@@ -95,10 +98,10 @@ module Async
|
|
|
95
98
|
return EMPTY_OPTIONS unless job_class.respond_to?(:resolve_options)
|
|
96
99
|
|
|
97
100
|
options = if job_class.respond_to?(:queue_options)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
job_class.queue_options
|
|
102
|
+
else
|
|
103
|
+
job_class.resolve_options
|
|
104
|
+
end
|
|
102
105
|
|
|
103
106
|
options.empty? ? EMPTY_OPTIONS : options
|
|
104
107
|
end
|
|
@@ -6,9 +6,9 @@ module Async
|
|
|
6
6
|
EMPTY_ARGS = [].freeze
|
|
7
7
|
EMPTY_OPTIONS = {}.freeze
|
|
8
8
|
|
|
9
|
-
SYNCHRONOUS_LEVELS = {normal: 'NORMAL', full: 'FULL', extra: 'EXTRA'}.freeze
|
|
9
|
+
SYNCHRONOUS_LEVELS = { normal: 'NORMAL', full: 'FULL', extra: 'EXTRA' }.freeze
|
|
10
10
|
WAL_AUTOCHECKPOINT_RANGE = 100..10_000
|
|
11
|
-
DEFAULT_STORE_OPTIONS = {mmap: true, synchronous: :normal, wal_autocheckpoint: 1_000}.freeze
|
|
11
|
+
DEFAULT_STORE_OPTIONS = { mmap: true, synchronous: :normal, wal_autocheckpoint: 1_000 }.freeze
|
|
12
12
|
DEFAULTS = DEFAULT_STORE_OPTIONS
|
|
13
13
|
MMAP_SIZE = 268_435_456
|
|
14
14
|
|
|
@@ -45,7 +45,7 @@ module Async
|
|
|
45
45
|
private
|
|
46
46
|
|
|
47
47
|
def validate_mmap!(value)
|
|
48
|
-
return if
|
|
48
|
+
return if [true, false].include?(value)
|
|
49
49
|
|
|
50
50
|
raise ArgumentError, "mmap must be true or false, got #{value.inspect}"
|
|
51
51
|
end
|