honker 0.1.0 → 0.1.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/README.md +25 -60
- data/honker.gemspec +1 -1
- data/lib/honker/scheduler.rb +55 -18
- data/lib/honker/version.rb +1 -1
- data/lib/honker.rb +10 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41df996d169333da2b22d08dbb613537c16c1d92519e86558986b11737406bda
|
|
4
|
+
data.tar.gz: c052dd0d545ea520914e650b488138b1512527f1278c3e7c4bf72932c40df055
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 414c0d65e26bb76e4a6aad15674b32a448fefc75ca13ad060cc570fa86e5b0180efa5875859e08def6afd0b9c92f9eb42f26d7420478abc7a50d9ec9ab133550
|
|
7
|
+
data.tar.gz: 78c16c3beea3611f590602f338ec7aa6e5d27edb9a2ffa81b8267c64a835fd129f07562bbbf6c67d6d4fb386421b5a2d965c3a7454c26f21afe76ee7a29ade36
|
data/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# honker (Ruby)
|
|
2
2
|
|
|
3
|
-
Ruby binding for [Honker](https://honker
|
|
3
|
+
Ruby binding for [Honker](https://github.com/russellromney/honker): durable queues, streams, pub/sub, and time-trigger scheduling on SQLite.
|
|
4
|
+
|
|
5
|
+
Full docs:
|
|
6
|
+
|
|
7
|
+
- [Main repo](https://github.com/russellromney/honker)
|
|
8
|
+
- [Docs](https://honker.dev)
|
|
4
9
|
|
|
5
10
|
## Install
|
|
6
11
|
|
|
@@ -8,81 +13,41 @@ Ruby binding for [Honker](https://honker.dev) — durable queues, streams, pub/s
|
|
|
8
13
|
gem "honker"
|
|
9
14
|
```
|
|
10
15
|
|
|
11
|
-
You
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
git clone https://github.com/russellromney/honker
|
|
15
|
-
cd honker
|
|
16
|
-
cargo build --release -p honker-extension
|
|
17
|
-
# → target/release/libhonker_extension.{dylib,so}
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Requirements
|
|
21
|
-
|
|
22
|
-
- Ruby 3.0+
|
|
23
|
-
- `sqlite3` gem ≥ 1.7 (pulled in automatically)
|
|
16
|
+
You also need the Honker SQLite extension from the main repo.
|
|
24
17
|
|
|
25
18
|
## Quick start
|
|
26
19
|
|
|
27
20
|
```ruby
|
|
28
21
|
require "honker"
|
|
29
22
|
|
|
30
|
-
db = Honker::Database.new("app.db", extension_path: "./
|
|
31
|
-
q
|
|
23
|
+
db = Honker::Database.new("app.db", extension_path: "./libhonker_ext.dylib")
|
|
24
|
+
q = db.queue("emails")
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
q.enqueue({ to: "alice@example.com" })
|
|
26
|
+
q.enqueue({to: "alice@example.com"})
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
job = q.claim_one("worker-1")
|
|
38
|
-
if job
|
|
28
|
+
if (job = q.claim_one("worker-1"))
|
|
39
29
|
send_email(job.payload)
|
|
40
30
|
job.ack
|
|
41
31
|
end
|
|
42
32
|
```
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
### `Honker::Database.new(path, extension_path:)`
|
|
47
|
-
|
|
48
|
-
Opens (or creates) a SQLite database, loads the Honker extension, applies default PRAGMAs, and bootstraps the schema.
|
|
49
|
-
|
|
50
|
-
### `#queue(name, visibility_timeout_s: 300, max_attempts: 3)`
|
|
51
|
-
|
|
52
|
-
Handle to a named queue.
|
|
53
|
-
|
|
54
|
-
### `Queue#enqueue(payload, delay:, run_at:, priority:, expires:)`
|
|
55
|
-
|
|
56
|
-
Inserts a job. `payload` is any JSON-serializable value. Returns the row id.
|
|
34
|
+
Delayed jobs use `run_at:`:
|
|
57
35
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
### `Job#ack` / `Job#retry(delay_s:, error:)` / `Job#fail(error:)` / `Job#heartbeat(extend_s:)`
|
|
63
|
-
|
|
64
|
-
Claim lifecycle. `ack` deletes. `retry` puts back with a delay (or moves to `_honker_dead` if max_attempts reached). `fail` moves to dead unconditionally. `heartbeat` extends the visibility timeout.
|
|
65
|
-
|
|
66
|
-
### `Database#notify(channel, payload)`
|
|
67
|
-
|
|
68
|
-
Fire a `pg_notify`-style signal. Returns the notification id.
|
|
69
|
-
|
|
70
|
-
## What's not here yet
|
|
36
|
+
```ruby
|
|
37
|
+
q.enqueue({to: "later@example.com"}, run_at: Time.now.to_i + 10)
|
|
38
|
+
```
|
|
71
39
|
|
|
72
|
-
|
|
73
|
-
- Streams (durable pub/sub with per-consumer offsets)
|
|
74
|
-
- Scheduler (cron-style periodic tasks)
|
|
40
|
+
Recurring schedules use `schedule:`:
|
|
75
41
|
|
|
76
|
-
|
|
42
|
+
```ruby
|
|
43
|
+
sched = db.scheduler
|
|
44
|
+
sched.add(name: "fast", queue: "emails", schedule: "@every 1s", payload: {kind: "tick"})
|
|
45
|
+
```
|
|
77
46
|
|
|
78
|
-
|
|
47
|
+
Supported schedule forms:
|
|
79
48
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
49
|
+
- `0 3 * * *`
|
|
50
|
+
- `*/2 * * * * *`
|
|
51
|
+
- `@every 1s`
|
|
83
52
|
|
|
84
|
-
|
|
85
|
-
cd packages/honker-ruby
|
|
86
|
-
bundle install
|
|
87
|
-
bundle exec rake test # or: ruby -Ilib -Ispec spec/honker_spec.rb
|
|
88
|
-
```
|
|
53
|
+
`schedule:` is the canonical recurring name. `cron:` still works as a compatibility alias.
|
data/honker.gemspec
CHANGED
|
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
spec.summary = "Durable queues, streams, pub/sub, and scheduler on SQLite."
|
|
11
11
|
spec.description = <<~DESC.strip
|
|
12
12
|
Ruby binding for Honker — a SQLite-native task runtime. Queues,
|
|
13
|
-
streams, pub/sub,
|
|
13
|
+
streams, pub/sub, time-trigger scheduler, results, locks, rate limits, all
|
|
14
14
|
in one .db file. Thin wrapper around the Honker SQLite loadable
|
|
15
15
|
extension; no Redis, no external broker.
|
|
16
16
|
DESC
|
data/lib/honker/scheduler.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Honker
|
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# Time-trigger scheduler. Register tasks with `add`; `tick` fires all
|
|
14
14
|
# boundaries that have elapsed since the last tick and enqueues the
|
|
15
15
|
# resulting jobs. `run(owner:, stop:)` drives the loop under a
|
|
16
16
|
# leader-elected advisory lock.
|
|
@@ -28,18 +28,30 @@ module Honker
|
|
|
28
28
|
# write; too large and a standby waits longer than necessary after
|
|
29
29
|
# a crash. Matches the Rust binding.
|
|
30
30
|
HEARTBEAT_S = 20
|
|
31
|
+
UPDATE_POLL_S = 0.05
|
|
31
32
|
|
|
32
33
|
def initialize(db)
|
|
33
34
|
@db = db
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
# Register a
|
|
37
|
-
# the
|
|
38
|
-
|
|
37
|
+
# Register a scheduled task. `cron:` is kept for backward
|
|
38
|
+
# compatibility; `schedule:` is the clearer name and can hold:
|
|
39
|
+
#
|
|
40
|
+
# - 5-field cron
|
|
41
|
+
# - 6-field cron
|
|
42
|
+
# - `@every <n><unit>` like `@every 1s`
|
|
43
|
+
#
|
|
44
|
+
# Idempotent by `name`; registering the same name twice replaces
|
|
45
|
+
# the previous row.
|
|
46
|
+
def add(name:, queue:, cron: nil, schedule: nil, payload:, priority: 0, expires_s: nil)
|
|
47
|
+
expr = schedule || cron
|
|
48
|
+
raise ArgumentError, "must provide cron: or schedule:" if expr.nil? || expr.empty?
|
|
49
|
+
|
|
39
50
|
@db.db.get_first_row(
|
|
40
51
|
"SELECT honker_scheduler_register(?, ?, ?, ?, ?, ?)",
|
|
41
|
-
[name, queue,
|
|
52
|
+
[name, queue, expr, JSON.dump(payload), priority, expires_s],
|
|
42
53
|
)
|
|
54
|
+
@db.mark_updated
|
|
43
55
|
nil
|
|
44
56
|
end
|
|
45
57
|
|
|
@@ -49,7 +61,7 @@ module Honker
|
|
|
49
61
|
@db.db.get_first_row(
|
|
50
62
|
"SELECT honker_scheduler_unregister(?)",
|
|
51
63
|
[name],
|
|
52
|
-
)[0]
|
|
64
|
+
)[0].tap { @db.mark_updated }
|
|
53
65
|
end
|
|
54
66
|
|
|
55
67
|
# Fire all due boundaries at `now`. Returns an array of
|
|
@@ -86,7 +98,7 @@ module Honker
|
|
|
86
98
|
until stop_fn.call
|
|
87
99
|
acquired = lock_try_acquire(LEADER_LOCK, owner, LOCK_TTL_S)
|
|
88
100
|
unless acquired
|
|
89
|
-
|
|
101
|
+
wait_for_update_or_timeout(5, stop_fn)
|
|
90
102
|
next
|
|
91
103
|
end
|
|
92
104
|
|
|
@@ -113,16 +125,6 @@ module Honker
|
|
|
113
125
|
"stop must be callable (proc/lambda) or respond to :stop?"
|
|
114
126
|
end
|
|
115
127
|
|
|
116
|
-
# Break sleeps into 1s increments so `stop` is honored promptly
|
|
117
|
-
# without busy-waiting.
|
|
118
|
-
def sleep_with_stop(total_s, stop_fn)
|
|
119
|
-
elapsed = 0
|
|
120
|
-
while elapsed < total_s && !stop_fn.call
|
|
121
|
-
sleep(1)
|
|
122
|
-
elapsed += 1
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
128
|
def leader_loop(owner, stop_fn)
|
|
127
129
|
last_heartbeat = monotonic_now
|
|
128
130
|
until stop_fn.call
|
|
@@ -138,7 +140,18 @@ module Honker
|
|
|
138
140
|
|
|
139
141
|
last_heartbeat = monotonic_now
|
|
140
142
|
end
|
|
141
|
-
|
|
143
|
+
|
|
144
|
+
wait_s = HEARTBEAT_S - (monotonic_now - last_heartbeat)
|
|
145
|
+
wait_s = 0 if wait_s.negative?
|
|
146
|
+
|
|
147
|
+
next_fire = soonest
|
|
148
|
+
if next_fire.positive?
|
|
149
|
+
until_next = next_fire - Time.now.to_i
|
|
150
|
+
until_next = 0 if until_next.negative?
|
|
151
|
+
wait_s = [wait_s, until_next].min
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
wait_for_update_or_timeout(wait_s, stop_fn)
|
|
142
155
|
end
|
|
143
156
|
end
|
|
144
157
|
|
|
@@ -146,6 +159,30 @@ module Honker
|
|
|
146
159
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
147
160
|
end
|
|
148
161
|
|
|
162
|
+
def data_version
|
|
163
|
+
@db.db.get_first_row("PRAGMA data_version")[0].to_i
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def wait_for_update_or_timeout(total_s, stop_fn)
|
|
167
|
+
return if total_s <= 0
|
|
168
|
+
|
|
169
|
+
deadline = monotonic_now + total_s
|
|
170
|
+
last_version = data_version
|
|
171
|
+
last_local = @db.update_snapshot
|
|
172
|
+
|
|
173
|
+
until stop_fn.call
|
|
174
|
+
now = monotonic_now
|
|
175
|
+
break if now >= deadline
|
|
176
|
+
|
|
177
|
+
slice = [UPDATE_POLL_S, deadline - now].min
|
|
178
|
+
sleep(slice)
|
|
179
|
+
|
|
180
|
+
version = data_version
|
|
181
|
+
local = @db.update_snapshot
|
|
182
|
+
return if version != last_version || local != last_local
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
149
186
|
def lock_try_acquire(name, owner, ttl_s)
|
|
150
187
|
@db.db.get_first_row(
|
|
151
188
|
"SELECT honker_lock_acquire(?, ?, ?)",
|
data/lib/honker/version.rb
CHANGED
data/lib/honker.rb
CHANGED
|
@@ -45,6 +45,7 @@ module Honker
|
|
|
45
45
|
|
|
46
46
|
def initialize(path, extension_path:)
|
|
47
47
|
@db = SQLite3::Database.new(path)
|
|
48
|
+
@local_update_seq = 0
|
|
48
49
|
@db.enable_load_extension(true)
|
|
49
50
|
@db.load_extension(extension_path)
|
|
50
51
|
@db.enable_load_extension(false)
|
|
@@ -56,6 +57,14 @@ module Honker
|
|
|
56
57
|
@db&.close
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
def mark_updated
|
|
61
|
+
@local_update_seq += 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def update_snapshot
|
|
65
|
+
@local_update_seq
|
|
66
|
+
end
|
|
67
|
+
|
|
59
68
|
# Returns a Queue handle for a named queue.
|
|
60
69
|
#
|
|
61
70
|
# visibility_timeout_s: 300 # claim expiry before reclaim
|
|
@@ -74,7 +83,7 @@ module Honker
|
|
|
74
83
|
Stream.new(self, name)
|
|
75
84
|
end
|
|
76
85
|
|
|
77
|
-
# Returns the
|
|
86
|
+
# Returns the time-trigger Scheduler facade. Cheap — no allocation
|
|
78
87
|
# beyond the wrapper object.
|
|
79
88
|
def scheduler
|
|
80
89
|
Scheduler.new(self)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: honker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Russell Romney
|
|
@@ -25,7 +25,7 @@ dependencies:
|
|
|
25
25
|
version: '1.7'
|
|
26
26
|
description: |-
|
|
27
27
|
Ruby binding for Honker — a SQLite-native task runtime. Queues,
|
|
28
|
-
streams, pub/sub,
|
|
28
|
+
streams, pub/sub, time-trigger scheduler, results, locks, rate limits, all
|
|
29
29
|
in one .db file. Thin wrapper around the Honker SQLite loadable
|
|
30
30
|
extension; no Redis, no external broker.
|
|
31
31
|
executables: []
|