philiprehberger-task_queue 0.1.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 +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/README.md +58 -0
- data/lib/philiprehberger/task_queue/queue.rb +100 -0
- data/lib/philiprehberger/task_queue/version.rb +7 -0
- data/lib/philiprehberger/task_queue/worker.rb +52 -0
- data/lib/philiprehberger/task_queue.rb +16 -0
- metadata +55 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0234cce054f1fc8e8923a25a414198c1863bfa035f9e9881ab38b48c484460d4
|
|
4
|
+
data.tar.gz: e93358c9bec5ab194464dbdec66cdbf9ff414970d21e4539b578c9f01effd6ff
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 045c07963ff8f40a847eb375b2b04830f092fe5d0355fc3cf0db59865328083141f6859b2c18abbe46d844c8cd4af3b25143cfa79f4b01fabb28fc7e3acf0a2f
|
|
7
|
+
data.tar.gz: f6d1b9a18e472fc17b717b65a076918319e83445d30f668df121833207fe7b70345d6e1b0264951b95e3f39f79643c4d6f8e3d0e575ee918175dc152cf2d9470
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-03-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Initial release
|
|
10
|
+
- In-process async job queue with configurable concurrency
|
|
11
|
+
- Thread-safe task enqueuing with `push` / `<<`
|
|
12
|
+
- Graceful shutdown with timeout support
|
|
13
|
+
- Auto-starting worker threads on first push
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 philiprehberger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# philiprehberger-task_queue
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/philiprehberger-task_queue)
|
|
4
|
+
[](https://github.com/philiprehberger/rb-task-queue/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
In-process async job queue with concurrency control for Ruby.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Add to your Gemfile:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem "philiprehberger-task_queue"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or install directly:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
gem install philiprehberger-task_queue
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require "philiprehberger/task_queue"
|
|
26
|
+
|
|
27
|
+
queue = Philiprehberger::TaskQueue.new(concurrency: 4)
|
|
28
|
+
|
|
29
|
+
10.times do |i|
|
|
30
|
+
queue.push { puts "Processing job #{i}" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
puts queue.size # number of pending tasks
|
|
34
|
+
puts queue.running? # => true
|
|
35
|
+
|
|
36
|
+
queue.shutdown(timeout: 30)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Using the `<<` alias
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
queue << -> { puts "Hello from a task!" }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
| Method | Description |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `.new(concurrency: 4)` | Create a new queue with the given max worker count |
|
|
50
|
+
| `#push(&block)` | Enqueue a task (block) for async execution |
|
|
51
|
+
| `#<< (&block)` | Alias for `#push` |
|
|
52
|
+
| `#size` | Number of pending (not yet started) tasks |
|
|
53
|
+
| `#running?` | Whether the queue is accepting new tasks |
|
|
54
|
+
| `#shutdown(timeout: 30)` | Gracefully stop all workers, waiting up to `timeout` seconds |
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "worker"
|
|
4
|
+
|
|
5
|
+
module Philiprehberger
|
|
6
|
+
module TaskQueue
|
|
7
|
+
# In-process async job queue with concurrency control.
|
|
8
|
+
#
|
|
9
|
+
# Tasks are enqueued as blocks or callable objects and executed by a pool of
|
|
10
|
+
# worker threads. The queue is fully thread-safe.
|
|
11
|
+
class Queue
|
|
12
|
+
# @param concurrency [Integer] maximum number of concurrent worker threads
|
|
13
|
+
def initialize(concurrency: 4)
|
|
14
|
+
@concurrency = concurrency
|
|
15
|
+
@tasks = []
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@condition = ConditionVariable.new
|
|
18
|
+
@workers = []
|
|
19
|
+
@running = true
|
|
20
|
+
@started = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Enqueue a task to be processed asynchronously.
|
|
24
|
+
#
|
|
25
|
+
# @param callable [#call, nil] a callable object (used by +<<+)
|
|
26
|
+
# @yield the block to execute (takes precedence over +callable+)
|
|
27
|
+
# @return [self]
|
|
28
|
+
def push(callable = nil, &block)
|
|
29
|
+
task = block || callable
|
|
30
|
+
raise ArgumentError, "a block is required" unless task
|
|
31
|
+
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
raise "queue is shut down" unless @running
|
|
34
|
+
|
|
35
|
+
start_workers unless @started
|
|
36
|
+
@tasks << task
|
|
37
|
+
@condition.signal
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias << push
|
|
44
|
+
|
|
45
|
+
# Number of pending (not yet started) tasks.
|
|
46
|
+
#
|
|
47
|
+
# @return [Integer]
|
|
48
|
+
def size
|
|
49
|
+
@mutex.synchronize { @tasks.size }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether the queue is accepting new tasks.
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def running?
|
|
56
|
+
@mutex.synchronize { @running }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Gracefully shut down the queue.
|
|
60
|
+
#
|
|
61
|
+
# Signals all workers to finish their current task and drain remaining
|
|
62
|
+
# tasks, then waits up to +timeout+ seconds for threads to exit.
|
|
63
|
+
#
|
|
64
|
+
# @param timeout [Numeric] seconds to wait for workers to finish
|
|
65
|
+
# @return [void]
|
|
66
|
+
def shutdown(timeout: 30)
|
|
67
|
+
signal_shutdown
|
|
68
|
+
wait_for_workers(timeout)
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def signal_shutdown
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
return unless @running
|
|
77
|
+
|
|
78
|
+
@running = false
|
|
79
|
+
@workers.each(&:stop)
|
|
80
|
+
@condition.broadcast
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def wait_for_workers(timeout)
|
|
85
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
86
|
+
@workers.each do |worker|
|
|
87
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
88
|
+
worker.thread&.join([remaining, 0].max)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def start_workers
|
|
93
|
+
@concurrency.times do
|
|
94
|
+
@workers << Worker.new(@tasks, @mutex, @condition)
|
|
95
|
+
end
|
|
96
|
+
@started = true
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module TaskQueue
|
|
5
|
+
# Worker processes tasks from the queue in a dedicated thread.
|
|
6
|
+
class Worker
|
|
7
|
+
attr_reader :thread
|
|
8
|
+
|
|
9
|
+
def initialize(queue, mutex, condition)
|
|
10
|
+
@queue = queue
|
|
11
|
+
@mutex = mutex
|
|
12
|
+
@condition = condition
|
|
13
|
+
@running = true
|
|
14
|
+
@thread = Thread.new { run }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stop
|
|
18
|
+
@running = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def alive?
|
|
22
|
+
@thread&.alive? || false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
loop do
|
|
29
|
+
task = next_task
|
|
30
|
+
break unless task
|
|
31
|
+
|
|
32
|
+
execute(task)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def next_task
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
@condition.wait(@mutex) while @queue.empty? && @running
|
|
39
|
+
return nil unless @running || !@queue.empty?
|
|
40
|
+
|
|
41
|
+
@queue.shift
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def execute(task)
|
|
46
|
+
task.call
|
|
47
|
+
rescue StandardError
|
|
48
|
+
# Swallow exceptions to keep the worker alive.
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "task_queue/version"
|
|
4
|
+
require_relative "task_queue/queue"
|
|
5
|
+
|
|
6
|
+
module Philiprehberger
|
|
7
|
+
module TaskQueue
|
|
8
|
+
# Convenience constructor.
|
|
9
|
+
#
|
|
10
|
+
# @param options [Hash] forwarded to {Queue#initialize}
|
|
11
|
+
# @return [Queue]
|
|
12
|
+
def self.new(**options)
|
|
13
|
+
Queue.new(**options)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: philiprehberger-task_queue
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Philip Rehberger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A lightweight, zero-dependency, thread-safe in-process async job queue
|
|
14
|
+
with configurable concurrency for Ruby applications.
|
|
15
|
+
email:
|
|
16
|
+
- me@philiprehberger.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/philiprehberger/task_queue.rb
|
|
25
|
+
- lib/philiprehberger/task_queue/queue.rb
|
|
26
|
+
- lib/philiprehberger/task_queue/version.rb
|
|
27
|
+
- lib/philiprehberger/task_queue/worker.rb
|
|
28
|
+
homepage: https://github.com/philiprehberger/rb-task-queue
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
homepage_uri: https://github.com/philiprehberger/rb-task-queue
|
|
33
|
+
source_code_uri: https://github.com/philiprehberger/rb-task-queue
|
|
34
|
+
changelog_uri: https://github.com/philiprehberger/rb-task-queue/blob/main/CHANGELOG.md
|
|
35
|
+
rubygems_mfa_required: 'true'
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '3.1'
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 3.5.22
|
|
52
|
+
signing_key:
|
|
53
|
+
specification_version: 4
|
|
54
|
+
summary: In-process async job queue with concurrency control
|
|
55
|
+
test_files: []
|