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 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
+ [![Gem Version](https://badge.fury.io/rb/philiprehberger-task_queue.svg)](https://rubygems.org/gems/philiprehberger-task_queue)
4
+ [![CI](https://github.com/philiprehberger/rb-task-queue/actions/workflows/ci.yml/badge.svg)](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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module TaskQueue
5
+ VERSION = "0.1.0"
6
+ end
7
+ 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: []