cr-semafor
cr-semafor
A lightweight Crystal library for bounding concurrent work with keyed semaphores, acquisition wait timeouts, and execution time limits.
cr-semafor is a Crystal port of semafor, a TypeScript library for controlling how many tasks may run at once for a given key, how long callers are willing to wait to acquire a permit, and how long the guarded work may run.
Installation
Add the shard to your shard.yml:
dependencies:
cr-semafor:
github: Gwash3189/cr-semafor
Then run shards install.
Usage
Direct execution
Acquire a slot for a key and immediately run a block:
require "cr-semafor"
result = Semafor.semafor(
"key",
wait: 5.seconds,
workers: 35,
limit: 2.5.seconds
) do |release, signal|
begin
do_work(signal)
ensure
release.call
end
end
result.result
result.release.call
release is idempotent, so calling it from both the block's ensure and the returned result is safe.
Independent instance API
Create a standalone semaphore instance when state does not need to be shared by key:
semaphore = Semafor.new(workers: 35, wait: 5.seconds, limit: 2.5.seconds)
semaphore.acquire do
begin
do_work
ensure
semaphore.release
end
end
Semafor.new creates an independent Semafor::Semaphore; it is not registered globally. The instance API is block-scoped: the permit is released automatically when the block exits, and semaphore.release can be used to release it early. An optional key can be supplied for error messages:
semaphore = Semafor.new("image-resizer", workers: 4)
The block can also receive the lease and signal:
semaphore.acquire do |lease, signal|
begin
do_work(signal)
ensure
lease.call
end
end
Use Semafor.semaphore when multiple callers should share state for the same key.
Wrapped instance methods
Include Semafor and declare a semaphore before the method:
class ImageProcessor
include Semafor
semafor key: :image_processing, wait: 5.seconds, workers: 4, limit: 30.seconds, method: :process
def process(image)
begin
do_work(image)
ensure
release.call
end
end
end
- The key is shared through the global keyed registry, including with
Semafor.semaforandSemafor.semaphore. release.callreleases the current fiber's permit. The wrapper also releases automatically when the method exits.releaseraisesSemafor::Errorwhen there is no active acquisition.- The
semafordeclaration must appear before the wrapped method. - Only public instance methods are supported. Wrapped methods cannot use
superorprevious_defin their bodies, and any block argument must be named and type-restricted.
Handle API
Create a reusable handle for a key:
handle = Semafor.semaphore("key", wait: 5.seconds, workers: 35)
result = handle.acquire do |release, signal|
begin
do_work(signal)
ensure
release.call
end
end
handle.release
Configuration object
Named options can be captured in a reusable Semafor::Config:
config = Semafor::Config.new(workers: 35, wait: 5.seconds, limit: 2.5.seconds)
Semafor.semafor("key", config) do |release, signal|
begin
do_work(signal)
ensure
release.call
end
end
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
workers |
Int32 |
1 |
Maximum number of concurrent executions for the key. |
wait |
Time::Span? |
nil |
Maximum time to wait for a slot. nil waits indefinitely. |
limit |
Time::Span? |
nil |
Maximum time the guarded work may run. nil means no limit. |
Invalid configuration raises ArgumentError.
Execution limits and cancellation
Crystal fibers cannot be safely force-killed, so cancellation is cooperative.
When limit is set, the block runs in a spawned fiber. If it exceeds the limit, the caller raises Semafor::LimitExceededError, releases the semaphore slot, and cancels the signal. The block may continue running until it checks the signal or returns.
Use the signal inside long-running work:
Semafor.semafor("key", limit: 1.second) do |release, signal|
begin
loop do
signal.check! # raises if the limit was exceeded
do_chunk_of_work
end
ensure
release.call
end
end
The signal also supports #cancelled?, #reason, and #on_cancel.
Errors
Semafor::Error: base class for all semaphore errors.Semafor::WaitTimeoutError: waiting to acquire a slot exceededwait.Semafor::LimitExceededError: guarded work exceededlimit.Semafor::CancelledError:Signal#check!was called on a signal cancelled without a reason.
Development
crystal spec # run the specs
crystal tool format --check src spec bench
crystal run bench/semaphore_bench.cr # run the benchmark
License
MIT. See LICENSE.
cr-semafor
- 0
- 0
- 0
- 0
- 0
- about 1 hour ago
- November 3, 2026
MIT License
Tue, 03 Nov 2026 06:08:29 GMT