philiprehberger-task_queue 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0234cce054f1fc8e8923a25a414198c1863bfa035f9e9881ab38b48c484460d4
4
- data.tar.gz: e93358c9bec5ab194464dbdec66cdbf9ff414970d21e4539b578c9f01effd6ff
3
+ metadata.gz: 34263f9a3370dc191d5f7f2ec9744e9c92b9e50b80e305e9916d097a070d6b8b
4
+ data.tar.gz: 230bf370d52a1b02eb928ac0a53b3f0abbb519cd5396903c622f831691f6128e
5
5
  SHA512:
6
- metadata.gz: 045c07963ff8f40a847eb375b2b04830f092fe5d0355fc3cf0db59865328083141f6859b2c18abbe46d844c8cd4af3b25143cfa79f4b01fabb28fc7e3acf0a2f
7
- data.tar.gz: f6d1b9a18e472fc17b717b65a076918319e83445d30f668df121833207fe7b70345d6e1b0264951b95e3f39f79643c4d6f8e3d0e575ee918175dc152cf2d9470
6
+ metadata.gz: f96333a34f87690f6ada381f4eec29e419aaf1a72aaf2237f7fd31d3af252aa6d11f2851613afcf56040f4c79bd50d12d397adf949b7eac2a01ee2d10f15b886
7
+ data.tar.gz: a607205711f5647aceb92c6eec0c19103681a7c074ddfd1de7b3dbda30956f356403770bb36721c9ea5a2171d9282a84117f239c66b9a99b8c1a47a73875119b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.0] - 2026-03-12
6
+
7
+ ### Added
8
+
9
+ - `on_error` callback for handling task failures
10
+ - `stats` method returning completed, failed, and pending task counts
11
+ - `drain(timeout:)` method to wait for task completion without shutting down
12
+
5
13
  ## [0.1.0] - 2026-03-10
6
14
 
7
15
  ### Added
data/README.md CHANGED
@@ -42,6 +42,33 @@ queue.shutdown(timeout: 30)
42
42
  queue << -> { puts "Hello from a task!" }
43
43
  ```
44
44
 
45
+ ### Error handling
46
+
47
+ ```ruby
48
+ queue = Philiprehberger::TaskQueue.new
49
+
50
+ queue.on_error do |exception, task|
51
+ puts "Task failed: #{exception.message}"
52
+ end
53
+
54
+ queue.push { raise "oops" }
55
+ ```
56
+
57
+ ### Statistics
58
+
59
+ ```ruby
60
+ queue.stats
61
+ # => { completed: 5, failed: 1, pending: 2 }
62
+ ```
63
+
64
+ ### Draining
65
+
66
+ ```ruby
67
+ 10.times { |i| queue.push { process(i) } }
68
+ queue.drain(timeout: 10) # waits for all tasks to finish
69
+ # queue is still running and accepting new tasks
70
+ ```
71
+
45
72
  ## API
46
73
 
47
74
  | Method | Description |
@@ -52,6 +79,9 @@ queue << -> { puts "Hello from a task!" }
52
79
  | `#size` | Number of pending (not yet started) tasks |
53
80
  | `#running?` | Whether the queue is accepting new tasks |
54
81
  | `#shutdown(timeout: 30)` | Gracefully stop all workers, waiting up to `timeout` seconds |
82
+ | `#on_error(&block)` | Register error callback for failed tasks |
83
+ | `#stats` | Returns hash with `:completed`, `:failed`, `:pending` counts |
84
+ | `#drain(timeout: 30)` | Block until all pending tasks complete (without shutdown) |
55
85
 
56
86
  ## License
57
87
 
@@ -15,9 +15,49 @@ module Philiprehberger
15
15
  @tasks = []
16
16
  @mutex = Mutex.new
17
17
  @condition = ConditionVariable.new
18
+ @drain_condition = ConditionVariable.new
18
19
  @workers = []
19
20
  @running = true
20
21
  @started = false
22
+ @error_handler = nil
23
+ @stats = { completed: 0, failed: 0, in_flight: 0 }
24
+ end
25
+
26
+ # Register a callback invoked when a task raises an exception.
27
+ #
28
+ # The callback receives the exception and the task that raised it.
29
+ #
30
+ # @yield [exception, task] called on task failure
31
+ # @return [self]
32
+ def on_error(&block)
33
+ @mutex.synchronize { @error_handler = block }
34
+ self
35
+ end
36
+
37
+ # Return statistics about processed tasks.
38
+ #
39
+ # @return [Hash{Symbol => Integer}] counts for :completed, :failed, :pending
40
+ def stats
41
+ @mutex.synchronize do
42
+ { completed: @stats[:completed], failed: @stats[:failed], pending: @tasks.size }
43
+ end
44
+ end
45
+
46
+ # Block until all pending tasks are complete without shutting down.
47
+ #
48
+ # @param timeout [Numeric] seconds to wait before returning
49
+ # @return [void]
50
+ def drain(timeout: 30)
51
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
52
+ @mutex.synchronize do
53
+ while !@tasks.empty? || @stats[:in_flight].positive?
54
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
55
+ break if remaining <= 0
56
+
57
+ @drain_condition.wait(@mutex, remaining)
58
+ end
59
+ end
60
+ nil
21
61
  end
22
62
 
23
63
  # Enqueue a task to be processed asynchronously.
@@ -91,7 +131,10 @@ module Philiprehberger
91
131
 
92
132
  def start_workers
93
133
  @concurrency.times do
94
- @workers << Worker.new(@tasks, @mutex, @condition)
134
+ @workers << Worker.new(
135
+ @tasks, @mutex, @condition,
136
+ context: { stats: @stats, error_handler: @error_handler, drain_condition: @drain_condition }
137
+ )
95
138
  end
96
139
  @started = true
97
140
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module TaskQueue
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -6,10 +6,13 @@ module Philiprehberger
6
6
  class Worker
7
7
  attr_reader :thread
8
8
 
9
- def initialize(queue, mutex, condition)
9
+ def initialize(queue, mutex, condition, context:)
10
10
  @queue = queue
11
11
  @mutex = mutex
12
12
  @condition = condition
13
+ @stats = context[:stats]
14
+ @error_handler = context[:error_handler]
15
+ @drain_condition = context[:drain_condition]
13
16
  @running = true
14
17
  @thread = Thread.new { run }
15
18
  end
@@ -30,6 +33,7 @@ module Philiprehberger
30
33
  break unless task
31
34
 
32
35
  execute(task)
36
+ @mutex.synchronize { @drain_condition.broadcast }
33
37
  end
34
38
  end
35
39
 
@@ -38,14 +42,31 @@ module Philiprehberger
38
42
  @condition.wait(@mutex) while @queue.empty? && @running
39
43
  return nil unless @running || !@queue.empty?
40
44
 
45
+ @stats[:in_flight] += 1
41
46
  @queue.shift
42
47
  end
43
48
  end
44
49
 
45
50
  def execute(task)
46
51
  task.call
47
- rescue StandardError
48
- # Swallow exceptions to keep the worker alive.
52
+ record_completion
53
+ rescue StandardError => e
54
+ record_failure(e, task)
55
+ end
56
+
57
+ def record_completion
58
+ @mutex.synchronize do
59
+ @stats[:completed] += 1
60
+ @stats[:in_flight] -= 1
61
+ end
62
+ end
63
+
64
+ def record_failure(error, task)
65
+ @mutex.synchronize do
66
+ @stats[:failed] += 1
67
+ @stats[:in_flight] -= 1
68
+ end
69
+ @error_handler&.call(error, task)
49
70
  end
50
71
  end
51
72
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-task_queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-10 00:00:00.000000000 Z
11
+ date: 2026-03-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A lightweight, zero-dependency, thread-safe in-process async job queue
14
14
  with configurable concurrency for Ruby applications.