snerdmq 0.3.3 → 0.4.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.
- checksums.yaml +4 -4
- data/.snerdata/.membership.lock +0 -0
- data/.snerdata/membership.json +6 -0
- data/.snerdata/shard-0/.lock +1 -0
- data/.snerdata/shard-0/tasks/tasks.log +4 -0
- data/README.md +135 -6
- data/lib/snerdmq/queue.rb +11 -3
- data/snerdmq.gemspec +1 -1
- metadata +6 -3
- data/.snerdata/tasks/tasks.log +0 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45cda2f70cd673e8e419afd54eb65adc3aee234eb91c61ccae9f12e535dbe247
|
|
4
|
+
data.tar.gz: e3720631cf201ab202b432c81e73d93933127faddfc07c40e27cde58bab199ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35a3dde0f3b2febdf5c78787b2419f09e8af3d4581086e46c376c96486a72f9c8612d6f3a5d14be7188d342c74b1947450576e835b69bdb3f0d4aa4dc6eb8f0f
|
|
7
|
+
data.tar.gz: f77e67cf6d8e76b82a7cfeabbb881aaa781ae59214ef40d96538512b3110780055420cfc80c3eff874a59fe496ceb9d985de2aa6171b5865d75fb8acf8e22db0
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
8643
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"taskId":"ruby-job-1","retryCount":0,"maxRetries":3,"retryAfterHours":0.0,"retryAfterTime":"2026-08-14T09:43:50.902732Z","taskData":"{\"user_id\":\"ruby_master\",\"message\":\"matz\"}","taskType":"test_ruby_job"}
|
|
2
|
+
{"taskId":"ruby-job-1","retryCount":0,"maxRetries":3,"retryAfterHours":0.0,"retryAfterTime":"2026-08-16T10:42:24.239760Z","taskData":"{\"user_id\":\"ruby_master\",\"message\":\"matz\"}","taskType":"test_ruby_job","executeAt":"2026-08-16T10:42:24.239760Z"}
|
|
3
|
+
{"taskId":"ruby-job-1","retryCount":0,"maxRetries":3,"retryAfterHours":0.0,"retryAfterTime":"2026-08-16T10:42:24.239760Z","taskData":"{\"user_id\":\"ruby_master\",\"message\":\"matz\"}","taskType":"test_ruby_job","deletedAt":"2026-09-25T10:43:04.524511Z","executeAt":"2026-08-16T10:42:24.239760Z"}
|
|
4
|
+
{"taskId":"ruby-job-1","retryCount":0,"maxRetries":3,"retryAfterHours":0.0,"retryAfterTime":"2026-09-25T10:43:04.605292Z","taskData":"{\"user_id\":\"ruby_master\",\"message\":\"matz\"}","taskType":"test_ruby_job","executeAt":"2026-09-25T10:43:04.605292Z"}
|
data/README.md
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<img src="./assets/Designer-9.png" height="120" alt="SnerdMQ Ruby Logo" />
|
|
3
|
-
<h1>💎 SnerdMQ Ruby SDK v0.
|
|
3
|
+
<h1>💎 SnerdMQ Ruby SDK v0.4.1</h1>
|
|
4
4
|
<p>A zero-config, C-speed background job queue for Ruby. Ditch Redis and Sidekiq for lightweight, persistent background jobs.</p>
|
|
5
5
|
|
|
6
6
|
[](https://badge.fury.io/rb/snerdmq)
|
|
7
|
-
[](https://speed-nerd.github.io/docs/)
|
|
7
|
+
[](https://speed-nerd.github.io/docs/sdks/ruby/)
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
This is the official Ruby SDK wrapper for **SnerdMQ**. It handles all JSON-RPC communication and `IO.popen` orchestration so you can write lightning-fast background jobs without managing any external databases like Redis or Postgres.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
> 📚 **Full Documentation & Advanced Features:** Check out the [official Ruby SDK documentation](https://speed-nerd.github.io/docs/sdks/ruby/) on our docs site!
|
|
13
|
+
|
|
14
|
+
## ✨ v0.4.1 AI Features
|
|
15
|
+
- **Worker Pools**: Prevent slow generative AI tasks from starving fast DB tasks by dedicating workers to specific pools (e.g. `"urgent"`).
|
|
16
|
+
- **Sharded Queues**: Distribute load across multiple queue nodes safely using file-backed lock sharding (`max_local_shards`).
|
|
13
17
|
- **Smart API Rate-Limiting**: Natively tracks `rate_limit_group` execution velocity to prevent 429 "Too Many Requests" API errors.
|
|
14
18
|
- **Payload-Hashing Deduplication**: Automatically computes cryptographic hashes to drop duplicate tasks instantly.
|
|
15
19
|
- **Dynamic Float Prioritization**: A native Binary Max-Heap bypasses standard FIFO rules for high urgency tasks.
|
|
20
|
+
- **Job Chaining (DAGs)**: Define complex workflow dependencies natively. Tasks wait in a blocked state until their parent tasks succeed.
|
|
16
21
|
- **Progress Streaming & Live Dashboard**: Handlers can stream progress updates to a built-in React UI dashboard served by the SDK.
|
|
17
22
|
- **Ditch Sidekiq & Redis**: Gives your Ruby apps persistent state, automatic retries, and dead-letter queues right out of the box with zero external infrastructure.
|
|
18
23
|
- **Zero Rust Required**: Our gem installation script automatically downloads the pre-compiled C-speed Rust binary for your OS.
|
|
19
24
|
- **Thread Safe**: Uses native Ruby `Thread`s and `Mutex` locks to orchestrate I/O without blocking your main event loop.
|
|
20
25
|
|
|
21
|
-
### ⚙️ Advanced Task Configuration (v0.
|
|
26
|
+
### ⚙️ Advanced Task Configuration (v0.4.1)
|
|
22
27
|
To power complex AI workflows, tasks can now be configured with advanced orchestration parameters:
|
|
23
28
|
|
|
24
29
|
* **`auto_dedupe` (`true/false`)**: If set to `true`, the daemon computes a cryptographic hash of the `task_type` and `data`. If an identical payload is currently sitting in the queue pending execution, this new task is silently dropped. Excellent for preventing duplicate generative AI requests from trigger-happy users!
|
|
@@ -30,6 +35,8 @@ To power complex AI workflows, tasks can now be configured with advanced orchest
|
|
|
30
35
|
* **`cron` (`String`)**: A cron expression (e.g. `"0 * * * *"`) for recurring jobs. Shorthands like `"2h"` or `"10m"` are also supported.
|
|
31
36
|
* **`webhook_url` (`String`)**: By providing a webhook URL, SnerdMQ will completely bypass your local Ruby blocks and dispatch the task payload via an HTTP POST request directly to the specified URL.
|
|
32
37
|
* **`max_execution_seconds` (`Integer`)**: Optional hard timeout in seconds. If execution takes longer, it's marked as failed.
|
|
38
|
+
* **`trigger_after_ids` (`Array` of `String`)**: A list of parent task IDs that must complete successfully before this task is allowed to dispatch. Enables complex DAG workflows natively within the queue.
|
|
39
|
+
* **`pool` (`String`)**: Dedicate this task to a specific worker pool (e.g. `"urgent"`).
|
|
33
40
|
|
|
34
41
|
### Note on Hard Timeouts (`max_execution_seconds`)
|
|
35
42
|
When `max_execution_seconds` is provided, the Ruby SDK wraps the execution of your handler in a `Timeout.timeout` block. If the task takes longer than the timeout, a `Timeout::Error` is raised and the execution will be marked as failed. The background Rust daemon also enforces this timeout at the IPC level.
|
|
@@ -107,7 +114,9 @@ queue.enqueue(
|
|
|
107
114
|
auto_dedupe: true, # Drop identical pending payloads
|
|
108
115
|
urgency_score: 0.99, # Float to the front of the queue
|
|
109
116
|
webhook_url: "https://api.example.com/webhook", # Execute via HTTP instead of local blocks
|
|
110
|
-
max_execution_seconds: 300
|
|
117
|
+
max_execution_seconds: 300, # Hard timeout
|
|
118
|
+
trigger_after_ids: ["parent-123"], # Wait for parent tasks to complete
|
|
119
|
+
pool: "urgent" # Dedicate to a specific worker pool
|
|
111
120
|
)
|
|
112
121
|
|
|
113
122
|
# Keep main thread alive
|
|
@@ -211,7 +220,11 @@ second = Snerdmq::SnerdQueue.new # ❌ daemon refuses to start:
|
|
|
211
220
|
# "Another daemon is already running on storage '.snerdata'"
|
|
212
221
|
```
|
|
213
222
|
|
|
214
|
-
This applies across processes too — with **Puma/Unicorn clustered workers, every worker is a separate process
|
|
223
|
+
This applies across processes too — with **Puma/Unicorn clustered workers, every worker is a separate process**. To safely scale on the same disk without double-executing jobs, you must initialize the daemon with `max_local_shards`:
|
|
224
|
+
```ruby
|
|
225
|
+
# SnerdMQ will partition the .snerdata locks across shards
|
|
226
|
+
queue = Snerdmq::SnerdQueue.new(max_local_shards: 4, max_workers: { "urgent" => 5 })
|
|
227
|
+
```
|
|
215
228
|
|
|
216
229
|
### 🔀 Need multiple queues? Give each one its own storage
|
|
217
230
|
|
|
@@ -240,4 +253,120 @@ queue = Snerdmq::SnerdQueue.new(storage_path: "/var/data/snerd") # per-server st
|
|
|
240
253
|
|
|
241
254
|
A shared network drive (AWS EFS or NFS) is still a good home for that storage when a single instance needs durable state — e.g. a container that restarts but must keep its queue. Native OS file locking (`flock`) keeps writes safe — no Redis required.
|
|
242
255
|
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 🚀 Advanced Orchestration
|
|
260
|
+
|
|
261
|
+
### 🏊 Worker Pools
|
|
262
|
+
|
|
263
|
+
SnerdMQ supports dedicating worker resources to specific tasks so that slow AI generation tasks don't starve fast database updates.
|
|
264
|
+
|
|
265
|
+
In the SDK, simply assign a pool name when enqueueing the task using the `pool` parameter. When running the daemon, you can allocate concurrent workers per pool using the environment variable `SNERD_POOLS="default:100,urgent:50"`.
|
|
266
|
+
|
|
267
|
+
### 🔗 Job Chaining (DAGs)
|
|
268
|
+
|
|
269
|
+
You can define complex workflow dependencies natively. Tasks will wait in a blocked state until their parent tasks successfully complete.
|
|
270
|
+
|
|
271
|
+
Simply pass an array of parent task IDs to the `trigger_after_ids` parameter when enqueueing. This easily unlocks Fan-In and Linear workflows natively within the queue.
|
|
272
|
+
|
|
273
|
+
### 🍕 Sharded Queues (Scaling Out)
|
|
274
|
+
|
|
275
|
+
SnerdMQ natively supports distributed execution across multiple servers while acting as a single logical queue. Just mount a shared storage drive (like AWS EFS) and boot multiple daemons. They will automatically lock and negotiate ownership of shards. No config required in the SDK for enqueueing! Just tell the daemon how many shards to claim on boot:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
# Boot a multi-tenant daemon that owns up to 4 shards locally
|
|
279
|
+
queue = SnerdQueue.new(max_local_shards: 4)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# 1. Worker Pools: Route tasks to the 'urgent' pool
|
|
284
|
+
queue.enqueue(
|
|
285
|
+
task_id: 'payment-job',
|
|
286
|
+
task_type: 'process_payment',
|
|
287
|
+
data: { amount: 100 },
|
|
288
|
+
pool: 'urgent'
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# 2. Job Chaining: Block execution until parents succeed
|
|
292
|
+
queue.enqueue(
|
|
293
|
+
task_id: 'final-job',
|
|
294
|
+
task_type: 'send_report',
|
|
295
|
+
data: { id: 1 },
|
|
296
|
+
trigger_after_ids: ['parent-job-1', 'parent-job-2']
|
|
297
|
+
)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
### 🕒 Cron & Scheduled Jobs
|
|
302
|
+
```ruby
|
|
303
|
+
# Run every day at 08:00
|
|
304
|
+
queue.enqueue(
|
|
305
|
+
task_id: 'daily-digest',
|
|
306
|
+
task_type: 'send_email',
|
|
307
|
+
data: { template: 'daily' },
|
|
308
|
+
cron: '0 8 * * *'
|
|
309
|
+
)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 🛑 Hard Timeouts
|
|
313
|
+
```ruby
|
|
314
|
+
# Forcefully kill if running > 5 mins
|
|
315
|
+
queue.enqueue(
|
|
316
|
+
task_id: 'risky-task',
|
|
317
|
+
task_type: 'process_data',
|
|
318
|
+
data: {},
|
|
319
|
+
max_execution_seconds: 300
|
|
320
|
+
)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 🌐 Webhook Callbacks
|
|
324
|
+
```ruby
|
|
325
|
+
# Execute via HTTP instead of local handlers
|
|
326
|
+
queue.enqueue(
|
|
327
|
+
task_id: 'serverless-task',
|
|
328
|
+
task_type: 'resize_image',
|
|
329
|
+
data: { img: 'cat.jpg' },
|
|
330
|
+
webhook_url: 'https://api.example.com/webhooks/snerdmq'
|
|
331
|
+
)
|
|
332
|
+
```
|
|
333
|
+
|
|
243
334
|
*Built with ❤️ for John Wick tier engineering.*
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
## Architecture Best Practices
|
|
338
|
+
|
|
339
|
+
When building production applications with SnerdMQ, it is recommended to initialize the queue as a Singleton, isolate your domain workers into separate files/functions, use Dead Letter Queues (DLQ) for failed tasks via `RegisterMaxRetryHandler`, and ensure manual graceful shutdown. The embedded Dashboard UI can also be easily served from the same instance.
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
require 'snerdmq'
|
|
343
|
+
|
|
344
|
+
queue = SnerdQueue.new(storage_path: "./.snerdata")
|
|
345
|
+
|
|
346
|
+
def init_email_workers(queue)
|
|
347
|
+
queue.register_handler('send_email') do |data|
|
|
348
|
+
puts "Sending email to #{data['email']}..."
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
queue.register_max_retry_handler('send_email') do |data|
|
|
352
|
+
puts "Email to #{data['email']} failed permanently. Dead letter processing..."
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def init_image_workers(queue)
|
|
357
|
+
queue.register_handler('process_image') do |data|
|
|
358
|
+
puts "Processing image #{data['imageId']}..."
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
init_email_workers(queue)
|
|
363
|
+
init_image_workers(queue)
|
|
364
|
+
|
|
365
|
+
queue.start_dashboard(8080)
|
|
366
|
+
|
|
367
|
+
# Trap signals for graceful shutdown
|
|
368
|
+
trap('INT') { queue.shutdown; exit }
|
|
369
|
+
trap('TERM') { queue.shutdown; exit }
|
|
370
|
+
|
|
371
|
+
queue.start_listening
|
|
372
|
+
```
|
data/lib/snerdmq/queue.rb
CHANGED
|
@@ -5,9 +5,11 @@ require 'time'
|
|
|
5
5
|
|
|
6
6
|
module Snerdmq
|
|
7
7
|
class SnerdQueue
|
|
8
|
-
def initialize(binary_path: nil, storage_path: nil)
|
|
8
|
+
def initialize(binary_path: nil, storage_path: nil, max_local_shards: nil, max_workers: nil)
|
|
9
9
|
@binary_path = binary_path
|
|
10
10
|
@storage_path = storage_path
|
|
11
|
+
@max_local_shards = max_local_shards
|
|
12
|
+
@max_workers = max_workers
|
|
11
13
|
|
|
12
14
|
if @binary_path.nil?
|
|
13
15
|
ext = RbConfig::CONFIG['host_os'].match?(/mswin|msys|mingw|cygwin|bccwin|wince|emc/) ? '.exe' : ''
|
|
@@ -56,8 +58,12 @@ module Snerdmq
|
|
|
56
58
|
args = []
|
|
57
59
|
args << @storage_path if @storage_path
|
|
58
60
|
|
|
61
|
+
env = {}
|
|
62
|
+
env["SNERD_MAX_SHARDS"] = @max_local_shards.to_s if @max_local_shards
|
|
63
|
+
env["SNERD_MAX_WORKERS"] = @max_workers.to_s if @max_workers
|
|
64
|
+
|
|
59
65
|
# Open a bidirectional pipe to the Rust daemon
|
|
60
|
-
@io = IO.popen([@binary_path] + args, "r+")
|
|
66
|
+
@io = IO.popen(env, [@binary_path] + args, "r+")
|
|
61
67
|
|
|
62
68
|
# Re-register all existing handlers
|
|
63
69
|
@handlers_mutex.synchronize do
|
|
@@ -74,7 +80,7 @@ module Snerdmq
|
|
|
74
80
|
end
|
|
75
81
|
end
|
|
76
82
|
|
|
77
|
-
def enqueue(task_id:, task_type:, data:, max_retries: 3, retry_after_hours: 0.0, rate_limit_group: nil, max_per_minute: nil, auto_dedupe: false, urgency_score: nil, execute_at: nil, cron: nil, webhook_url: nil, max_execution_seconds: nil)
|
|
83
|
+
def enqueue(task_id:, task_type:, data:, max_retries: 3, retry_after_hours: 0.0, rate_limit_group: nil, max_per_minute: nil, auto_dedupe: false, urgency_score: nil, execute_at: nil, cron: nil, webhook_url: nil, max_execution_seconds: nil, trigger_after_ids: nil, pool: nil)
|
|
78
84
|
raise "[Snerd] Cannot enqueue task: Queue is not running. Call start_listening first." if @io.nil? || @shutting_down
|
|
79
85
|
|
|
80
86
|
payload = {
|
|
@@ -97,6 +103,8 @@ module Snerdmq
|
|
|
97
103
|
payload[:cron] = cron if cron
|
|
98
104
|
payload[:webhook_url] = webhook_url if webhook_url
|
|
99
105
|
payload[:max_execution_seconds] = max_execution_seconds if max_execution_seconds
|
|
106
|
+
payload[:trigger_after_ids] = trigger_after_ids if trigger_after_ids
|
|
107
|
+
payload[:pool] = pool if pool
|
|
100
108
|
|
|
101
109
|
cond = ConditionVariable.new
|
|
102
110
|
result = nil
|
data/snerdmq.gemspec
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: snerdmq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Greyhands2
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -106,7 +106,10 @@ files:
|
|
|
106
106
|
- ".github/PULL_REQUEST_TEMPLATE.md"
|
|
107
107
|
- ".github/workflows/ci.yml"
|
|
108
108
|
- ".gitignore"
|
|
109
|
-
- ".snerdata
|
|
109
|
+
- ".snerdata/.membership.lock"
|
|
110
|
+
- ".snerdata/membership.json"
|
|
111
|
+
- ".snerdata/shard-0/.lock"
|
|
112
|
+
- ".snerdata/shard-0/tasks/tasks.log"
|
|
110
113
|
- CONTRIBUTING.md
|
|
111
114
|
- Gemfile
|
|
112
115
|
- Gemfile.lock
|
data/.snerdata/tasks/tasks.log
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
{"taskId":"ruby-job-1","retryCount":0,"maxRetries":3,"retryAfterHours":0.0,"retryAfterTime":"2026-08-14T09:43:50.902732Z","taskData":"{\"user_id\":\"ruby_master\",\"message\":\"matz\"}","taskType":"test_ruby_job"}
|
|
2
|
-
{"taskId":"ruby-job-1","retryCount":0,"maxRetries":3,"retryAfterHours":0.0,"retryAfterTime":"2026-08-16T10:42:24.239760Z","taskData":"{\"user_id\":\"ruby_master\",\"message\":\"matz\"}","taskType":"test_ruby_job","executeAt":"2026-08-16T10:42:24.239760Z"}
|