honker 0.3.2 → 0.5.0
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/ext/honker/honker-core/Cargo.toml +9 -3
- data/ext/honker/honker-core/src/cron.rs +3 -3
- data/ext/honker/honker-core/src/honker_ops.rs +258 -46
- data/ext/honker/honker-core/src/kernel_watcher.rs +191 -24
- data/ext/honker/honker-core/src/lib.rs +577 -36
- data/ext/honker/honker-core/src/shm_watcher.rs +237 -22
- data/ext/honker/honker-extension/Cargo.toml +4 -3
- data/ext/honker/honker-extension/src/lib.rs +7 -9
- data/lib/honker/version.rb +1 -1
- metadata +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
//! Optional `-shm` fast path (feature = `shm-fast-path`).
|
|
2
2
|
//!
|
|
3
3
|
//! **Experimental.** Weaker correctness contract than the polling
|
|
4
|
-
//! backend, in exchange for
|
|
4
|
+
//! backend, in exchange for slightly lower CPU per tick. Not lower
|
|
5
|
+
//! latency — see "What this backend is actually worth" below.
|
|
5
6
|
//!
|
|
6
7
|
//! # Contract
|
|
7
8
|
//!
|
|
@@ -28,25 +29,60 @@
|
|
|
28
29
|
//! is read with bounded positional reads instead of mmap so SQLite file
|
|
29
30
|
//! churn cannot SIGBUS the host process.
|
|
30
31
|
//!
|
|
32
|
+
//! - **`-shm` descriptors are opened once and never closed early.** SQLite
|
|
33
|
+
//! locks `-shm`, and on POSIX closing any descriptor for an inode drops
|
|
34
|
+
//! every lock the *process* holds on it. See the registry below and
|
|
35
|
+
//! issue #80. This is why the backend does not simply `File::open` where
|
|
36
|
+
//! it needs a read.
|
|
37
|
+
//!
|
|
38
|
+
//! # What this backend is actually worth
|
|
39
|
+
//!
|
|
40
|
+
//! Less than its name suggests, and you should know that before enabling
|
|
41
|
+
//! it. The original design mapped `-shm` and read `iChange` as a load,
|
|
42
|
+
//! which was ~2000x cheaper than `PRAGMA data_version`. PR #43 replaced
|
|
43
|
+
//! that mapping with bounded positional reads, citing SIGBUS risk.
|
|
44
|
+
//!
|
|
45
|
+
//! That risk was overstated for *this* read. SQLite's only shrink of
|
|
46
|
+
//! `-shm` truncates it to 3 bytes, not 0 — deliberately — and `iChange`
|
|
47
|
+
//! sits at offset 8, inside the zero-filled partial page that POSIX
|
|
48
|
+
//! guarantees is readable. Only whole pages past the end of the object
|
|
49
|
+
//! fault, and this module never touches those. The SIGBUS actually seen
|
|
50
|
+
//! in CI came from *SQLite's own* mapping, which spans regions at 32 KiB
|
|
51
|
+
//! and beyond, and it happened because this crate was dropping SQLite's
|
|
52
|
+
//! DMS lock — issue #80, fixed by the registry above.
|
|
53
|
+
//!
|
|
54
|
+
//! The bounded read stays anyway: it costs ~1 us, it removes the need to
|
|
55
|
+
//! reason about page boundaries at all, and restoring the mapping would
|
|
56
|
+
//! not help. A bounded read is a syscall (~1.2 us here versus ~2.3 us for
|
|
57
|
+
//! `PRAGMA data_version` on macOS/arm64), but both backends then sleep
|
|
58
|
+
//! 1 ms, so **wake latency is identical**. The mapping would only pay off
|
|
59
|
+
//! paired with a much tighter poll interval — and that trade is already
|
|
60
|
+
//! available on the polling backend via `WatcherConfig::poll_interval`,
|
|
61
|
+
//! without a second backend. Prefer polling unless you have measured a
|
|
62
|
+
//! reason not to.
|
|
63
|
+
//!
|
|
31
64
|
//! Tests assert that wakes fire with sub-millisecond latency in WAL
|
|
32
65
|
//! mode. If a test fails, the backend is broken — not "fall back to
|
|
33
66
|
//! polling and pretend it worked".
|
|
34
67
|
|
|
35
68
|
use crate::stat_identity;
|
|
36
69
|
use rusqlite::{Connection, OpenFlags};
|
|
70
|
+
use std::collections::HashMap;
|
|
37
71
|
use std::fs::File;
|
|
38
|
-
use std::
|
|
39
|
-
use std::path::PathBuf;
|
|
40
|
-
use std::sync::Arc;
|
|
72
|
+
use std::path::{Path, PathBuf};
|
|
41
73
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
74
|
+
use std::sync::{Arc, Mutex, OnceLock};
|
|
42
75
|
use std::time::{Duration, Instant};
|
|
43
76
|
|
|
44
77
|
const WALINDEX_MAX_VERSION: u32 = 3_007_000;
|
|
45
78
|
const ICHANGE_OFFSET: usize = 8;
|
|
46
|
-
/// Same cadence as the polling backend.
|
|
47
|
-
///
|
|
48
|
-
///
|
|
49
|
-
/// for
|
|
79
|
+
/// Same cadence as the polling backend. The win over polling is CPU per
|
|
80
|
+
/// tick, not latency — both sleep 1 ms, so wake latency is identical.
|
|
81
|
+
/// Measured on macOS/arm64 against SQLite 3.51.3: `pread` of the header
|
|
82
|
+
/// is ~1.2 us versus ~2.3 us for `PRAGMA data_version`. That is a much
|
|
83
|
+
/// smaller margin than the original mmap design promised, because a
|
|
84
|
+
/// bounded read is a syscall and a mapped load was not. See the module
|
|
85
|
+
/// docs for why the mapping had to go.
|
|
50
86
|
const POLL_INTERVAL_MS: u64 = 1;
|
|
51
87
|
/// Cadence for the dead-man's switch (db / -shm replacement detection).
|
|
52
88
|
/// Same wall-clock interval as the polling and kernel backends. Tracked
|
|
@@ -54,6 +90,181 @@ const POLL_INTERVAL_MS: u64 = 1;
|
|
|
54
90
|
/// up to ~15 ms.
|
|
55
91
|
const IDENTITY_CHECK_INTERVAL: Duration = Duration::from_millis(100);
|
|
56
92
|
|
|
93
|
+
// ---------------------------------------------------------------------
|
|
94
|
+
// `-shm` descriptor registry (issue #80)
|
|
95
|
+
// ---------------------------------------------------------------------
|
|
96
|
+
//
|
|
97
|
+
// SQLite takes POSIX advisory locks on `-shm` (the WAL-index locks and
|
|
98
|
+
// the DMS byte). On POSIX, `close()` of *any* descriptor for an inode
|
|
99
|
+
// releases *every* lock the calling process holds on it — including
|
|
100
|
+
// locks belonging to unrelated SQLite connections in the same process.
|
|
101
|
+
// SQLite's `unixInodeInfo` deferred-close list only defers closes of
|
|
102
|
+
// descriptors SQLite itself opened, so it cannot protect us.
|
|
103
|
+
//
|
|
104
|
+
// So this module must never close a `-shm` descriptor while a connection
|
|
105
|
+
// in this process might still hold locks on that inode. Three call sites
|
|
106
|
+
// used to do exactly that: `probe()` on every `honker.open()`, the
|
|
107
|
+
// identity-change reopen, and the watcher's own shutdown.
|
|
108
|
+
//
|
|
109
|
+
// The rule enforced here:
|
|
110
|
+
//
|
|
111
|
+
// * open each `-shm` inode at most once per process, and share it;
|
|
112
|
+
// * never close a descriptor whose inode still has a name;
|
|
113
|
+
// * reclaim descriptors once `st_nlink == 0`, which proves the inode is
|
|
114
|
+
// unreferenced. SQLite only unlinks `-shm` while holding an EXCLUSIVE
|
|
115
|
+
// lock on the database file — i.e. when no connection anywhere is
|
|
116
|
+
// using that WAL — so at that point there is no lock left to drop.
|
|
117
|
+
//
|
|
118
|
+
// That bounds retention to one descriptor per *live* watched database
|
|
119
|
+
// rather than leaking one per WAL generation.
|
|
120
|
+
|
|
121
|
+
/// How many `-shm` descriptors this process will retain before giving up.
|
|
122
|
+
/// Only reachable if inodes are churning faster than they can be reclaimed;
|
|
123
|
+
/// failing loudly beats leaking descriptors without limit.
|
|
124
|
+
#[cfg(unix)]
|
|
125
|
+
const MAX_RETAINED_SHM_FDS: usize = 256;
|
|
126
|
+
|
|
127
|
+
#[cfg(unix)]
|
|
128
|
+
#[derive(Default)]
|
|
129
|
+
struct ShmFdRegistry {
|
|
130
|
+
/// Current descriptor per `(dev, ino)`, shared by every watcher on it.
|
|
131
|
+
live: HashMap<(u64, u64), Arc<File>>,
|
|
132
|
+
/// Descriptors we could not file under a free key (lost an open race).
|
|
133
|
+
/// Held, not closed, until reclaimable.
|
|
134
|
+
extra: Vec<Arc<File>>,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[cfg(unix)]
|
|
138
|
+
fn shm_registry() -> &'static Mutex<ShmFdRegistry> {
|
|
139
|
+
static REGISTRY: OnceLock<Mutex<ShmFdRegistry>> = OnceLock::new();
|
|
140
|
+
REGISTRY.get_or_init(|| Mutex::new(ShmFdRegistry::default()))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// `st_nlink` for an already-open file. `0` means the inode has no
|
|
144
|
+
/// remaining directory entry, so nothing can lock it any more.
|
|
145
|
+
#[cfg(unix)]
|
|
146
|
+
fn link_count(file: &File) -> Option<u64> {
|
|
147
|
+
use std::os::unix::fs::MetadataExt;
|
|
148
|
+
file.metadata().ok().map(|m| m.nlink())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[cfg(unix)]
|
|
152
|
+
impl ShmFdRegistry {
|
|
153
|
+
/// Drop every descriptor whose inode is provably unreferenced and that
|
|
154
|
+
/// no watcher still holds. Cheap: one `fstat` per retained descriptor,
|
|
155
|
+
/// run only on acquire.
|
|
156
|
+
fn reclaim_unlinked(&mut self) {
|
|
157
|
+
self.live
|
|
158
|
+
.retain(|_, fd| Arc::strong_count(fd) > 1 || link_count(fd) != Some(0));
|
|
159
|
+
self.extra
|
|
160
|
+
.retain(|fd| Arc::strong_count(fd) > 1 || link_count(fd) != Some(0));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn retained(&self) -> usize {
|
|
164
|
+
self.live.len() + self.extra.len()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Get a shared descriptor for `path`, opening it only if this process
|
|
169
|
+
/// does not already hold one for that inode.
|
|
170
|
+
///
|
|
171
|
+
/// The `stat`-before-`open` step is not an optimization — it is the whole
|
|
172
|
+
/// point. Opening a second descriptor just to discover we already had one
|
|
173
|
+
/// would mean closing it, and that close is the bug.
|
|
174
|
+
#[cfg(unix)]
|
|
175
|
+
fn acquire_shm_fd(path: &Path) -> std::io::Result<Arc<File>> {
|
|
176
|
+
let existing_id = stat_identity(path).ok();
|
|
177
|
+
let mut reg = shm_registry().lock().unwrap_or_else(|e| e.into_inner());
|
|
178
|
+
reg.reclaim_unlinked();
|
|
179
|
+
|
|
180
|
+
if let Some(id) = existing_id
|
|
181
|
+
&& let Some(fd) = reg.live.get(&id)
|
|
182
|
+
{
|
|
183
|
+
return Ok(Arc::clone(fd));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if reg.retained() >= MAX_RETAINED_SHM_FDS {
|
|
187
|
+
return Err(std::io::Error::other(format!(
|
|
188
|
+
"shm-fast-path is holding {MAX_RETAINED_SHM_FDS} -shm descriptors it \
|
|
189
|
+
cannot safely close; refusing to open more. Closing them would \
|
|
190
|
+
release this process's SQLite WAL-index locks (issue #80)."
|
|
191
|
+
)));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let file = Arc::new(File::open(path)?);
|
|
195
|
+
// Re-identify from the descriptor: `path` may have been replaced
|
|
196
|
+
// between the stat above and this open.
|
|
197
|
+
let id = match stat_identity_of(&file) {
|
|
198
|
+
Some(id) => id,
|
|
199
|
+
None => {
|
|
200
|
+
reg.extra.push(Arc::clone(&file));
|
|
201
|
+
return Ok(file);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
match reg.live.entry(id) {
|
|
205
|
+
std::collections::hash_map::Entry::Occupied(slot) => {
|
|
206
|
+
// Raced with another thread. Keep ours alive rather than
|
|
207
|
+
// closing it, and hand back the descriptor already on file.
|
|
208
|
+
let winner = Arc::clone(slot.get());
|
|
209
|
+
reg.extra.push(file);
|
|
210
|
+
Ok(winner)
|
|
211
|
+
}
|
|
212
|
+
std::collections::hash_map::Entry::Vacant(slot) => {
|
|
213
|
+
slot.insert(Arc::clone(&file));
|
|
214
|
+
Ok(file)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// `(dev, ino)` of an open file, matching [`crate::stat_identity`]'s shape.
|
|
220
|
+
#[cfg(unix)]
|
|
221
|
+
fn stat_identity_of(file: &File) -> Option<(u64, u64)> {
|
|
222
|
+
use std::os::unix::fs::MetadataExt;
|
|
223
|
+
let m = file.metadata().ok()?;
|
|
224
|
+
Some((m.dev(), m.ino()))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Windows has no POSIX advisory locks — closing a handle cannot release
|
|
228
|
+
/// another connection's lock — so the retention machinery above is not
|
|
229
|
+
/// needed and would only keep handles alive on a live `-shm`.
|
|
230
|
+
#[cfg(not(unix))]
|
|
231
|
+
fn acquire_shm_fd(path: &Path) -> std::io::Result<Arc<File>> {
|
|
232
|
+
File::open(path).map(Arc::new)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// A `-shm` descriptor, the 12-byte WAL-index header read from it, and
|
|
236
|
+
/// that file's `(dev, ino)` at the time of the read.
|
|
237
|
+
type ShmHeaderSnapshot = (Arc<File>, [u8; 12], (u64, u64));
|
|
238
|
+
|
|
239
|
+
/// Positional read of the WAL-index header. One syscall, and — unlike a
|
|
240
|
+
/// mapping — a file that shrinks underneath us yields `UnexpectedEof`
|
|
241
|
+
/// instead of SIGBUS. That protection came from PR #43 and must stay.
|
|
242
|
+
fn read_wal_index_header(file: &File) -> std::io::Result<[u8; 12]> {
|
|
243
|
+
let mut header = [0_u8; 12];
|
|
244
|
+
#[cfg(unix)]
|
|
245
|
+
{
|
|
246
|
+
use std::os::unix::fs::FileExt;
|
|
247
|
+
file.read_exact_at(&mut header, 0)?;
|
|
248
|
+
}
|
|
249
|
+
#[cfg(windows)]
|
|
250
|
+
{
|
|
251
|
+
use std::os::windows::fs::FileExt;
|
|
252
|
+
let mut done = 0;
|
|
253
|
+
while done < header.len() {
|
|
254
|
+
match file.seek_read(&mut header[done..], done as u64)? {
|
|
255
|
+
0 => {
|
|
256
|
+
return Err(std::io::Error::new(
|
|
257
|
+
std::io::ErrorKind::UnexpectedEof,
|
|
258
|
+
"-shm shorter than the WAL index header",
|
|
259
|
+
));
|
|
260
|
+
}
|
|
261
|
+
n => done += n,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
Ok(header)
|
|
266
|
+
}
|
|
267
|
+
|
|
57
268
|
pub(crate) fn run_shm_fast_path_loop<F>(
|
|
58
269
|
db_path: PathBuf,
|
|
59
270
|
on_change: F,
|
|
@@ -118,7 +329,7 @@ pub(crate) fn run_shm_fast_path_loop<F>(
|
|
|
118
329
|
|
|
119
330
|
while !stop.load(Ordering::Acquire) {
|
|
120
331
|
std::thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
|
|
121
|
-
let current = match read_wal_index_header(&
|
|
332
|
+
let current = match read_wal_index_header(&f) {
|
|
122
333
|
Ok(header) => read_ichange_from_header(&header),
|
|
123
334
|
Err(e) => {
|
|
124
335
|
if let Some((new_file, new_header, new_id)) = reopen_shm_header(&shm_path) {
|
|
@@ -164,13 +375,6 @@ pub(crate) fn run_shm_fast_path_loop<F>(
|
|
|
164
375
|
}
|
|
165
376
|
}
|
|
166
377
|
|
|
167
|
-
fn read_wal_index_header(file: &mut File) -> std::io::Result<[u8; 12]> {
|
|
168
|
-
let mut header = [0_u8; 12];
|
|
169
|
-
file.seek(SeekFrom::Start(0))?;
|
|
170
|
-
file.read_exact(&mut header)?;
|
|
171
|
-
Ok(header)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
378
|
fn read_ichange_from_header(header: &[u8; 12]) -> u32 {
|
|
175
379
|
u32::from_ne_bytes(
|
|
176
380
|
header[ICHANGE_OFFSET..ICHANGE_OFFSET + 4]
|
|
@@ -179,9 +383,16 @@ fn read_ichange_from_header(header: &[u8; 12]) -> u32 {
|
|
|
179
383
|
)
|
|
180
384
|
}
|
|
181
385
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
386
|
+
/// Re-acquire the current `-shm` descriptor and re-read its header.
|
|
387
|
+
///
|
|
388
|
+
/// Goes through [`acquire_shm_fd`], so the descriptor this replaces is
|
|
389
|
+
/// retained by the registry rather than closed. Closing it here would
|
|
390
|
+
/// release the WAL-index locks of every SQLite connection in this
|
|
391
|
+
/// process — the identity-change path was one of the three sites that
|
|
392
|
+
/// did exactly that before issue #80.
|
|
393
|
+
fn reopen_shm_header(path: &std::path::Path) -> Option<ShmHeaderSnapshot> {
|
|
394
|
+
let file = acquire_shm_fd(path).ok()?;
|
|
395
|
+
let header = read_wal_index_header(&file).ok()?;
|
|
185
396
|
let id = stat_identity(path).ok()?;
|
|
186
397
|
Some((file, header, id))
|
|
187
398
|
}
|
|
@@ -189,7 +400,7 @@ fn reopen_shm_header(path: &std::path::Path) -> Option<(File, [u8; 12], (u64, u6
|
|
|
189
400
|
fn wait_for_initial_shm_header(
|
|
190
401
|
path: &std::path::Path,
|
|
191
402
|
stop: &AtomicBool,
|
|
192
|
-
) -> Option<
|
|
403
|
+
) -> Option<ShmHeaderSnapshot> {
|
|
193
404
|
for _ in 0..200 {
|
|
194
405
|
if stop.load(Ordering::Acquire) {
|
|
195
406
|
return None;
|
|
@@ -236,10 +447,14 @@ pub(crate) fn probe(db_path: &std::path::Path) -> Result<(), String> {
|
|
|
236
447
|
return Err("shm-fast-path requires little-endian platform".into());
|
|
237
448
|
}
|
|
238
449
|
let shm = format!("{}-shm", db_path.display());
|
|
239
|
-
|
|
450
|
+
// Acquire through the registry, never `File::open` + drop. `probe`
|
|
451
|
+
// runs at `honker.open()` time, *after* the caller's writer connection
|
|
452
|
+
// has already created -wal/-shm, so a bare open+close here released
|
|
453
|
+
// this process's SQLite WAL-index locks on every single open.
|
|
454
|
+
let f = acquire_shm_fd(std::path::Path::new(&shm))
|
|
240
455
|
.map_err(|e| format!("-shm unavailable ({e}). WAL mode + open connection required."))?;
|
|
241
456
|
let header =
|
|
242
|
-
read_wal_index_header(&
|
|
457
|
+
read_wal_index_header(&f).map_err(|e| format!("-shm too small or unreadable: {e}"))?;
|
|
243
458
|
let iv = u32::from_ne_bytes(header[0..4].try_into().unwrap());
|
|
244
459
|
if iv != WALINDEX_MAX_VERSION {
|
|
245
460
|
return Err(format!(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "honker-extension"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "SQLite loadable extension for Honker. Adds honker_* SQL functions (queues, streams, scheduler, pub/sub) to any SQLite client."
|
|
6
6
|
license = "MIT OR Apache-2.0"
|
|
@@ -23,14 +23,15 @@ crate-type = ["cdylib"]
|
|
|
23
23
|
# Using both `path` and `version` lets Cargo publish this crate to
|
|
24
24
|
# crates.io referencing the real honker-core = "0.2" while still
|
|
25
25
|
# using the in-tree source for local builds.
|
|
26
|
-
honker-core = { path = "../honker-core", version = "0.
|
|
26
|
+
honker-core = { path = "../honker-core", version = "0.5.0", default-features = false }
|
|
27
27
|
# "loadable_extension" feature makes rusqlite usable from inside a
|
|
28
28
|
# sqlite3_extension_init entry point.
|
|
29
|
-
rusqlite = { version = "0.
|
|
29
|
+
rusqlite = { version = "0.40.1", features = ["functions", "hooks", "loadable_extension"] }
|
|
30
30
|
|
|
31
31
|
[features]
|
|
32
32
|
default = []
|
|
33
33
|
kernel-watcher = ["honker-core/kernel-watcher"]
|
|
34
34
|
shm-fast-path = ["honker-core/shm-fast-path"]
|
|
35
35
|
|
|
36
|
+
|
|
36
37
|
[workspace]
|
|
@@ -108,7 +108,7 @@ fn attach_watcher_sql_functions(conn: &Connection) -> Result<()> {
|
|
|
108
108
|
|ctx| {
|
|
109
109
|
let db_path: String = ctx.get(0)?;
|
|
110
110
|
let backend: Option<String> = ctx.get(1)?;
|
|
111
|
-
let poll_interval_ms: Option<i64> = ctx
|
|
111
|
+
let poll_interval_ms: Option<i64> = honker_core::arg_opt_i64(ctx, 2)?;
|
|
112
112
|
let poll_interval_ms = poll_interval_ms.map(|ms| ms.max(0) as u64);
|
|
113
113
|
let handle = open_watcher_handle(&db_path, backend.as_deref(), poll_interval_ms)
|
|
114
114
|
.map_err(|e| {
|
|
@@ -124,8 +124,8 @@ fn attach_watcher_sql_functions(conn: &Connection) -> Result<()> {
|
|
|
124
124
|
2,
|
|
125
125
|
FunctionFlags::SQLITE_UTF8,
|
|
126
126
|
|ctx| {
|
|
127
|
-
let id: i64 = ctx
|
|
128
|
-
let timeout_ms: i64 = ctx
|
|
127
|
+
let id: i64 = honker_core::arg_i64(ctx, 0)?;
|
|
128
|
+
let timeout_ms: i64 = honker_core::arg_i64(ctx, 1)?;
|
|
129
129
|
let Some(handle) = SQL_WATCHERS.lock().unwrap().remove(&(id as u64)) else {
|
|
130
130
|
return Ok(-1);
|
|
131
131
|
};
|
|
@@ -149,7 +149,7 @@ fn attach_watcher_sql_functions(conn: &Connection) -> Result<()> {
|
|
|
149
149
|
1,
|
|
150
150
|
FunctionFlags::SQLITE_UTF8,
|
|
151
151
|
|ctx| {
|
|
152
|
-
let id: i64 = ctx
|
|
152
|
+
let id: i64 = honker_core::arg_i64(ctx, 0)?;
|
|
153
153
|
if let Some(handle) = SQL_WATCHERS.lock().unwrap().remove(&(id as u64)) {
|
|
154
154
|
handle.shared.unsubscribe(handle.sub_id);
|
|
155
155
|
let _ = handle.shared.close();
|
|
@@ -363,17 +363,15 @@ pub unsafe extern "C" fn honker_watcher_wait(
|
|
|
363
363
|
if handle.is_null() {
|
|
364
364
|
return -1;
|
|
365
365
|
}
|
|
366
|
-
|
|
366
|
+
catch_unwind(AssertUnwindSafe(|| {
|
|
367
367
|
let handle = unsafe { &mut *handle };
|
|
368
368
|
match handle.rx.recv_timeout(Duration::from_millis(timeout_ms)) {
|
|
369
369
|
Ok(()) => 1,
|
|
370
370
|
Err(RecvTimeoutError::Timeout) => 0,
|
|
371
371
|
Err(RecvTimeoutError::Disconnected) => -1,
|
|
372
372
|
}
|
|
373
|
-
}))
|
|
374
|
-
|
|
375
|
-
Err(_) => -2,
|
|
376
|
-
}
|
|
373
|
+
}))
|
|
374
|
+
.unwrap_or(-2)
|
|
377
375
|
}
|
|
378
376
|
|
|
379
377
|
/// Close a watcher opened by `honker_watcher_open`.
|
data/lib/honker/version.rb
CHANGED