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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85e9f934b6976cdbd8c95718707762e2da87e93ea53448a51b3983b40d145f26
4
- data.tar.gz: 82d99ccce4066f516da45f536e36f8539d569cdf47d1a74442b1557086830243
3
+ metadata.gz: 45cda2f70cd673e8e419afd54eb65adc3aee234eb91c61ccae9f12e535dbe247
4
+ data.tar.gz: e3720631cf201ab202b432c81e73d93933127faddfc07c40e27cde58bab199ae
5
5
  SHA512:
6
- metadata.gz: b18cee9ae796c2c612d44bea8c6aa0e81725e4a27f7f4001bb4c1d66acafc89ea2766c21e573dedebf45a8689b4b2a60abc885ad0d13a632de4da3d9934e2d5c
7
- data.tar.gz: 815dc8ba1eb2977aba024af95fba389753a0428d3483c3db438e8ced86fada4666c0375d7d6d4b032a3705ac2f8c9fe30aea5c82d9967adc1260c17cfcf53d86
6
+ metadata.gz: 35a3dde0f3b2febdf5c78787b2419f09e8af3d4581086e46c376c96486a72f9c8612d6f3a5d14be7188d342c74b1947450576e835b69bdb3f0d4aa4dc6eb8f0f
7
+ data.tar.gz: f77e67cf6d8e76b82a7cfeabbb881aaa781ae59214ef40d96538512b3110780055420cfc80c3eff874a59fe496ceb9d985de2aa6171b5865d75fb8acf8e22db0
File without changes
@@ -0,0 +1,6 @@
1
+ {
2
+ "queue": "snerdmq-daemon",
3
+ "shards": 1,
4
+ "version": 2,
5
+ "claims": {}
6
+ }
@@ -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.3</h1>
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
  [![Gem Version](https://badge.fury.io/rb/snerdmq.svg)](https://badge.fury.io/rb/snerdmq)
7
- [![Docs](https://img.shields.io/badge/docs-speed--nerd.github.io-blue)](https://speed-nerd.github.io/docs/)
7
+ [![Docs](https://img.shields.io/badge/docs-speed--nerd.github.io-blue)](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
- ## ✨ v0.3.3 AI Features
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.3.3)
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 # Hard timeout
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** that spawns its own daemon, so each worker needs its own `storage_path` (or run a single dedicated worker process for jobs).
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
@@ -3,7 +3,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
 
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = "snerdmq"
6
- spec.version = "0.3.3"
6
+ spec.version = "0.4.1"
7
7
  spec.authors = ["Greyhands2"]
8
8
  spec.email = ["developer@example.com"]
9
9
 
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.3.3
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-08-19 00:00:00.000000000 Z
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/tasks/tasks.log"
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
@@ -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"}