snerdmq 0.3.1 → 0.3.3
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/.github/workflows/ci.yml +23 -0
- data/README.md +128 -18
- data/bin/snerdmq-install +1 -1
- data/lib/snerdmq/queue.rb +82 -25
- data/snerdmq.gemspec +2 -2
- data/static/index.html +6 -6
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85e9f934b6976cdbd8c95718707762e2da87e93ea53448a51b3983b40d145f26
|
|
4
|
+
data.tar.gz: 82d99ccce4066f516da45f536e36f8539d569cdf47d1a74442b1557086830243
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b18cee9ae796c2c612d44bea8c6aa0e81725e4a27f7f4001bb4c1d66acafc89ea2766c21e573dedebf45a8689b4b2a60abc885ad0d13a632de4da3d9934e2d5c
|
|
7
|
+
data.tar.gz: 815dc8ba1eb2977aba024af95fba389753a0428d3483c3db438e8ced86fada4666c0375d7d6d4b032a3705ac2f8c9fe30aea5c82d9967adc1260c17cfcf53d86
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "main" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "main" ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Ruby
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: '3.2'
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: bundle exec rake test
|
data/README.md
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
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>💎 SnerdMQ Ruby SDK v0.3.3</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
8
|
</div>
|
|
8
9
|
|
|
9
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.
|
|
10
11
|
|
|
11
|
-
## ✨ v0.3.
|
|
12
|
+
## ✨ v0.3.3 AI Features
|
|
12
13
|
- **Smart API Rate-Limiting**: Natively tracks `rate_limit_group` execution velocity to prevent 429 "Too Many Requests" API errors.
|
|
13
14
|
- **Payload-Hashing Deduplication**: Automatically computes cryptographic hashes to drop duplicate tasks instantly.
|
|
14
15
|
- **Dynamic Float Prioritization**: A native Binary Max-Heap bypasses standard FIFO rules for high urgency tasks.
|
|
16
|
+
- **Progress Streaming & Live Dashboard**: Handlers can stream progress updates to a built-in React UI dashboard served by the SDK.
|
|
15
17
|
- **Ditch Sidekiq & Redis**: Gives your Ruby apps persistent state, automatic retries, and dead-letter queues right out of the box with zero external infrastructure.
|
|
16
18
|
- **Zero Rust Required**: Our gem installation script automatically downloads the pre-compiled C-speed Rust binary for your OS.
|
|
17
19
|
- **Thread Safe**: Uses native Ruby `Thread`s and `Mutex` locks to orchestrate I/O without blocking your main event loop.
|
|
18
20
|
|
|
19
|
-
### ⚙️ Advanced Task Configuration (v0.3.
|
|
21
|
+
### ⚙️ Advanced Task Configuration (v0.3.3)
|
|
20
22
|
To power complex AI workflows, tasks can now be configured with advanced orchestration parameters:
|
|
21
23
|
|
|
22
24
|
* **`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!
|
|
@@ -24,6 +26,7 @@ To power complex AI workflows, tasks can now be configured with advanced orchest
|
|
|
24
26
|
* **`rate_limit_group` (`String`)**: A custom string (e.g. `"openai_api"` or `"db_writes"`) that groups tasks together for backpressure control.
|
|
25
27
|
* **`max_per_minute` (`Integer`)**: Used in conjunction with `rate_limit_group`. If the queue processes more tasks in this group than the allowed limit within a 60-second rolling window, further tasks in this group are temporarily paused. This natively prevents 429 "Too Many Requests" errors when bursting third-party APIs.
|
|
26
28
|
* **`execute_at` (`String` | `Time`)**: A timestamp of when the job should be executed in the future.
|
|
29
|
+
* **`retry_after_hours` (`Float`)**: Backoff in **hours** before a failed job is retried (default `0.0`). See *Cron Jobs vs. Retryable Jobs* below.
|
|
27
30
|
* **`cron` (`String`)**: A cron expression (e.g. `"0 * * * *"`) for recurring jobs. Shorthands like `"2h"` or `"10m"` are also supported.
|
|
28
31
|
* **`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.
|
|
29
32
|
* **`max_execution_seconds` (`Integer`)**: Optional hard timeout in seconds. If execution takes longer, it's marked as failed.
|
|
@@ -83,20 +86,28 @@ end
|
|
|
83
86
|
queue.start_listening
|
|
84
87
|
puts "SnerdMQ Ruby SDK is listening for jobs..."
|
|
85
88
|
|
|
86
|
-
# 4. Enqueue a job from anywhere in your codebase
|
|
89
|
+
# 4. Enqueue a job from anywhere in your codebase
|
|
87
90
|
queue.enqueue(
|
|
88
91
|
task_id: "email-123",
|
|
89
92
|
task_type: "send_email",
|
|
90
93
|
data: { "to" => "john@wick.com", "subject" => "Continental Update" },
|
|
91
94
|
max_retries: 3,
|
|
92
|
-
retry_after_hours: 0.
|
|
95
|
+
retry_after_hours: 0.5, # Wait 30 minutes before retrying a failed job
|
|
93
96
|
rate_limit_group: "email_api",
|
|
94
|
-
max_per_minute: 100
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
max_per_minute: 100
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# 5. Need scheduling, deduplication, or serverless execution? All
|
|
101
|
+
# orchestration options are opt-in — combine only what you need:
|
|
102
|
+
queue.enqueue(
|
|
103
|
+
task_id: "email-digest-1",
|
|
104
|
+
task_type: "send_email",
|
|
105
|
+
data: { "to" => "john@wick.com", "subject" => "Daily Digest" },
|
|
106
|
+
cron: "0 8 * * *", # Run every day at 08:00
|
|
107
|
+
auto_dedupe: true, # Drop identical pending payloads
|
|
108
|
+
urgency_score: 0.99, # Float to the front of the queue
|
|
109
|
+
webhook_url: "https://api.example.com/webhook", # Execute via HTTP instead of local blocks
|
|
110
|
+
max_execution_seconds: 300 # Hard timeout
|
|
100
111
|
)
|
|
101
112
|
|
|
102
113
|
# Keep main thread alive
|
|
@@ -105,7 +116,7 @@ sleep
|
|
|
105
116
|
|
|
106
117
|
### ☠️ Dead Letter Queue (Handling Permanent Failures)
|
|
107
118
|
|
|
108
|
-
When a task fails repeatedly and exhausts its `
|
|
119
|
+
When a task fails repeatedly and exhausts its `max_retries`, the SnerdMQ daemon permanently moves it to the Dead Letter Queue. You can hook into this event to alert your team, update your database, or send a Slack message by registering a Max Retry Handler.
|
|
109
120
|
|
|
110
121
|
```ruby
|
|
111
122
|
# 5. Catch tasks that have permanently failed (Dead Letter Queue)
|
|
@@ -116,18 +127,117 @@ end
|
|
|
116
127
|
|
|
117
128
|
---
|
|
118
129
|
|
|
119
|
-
##
|
|
130
|
+
## 📊 Live Dashboard
|
|
131
|
+
|
|
132
|
+
SnerdMQ ships with a built-in **React UI dashboard** served directly by the SDK — no extra services or ports to manage in your infrastructure. It gives you a real-time window into your queue:
|
|
133
|
+
|
|
134
|
+
- **Live stats**: total enqueued, processed, and failed jobs
|
|
135
|
+
- **Recent Jobs table**: per-task status (`queued`, `active`, `completed`, `failed`, `dead_letter`), retry counts, and badges showing which features a task uses (cron / webhook / timeout)
|
|
136
|
+
- **Real-time Progress Stream**: live output from `yield_progress` calls in your handlers
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
queue = Snerdmq::SnerdQueue.new
|
|
140
|
+
|
|
141
|
+
# Start the built-in dashboard on http://localhost:9090
|
|
142
|
+
queue.start_dashboard(port: 9090)
|
|
143
|
+
|
|
144
|
+
# ... register handlers, start listening, enqueue jobs ...
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then open **http://localhost:9090** in your browser. The dashboard UI automatically uses HTTP polling to stay up to date (progress events included), and the SDK also exposes a small JSON API (`/api/stats`, `/api/tasks`, `/api/progress`) if you want to build your own tooling on top. The dashboard assets ship inside the gem — nothing extra to deploy.
|
|
148
|
+
|
|
149
|
+
> **Note:** `start_dashboard` only serves the UI — your jobs keep running whether or not the dashboard is open.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 📡 Progress Reporting
|
|
154
|
+
|
|
155
|
+
Long-running handlers can stream live updates to the Dashboard's Progress Stream (ideal for streaming LLM tokens or multi-step ETL work):
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
queue.register_handler("generate_report") do |data|
|
|
159
|
+
(1..10).each do |step|
|
|
160
|
+
do_work(step)
|
|
161
|
+
queue.yield_progress("Step #{step}/10 complete")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
> `yield_progress` must be called **inside a task handler** — the SDK tracks which task is currently executing so each update lands on the right job in the dashboard.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 🧩 Queue Topology: One Queue or Many?
|
|
171
|
+
|
|
172
|
+
### ✅ Recommended: one queue, all job types (singleton)
|
|
173
|
+
|
|
174
|
+
Each `Snerdmq::SnerdQueue` client spawns its own Rust daemon and **exclusively owns** its storage directory (`.snerdata` by default). The recommended pattern is **one client per application process**: register every job type on it and serve a single shared dashboard:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
require 'snerdmq'
|
|
178
|
+
|
|
179
|
+
# ONE queue client for the whole app
|
|
180
|
+
queue = Snerdmq::SnerdQueue.new
|
|
181
|
+
|
|
182
|
+
# Job type #1: image processing
|
|
183
|
+
queue.register_handler("process_image") do |data|
|
|
184
|
+
puts "Processing image: #{data['image_id']}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Job type #2: OTP emails — same queue, same daemon
|
|
188
|
+
queue.register_handler("send_otp_email") do |data|
|
|
189
|
+
puts "Sending OTP to: #{data['to']}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
queue.start_listening
|
|
193
|
+
|
|
194
|
+
# Both job types flow through the exact same queue
|
|
195
|
+
queue.enqueue(task_id: "img-1", task_type: "process_image", data: { "image_id" => "abc123" }, max_retries: 3, retry_after_hours: 0.5)
|
|
196
|
+
queue.enqueue(task_id: "otp-1", task_type: "send_otp_email", data: { "to" => "john@wick.com" }, max_retries: 3, retry_after_hours: 0.5)
|
|
197
|
+
|
|
198
|
+
# ONE dashboard shows every job type
|
|
199
|
+
queue.start_dashboard(port: 8080)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
All job types share everything: the same persistent job log, retry/DLQ pipeline, rate-limit state, stats — and one dashboard at `http://localhost:8080` showing all of them.
|
|
203
|
+
|
|
204
|
+
### 🚫 Same storage twice = fails fast
|
|
120
205
|
|
|
121
|
-
|
|
206
|
+
The daemon takes an **exclusive OS-level lock** on its storage directory at startup. A second client on the same storage fails instead of silently double-executing your jobs:
|
|
122
207
|
|
|
123
|
-
|
|
208
|
+
```ruby
|
|
209
|
+
first = Snerdmq::SnerdQueue.new # ✅ owns .snerdata
|
|
210
|
+
second = Snerdmq::SnerdQueue.new # ❌ daemon refuses to start:
|
|
211
|
+
# "Another daemon is already running on storage '.snerdata'"
|
|
212
|
+
```
|
|
213
|
+
|
|
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).
|
|
215
|
+
|
|
216
|
+
### 🔀 Need multiple queues? Give each one its own storage
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
images = Snerdmq::SnerdQueue.new(storage_path: ".snerdata-images")
|
|
220
|
+
emails = Snerdmq::SnerdQueue.new(storage_path: ".snerdata-emails")
|
|
221
|
+
|
|
222
|
+
images.start_dashboard(port: 8080) # separate dashboards, so separate ports
|
|
223
|
+
emails.start_dashboard(port: 8081)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Now you have two fully independent engines: separate job logs, separate rate-limit state, separate dashboards. Only split when you actually need isolation (different teams, different retention, independent monitoring) — otherwise the singleton is simpler and recommended.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 🌍 Advanced: Distributed Scaling
|
|
231
|
+
|
|
232
|
+
Because the daemon exclusively locks its storage directory, scaling horizontally means **one queue per server**, each with its own storage. Your load balancer routes requests across servers, and every server processes the jobs it enqueued:
|
|
124
233
|
|
|
125
234
|
```ruby
|
|
126
235
|
require 'snerdmq'
|
|
127
236
|
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
queue = Snerdmq::SnerdQueue.new(storage_path: "/mnt/aws-efs-shared-drive/snerd_tasks.log")
|
|
237
|
+
# Each server runs its own daemon on its own storage dir (local disk works fine)
|
|
238
|
+
queue = Snerdmq::SnerdQueue.new(storage_path: "/var/data/snerd") # per-server storage
|
|
131
239
|
```
|
|
132
240
|
|
|
241
|
+
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
|
+
|
|
133
243
|
*Built with ❤️ for John Wick tier engineering.*
|
data/bin/snerdmq-install
CHANGED
data/lib/snerdmq/queue.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'thread'
|
|
3
3
|
require 'timeout'
|
|
4
|
+
require 'time'
|
|
4
5
|
|
|
5
6
|
module Snerdmq
|
|
6
7
|
class SnerdQueue
|
|
@@ -134,18 +135,6 @@ module Snerdmq
|
|
|
134
135
|
@io.close if @io && !@io.closed?
|
|
135
136
|
end
|
|
136
137
|
|
|
137
|
-
private
|
|
138
|
-
|
|
139
|
-
def send_message(msg)
|
|
140
|
-
@stdin_mutex.synchronize do
|
|
141
|
-
return if @shutting_down || @io.nil? || @io.closed?
|
|
142
|
-
@io.puts(msg.to_json)
|
|
143
|
-
@io.flush
|
|
144
|
-
end
|
|
145
|
-
rescue Errno::EPIPE
|
|
146
|
-
# Broken pipe if the daemon died unexpectedly
|
|
147
|
-
end
|
|
148
|
-
|
|
149
138
|
def listen_to_stdout
|
|
150
139
|
@io.each_line do |line|
|
|
151
140
|
next if line.strip.empty?
|
|
@@ -178,6 +167,9 @@ module Snerdmq
|
|
|
178
167
|
warn "[Snerd] Error from engine: #{msg['message']}"
|
|
179
168
|
end
|
|
180
169
|
elsif msg["action"] == "progress"
|
|
170
|
+
# Persist progress events so the dashboard (which falls back to
|
|
171
|
+
# HTTP polling in Ruby) can display them in the Progress Stream.
|
|
172
|
+
append_progress_event(msg)
|
|
181
173
|
@ws_mutex.synchronize do
|
|
182
174
|
@ws_clients.each do |ws|
|
|
183
175
|
ws.send(msg.to_json)
|
|
@@ -275,6 +267,7 @@ module Snerdmq
|
|
|
275
267
|
|
|
276
268
|
def start_dashboard(port: 8080)
|
|
277
269
|
require 'rack'
|
|
270
|
+
require 'puma'
|
|
278
271
|
require 'faye/websocket'
|
|
279
272
|
require 'json'
|
|
280
273
|
|
|
@@ -305,26 +298,43 @@ module Snerdmq
|
|
|
305
298
|
else
|
|
306
299
|
return [404, {}, ['Dashboard UI not found']]
|
|
307
300
|
end
|
|
301
|
+
elsif req.get? && req.path == '/api/progress'
|
|
302
|
+
events = []
|
|
303
|
+
progress_path = File.join(@storage_path || './.snerdata', 'progress_events.log')
|
|
304
|
+
if File.exist?(progress_path)
|
|
305
|
+
File.readlines(progress_path).last(100).each do |line|
|
|
306
|
+
begin
|
|
307
|
+
ev = JSON.parse(line)
|
|
308
|
+
events << ev if ev.is_a?(Hash) && ev['ts']
|
|
309
|
+
rescue
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
return [200, { 'Content-Type' => 'application/json' }.merge(cors_headers), [events.to_json]]
|
|
308
314
|
elsif req.get? && req.path == '/api/stats'
|
|
309
315
|
stats = { enqueued: 0, processed: 0, failed: 0 }
|
|
316
|
+
tasks_map = {}
|
|
310
317
|
tasks_path = File.join(@storage_path || './.snerdata', 'tasks', 'tasks.log')
|
|
311
318
|
if File.exist?(tasks_path)
|
|
312
319
|
File.readlines(tasks_path).each do |line|
|
|
313
320
|
next if line.strip.empty?
|
|
314
321
|
begin
|
|
315
322
|
t = JSON.parse(line)
|
|
316
|
-
|
|
317
|
-
if t['deletedAt']
|
|
318
|
-
if t['lastJobError']
|
|
319
|
-
stats[:failed] += 1
|
|
320
|
-
else
|
|
321
|
-
stats[:processed] += 1
|
|
322
|
-
end
|
|
323
|
-
end
|
|
323
|
+
tasks_map[t['taskId']] = t if t['taskId']
|
|
324
324
|
rescue
|
|
325
325
|
end
|
|
326
326
|
end
|
|
327
327
|
end
|
|
328
|
+
tasks_map.values.each do |t|
|
|
329
|
+
stats[:enqueued] += 1
|
|
330
|
+
if t['deletedAt']
|
|
331
|
+
if t['LastJobError']
|
|
332
|
+
stats[:failed] += 1
|
|
333
|
+
else
|
|
334
|
+
stats[:processed] += 1
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
328
338
|
return [200, { 'Content-Type' => 'application/json' }.merge(cors_headers), [stats.to_json]]
|
|
329
339
|
elsif req.get? && req.path == '/api/tasks'
|
|
330
340
|
tasks_map = {}
|
|
@@ -343,9 +353,23 @@ module Snerdmq
|
|
|
343
353
|
formatted = []
|
|
344
354
|
tasks_map.values.each do |t|
|
|
345
355
|
if t['deletedAt']
|
|
346
|
-
|
|
356
|
+
if t['LastJobError'] && (t['retryCount'] || 0) >= (t['maxRetries'] || 3)
|
|
357
|
+
status = 'dead_letter'
|
|
358
|
+
elsif t['LastJobError']
|
|
359
|
+
status = 'failed'
|
|
360
|
+
else
|
|
361
|
+
status = 'completed'
|
|
362
|
+
end
|
|
363
|
+
elsif t['LastJobError']
|
|
364
|
+
status = 'failed'
|
|
347
365
|
else
|
|
348
|
-
status =
|
|
366
|
+
status = 'queued'
|
|
367
|
+
if t['executeAt']
|
|
368
|
+
begin
|
|
369
|
+
status = 'active' if Time.parse(t['executeAt']) <= Time.now
|
|
370
|
+
rescue
|
|
371
|
+
end
|
|
372
|
+
end
|
|
349
373
|
end
|
|
350
374
|
formatted << {
|
|
351
375
|
id: t['taskId'],
|
|
@@ -354,7 +378,10 @@ module Snerdmq
|
|
|
354
378
|
progress: 0,
|
|
355
379
|
retryCount: t['retryCount'] || 0,
|
|
356
380
|
maxRetries: t['maxRetries'] || 3,
|
|
357
|
-
retryAfterTime: t['retryAfterTime']
|
|
381
|
+
retryAfterTime: t['retryAfterTime'],
|
|
382
|
+
cronExpression: t['cronExpression'],
|
|
383
|
+
webhookUrl: t['webhookUrl'],
|
|
384
|
+
maxExecutionSeconds: t['maxExecutionSeconds']
|
|
358
385
|
}
|
|
359
386
|
end
|
|
360
387
|
return [200, { 'Content-Type' => 'application/json' }.merge(cors_headers), [formatted.first(50).to_json]]
|
|
@@ -365,9 +392,39 @@ module Snerdmq
|
|
|
365
392
|
|
|
366
393
|
Thread.new do
|
|
367
394
|
puts "[Snerd] Dashboard running on http://localhost:#{port}"
|
|
368
|
-
|
|
395
|
+
server = Puma::Server.new(app)
|
|
396
|
+
server.add_tcp_listener('0.0.0.0', port)
|
|
397
|
+
server.run
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
private
|
|
402
|
+
|
|
403
|
+
def append_progress_event(msg)
|
|
404
|
+
dir = @storage_path || './.snerdata'
|
|
405
|
+
return unless File.directory?(dir)
|
|
406
|
+
path = File.join(dir, 'progress_events.log')
|
|
407
|
+
|
|
408
|
+
event = { ts: Time.now.to_f, task_id: msg['task_id'], data: msg['data'] }.to_json
|
|
409
|
+
File.open(path, 'a') { |f| f.puts(event) }
|
|
410
|
+
|
|
411
|
+
# Keep the file bounded: retain only the most recent events
|
|
412
|
+
if File.size(path) > 512 * 1024
|
|
413
|
+
lines = File.readlines(path).map(&:strip).reject(&:empty?)
|
|
414
|
+
File.write(path, lines.last(200).join("\n") + "\n")
|
|
369
415
|
end
|
|
416
|
+
rescue
|
|
417
|
+
# Never break the listener loop over progress persistence
|
|
370
418
|
end
|
|
371
419
|
|
|
420
|
+
def send_message(msg)
|
|
421
|
+
@stdin_mutex.synchronize do
|
|
422
|
+
return if @shutting_down || @io.nil? || @io.closed?
|
|
423
|
+
@io.puts(msg.to_json)
|
|
424
|
+
@io.flush
|
|
425
|
+
end
|
|
426
|
+
rescue Errno::EPIPE, Errno::EIO, IOError
|
|
427
|
+
# Broken pipe / IO error if the daemon died unexpectedly
|
|
428
|
+
end
|
|
372
429
|
end
|
|
373
|
-
end
|
|
430
|
+
end
|
data/snerdmq.gemspec
CHANGED
|
@@ -3,13 +3,13 @@ $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.
|
|
6
|
+
spec.version = "0.3.3"
|
|
7
7
|
spec.authors = ["Greyhands2"]
|
|
8
8
|
spec.email = ["developer@example.com"]
|
|
9
9
|
|
|
10
10
|
spec.summary = "A zero-config, persistent background job queue for Ruby."
|
|
11
11
|
spec.description = "The official Ruby SDK for the SnerdMQ Rust daemon. Execute robust, lightning-fast background jobs without Redis."
|
|
12
|
-
spec.homepage = "https://github.com/
|
|
12
|
+
spec.homepage = "https://github.com/speed-nerd/snerdmq-ruby"
|
|
13
13
|
spec.license = "MIT"
|
|
14
14
|
|
|
15
15
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|