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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -0
  3. data/README.md +36 -15
  4. data/async-background.gemspec +13 -7
  5. data/lib/async/background/job.rb +75 -28
  6. data/lib/async/background/metrics.rb +41 -33
  7. data/lib/async/background/min_heap.rb +16 -12
  8. data/lib/async/background/queue/client.rb +11 -8
  9. data/lib/async/background/queue/options.rb +3 -3
  10. data/lib/async/background/queue/schema.rb +19 -15
  11. data/lib/async/background/queue/socket_notifier.rb +14 -4
  12. data/lib/async/background/queue/socket_waker.rb +139 -56
  13. data/lib/async/background/queue/sql.rb +31 -31
  14. data/lib/async/background/queue/store.rb +44 -45
  15. data/lib/async/background/runner/lifecycle.rb +131 -0
  16. data/lib/async/background/runner/queue_execution.rb +101 -57
  17. data/lib/async/background/runner/schedule.rb +20 -17
  18. data/lib/async/background/runner.rb +98 -77
  19. data/lib/async/background/runtime/notification.rb +42 -0
  20. data/lib/async/background/runtime/semaphore.rb +151 -0
  21. data/lib/async/background/runtime/task.rb +146 -0
  22. data/lib/async/background/runtime/task_group.rb +81 -0
  23. data/lib/async/background/runtime.rb +246 -0
  24. data/lib/async/background/scheduler.rb +145 -0
  25. data/lib/async/background/version.rb +1 -1
  26. data/lib/async/background/web/app.rb +43 -40
  27. data/lib/async/background/web/assets/app.css +174 -0
  28. data/lib/async/background/web/assets/app.js +486 -0
  29. data/lib/async/background/web/assets/index.html +29 -0
  30. data/lib/async/background/web/assets.rb +4 -697
  31. data/lib/async/background/web/auth.rb +3 -3
  32. data/lib/async/background/web/configuration.rb +40 -93
  33. data/lib/async/background/web/cursor.rb +22 -26
  34. data/lib/async/background/web/event_hub.rb +12 -7
  35. data/lib/async/background/web/metrics_reader.rb +28 -36
  36. data/lib/async/background/web/response.rb +39 -51
  37. data/lib/async/background/web/serializer.rb +23 -63
  38. data/lib/async/background/web/snapshot.rb +35 -81
  39. data/lib/async/background/web/sql.rb +63 -69
  40. data/lib/async/background/web/stream.rb +52 -32
  41. data/lib/async/background/web.rb +1 -1
  42. data/lib/async/background.rb +1 -2
  43. metadata +30 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 233f46d3617b40d9a8a27f83689050b0127603679342ddfa80a18eea0137651c
4
- data.tar.gz: 5d70fe357702dff97b51734c1fddf64178f3ea0d9d9d88449fe3af9b32fc59d3
3
+ metadata.gz: d9bd77f8af5601609ef43abb3b108ce225e0b20357e712023e2118a06c7e3496
4
+ data.tar.gz: a0e71bfec3a834da9d7a2df0fbd161ee42bbedff00ea1e90f62bee89d9c1edb3
5
5
  SHA512:
6
- metadata.gz: 148770cb2522083b5944a5ee0f5830a1e1453f9c06647feaf0b92842e6008e2654aefd7e8c4a4a97509fc823702d64176a7bf4e30e8a08d409308f57792bbb1d
7
- data.tar.gz: 99e64f1f1066528429617270d54ae64109896d1318634f5b4b001a9d10e4ed19b88239dc4b7a089c885d4f481d2799b22b70495b763d64df51702c82b15865f8
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's
4
- [Async](https://github.com/socketry/async) ecosystem. Built for
5
- [Falcon](https://github.com/socketry/falcon), works with any Async app.
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 Async? Why fibers?
18
+ ## Why fibers?
19
19
 
20
- The whole gem is built around the assumption that Falcon's reactor schedules
21
- many fibers on top of one OS thread per process — so the dashboard's SSE
22
- stream, the cron scheduler, and the queue worker all share that one thread
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
  ![Threads vs fibers under different Ruby web servers](docs/fibers-vs-threads.svg)
@@ -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
- | `async` | `~> 2.0` | yes |
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 "sqlite3", "~> 2.0" # if you use the queue or dashboard
52
- gem "async-utilization", ">= 0.3", "< 0.5" # if you want worker metrics
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
- A four-step walkthrough: schedule config, Falcon integration, Docker, queue,
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 Async task sleeps until the next entry is due, then dispatches it
124
- under a semaphore that caps concurrency. Overlapping ticks are skipped and
125
- rescheduled.
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
@@ -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 Async.'
12
- spec.description = 'A production-grade lightweight scheduler built on top of Async. ' \
13
- 'Single event loop with min-heap timer, skip-overlapping execution, ' \
14
- 'jitter, monotonic clock intervals, semaphore concurrency control, ' \
15
- 'and deterministic worker sharding. Designed for Falcon but works ' \
16
- 'with any Async-based application.'
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
- spec.add_dependency 'async', '~> 2.0'
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'
@@ -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
- timeout = Integer(timeout)
14
- retries = Integer(binding.local_variable_get(:retry) || 0)
15
- retry_delay = Float(retry_delay) unless retry_delay.nil?
16
- backoff = backoff.to_sym
17
- attempt = Integer(attempt) unless attempt.nil?
18
- jitter = Float(jitter) unless jitter.nil?
19
-
20
- raise ArgumentError, "timeout must be > 0" unless timeout.positive?
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, "attempt must be > 0" unless for_attempt.positive?
39
- raise ArgumentError, "retry_delay must be configured" if retry_delay.nil?
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
- base = case backoff
42
- when :fixed then retry_delay
43
- when :linear then retry_delay * for_attempt
44
- when :exponential then retry_delay * (2**(for_attempt - 1))
45
- end
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) = Async::Background::Queue.enqueue(self, *args, options: options)
60
- def perform_in(delay, *args, options: EMPTY_OPTIONS) = Async::Background::Queue.enqueue_in(delay, self, *args, options: options)
61
- def perform_at(time, *args, options: EMPTY_OPTIONS) = Async::Background::Queue.enqueue_at(time, self, *args, options: options)
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 => error
33
- mark_unavailable!(error)
32
+ rescue LoadError => e
33
+ mark_unavailable!(e)
34
34
  end
35
35
 
36
36
  def enabled? = @enabled
37
37
 
38
- def job_started(_entry)
39
- return unless enabled?
38
+ module JobEvents
39
+ def job_started(_entry)
40
+ return unless enabled?
40
41
 
41
- increment(:total_runs)
42
- increment(:active_jobs)
43
- set(:last_run_at, Process.clock_gettime(Process::CLOCK_REALTIME).to_i)
44
- end
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
- def job_succeeded(_entry, duration)
47
- return unless enabled?
47
+ def job_succeeded(_entry, duration)
48
+ return unless enabled?
48
49
 
49
- increment(:total_successes)
50
- set(:last_duration_ms, duration_to_milliseconds(duration))
51
- end
50
+ increment(:total_successes)
51
+ set(:last_duration_ms, duration_to_milliseconds(duration))
52
+ end
52
53
 
53
- def job_finished(entry, duration)
54
- job_succeeded(entry, duration)
55
- job_stopped(entry)
56
- end
54
+ def job_finished(entry, duration)
55
+ job_succeeded(entry, duration)
56
+ job_stopped(entry)
57
+ end
57
58
 
58
- def job_failed(_entry, _error)
59
- increment(:total_failures) if enabled?
60
- end
59
+ def job_failed(_entry, _error)
60
+ increment(:total_failures) if enabled?
61
+ end
61
62
 
62
- def job_timed_out(_entry)
63
- increment(:total_timeouts) if enabled?
64
- end
63
+ def job_timed_out(_entry)
64
+ increment(:total_timeouts) if enabled?
65
+ end
65
66
 
66
- def job_stopped(_entry)
67
- decrement(:active_jobs) if enabled?
68
- end
67
+ def job_stopped(_entry)
68
+ decrement(:active_jobs) if enabled?
69
+ end
69
70
 
70
- def job_skipped(_entry)
71
- increment(:total_skips) if enabled?
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
- class << self
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
- file.flock(File::LOCK_UN) rescue nil
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 > 0
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(i)
54
- while true
55
- smallest = i
56
- left = (i << 1) + 1
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
- smallest = left if left < @data.size && @data[left].next_run_at < @data[smallest].next_run_at
60
- smallest = right if right < @data.size && @data[right].next_run_at < @data[smallest].next_run_at
58
+ swap(index, child)
59
+ index = child
60
+ end
61
+ end
61
62
 
62
- break if smallest == i
63
+ def smallest_child(index)
64
+ left = (index << 1) + 1
65
+ right = left + 1
66
+ smallest = index
63
67
 
64
- swap(i, smallest)
65
- i = smallest
66
- end
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, options: build_options(job_class, options))
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), options: build_options(job_class, options))
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), options: build_options(job_class, options))
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 "Async::Background::Queue not configured" unless default_client
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
- job_class.queue_options
99
- else
100
- job_class.resolve_options
101
- end
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 value == true || value == false
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