honker 0.3.0 → 0.3.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/ext/honker/honker-core/Cargo.toml +1 -1
- data/ext/honker/honker-core/src/honker_ops.rs +11 -21
- data/ext/honker/honker-core/src/lib.rs +62 -13
- data/ext/honker/honker-extension/Cargo.toml +2 -2
- data/ext/honker/honker-extension/src/lib.rs +65 -3
- data/lib/honker/version.rb +1 -1
- data/lib/honker.rb +15 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0cbbc6a6ec693c1d719be5d556d216d19bf1f975c42d8ccdf599337117d052d
|
|
4
|
+
data.tar.gz: 8c2af61f7af5c590057d12bddbe73c99050b818afaab961301b2c0b91f1f7fe7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a07431eb49bb65c4ead35b9ab4b5d5259b7d766b77a20be36b6d8ed6f2da6f9280adc687b56e2624954a3a5ac4a58b7a6d2f9117205abfd09d0c78730d31b2a2
|
|
7
|
+
data.tar.gz: 84e314eda52d30700270a83c5ca8b32956a7ab78f8bce427303562c1186d938dd7802148a2cf9e32f5e57e2e40a530d535e6af89c838d35007fc1b0c3198189b
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "honker-core"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "Shared Rust foundation for Honker bindings (SQLite loadable extension, PyO3, napi-rs, and friends). Not intended for direct use."
|
|
6
6
|
license = "MIT OR Apache-2.0"
|
|
@@ -990,24 +990,14 @@ pub fn rate_limit_try(
|
|
|
990
990
|
rusqlite::params![per],
|
|
991
991
|
|r| r.get(0),
|
|
992
992
|
)?;
|
|
993
|
-
let
|
|
994
|
-
.query_row(
|
|
995
|
-
"SELECT COALESCE(MAX(count), 0) FROM _honker_rate_limits
|
|
996
|
-
WHERE name = ?1 AND window_start = ?2",
|
|
997
|
-
rusqlite::params![name, window_start],
|
|
998
|
-
|r| r.get(0),
|
|
999
|
-
)
|
|
1000
|
-
.unwrap_or(0);
|
|
1001
|
-
if current >= limit {
|
|
1002
|
-
return Ok(0);
|
|
1003
|
-
}
|
|
1004
|
-
conn.execute(
|
|
993
|
+
let changed = conn.execute(
|
|
1005
994
|
"INSERT INTO _honker_rate_limits (name, window_start, count)
|
|
1006
995
|
VALUES (?1, ?2, 1)
|
|
1007
|
-
ON CONFLICT(name, window_start) DO UPDATE SET count = count + 1
|
|
1008
|
-
|
|
996
|
+
ON CONFLICT(name, window_start) DO UPDATE SET count = count + 1
|
|
997
|
+
WHERE count < ?3",
|
|
998
|
+
rusqlite::params![name, window_start, limit],
|
|
1009
999
|
)?;
|
|
1010
|
-
Ok(1)
|
|
1000
|
+
Ok(if changed > 0 { 1 } else { 0 })
|
|
1011
1001
|
}
|
|
1012
1002
|
|
|
1013
1003
|
pub fn rate_limit_sweep(conn: &Connection, older_than_s: i64) -> rusqlite::Result<i64> {
|
|
@@ -1269,10 +1259,8 @@ pub fn scheduler_update(
|
|
|
1269
1259
|
if !exists {
|
|
1270
1260
|
return Ok(0);
|
|
1271
1261
|
}
|
|
1272
|
-
let any_field =
|
|
1273
|
-
|| payload.is_some()
|
|
1274
|
-
|| priority.is_some()
|
|
1275
|
-
|| expires_s.is_some();
|
|
1262
|
+
let any_field =
|
|
1263
|
+
cron_expr.is_some() || payload.is_some() || priority.is_some() || expires_s.is_some();
|
|
1276
1264
|
if !any_field {
|
|
1277
1265
|
// No fields to change. Don't wake the leader for a no-op.
|
|
1278
1266
|
return Ok(0);
|
|
@@ -1316,8 +1304,10 @@ pub fn scheduler_update(
|
|
|
1316
1304
|
Ok(())
|
|
1317
1305
|
})();
|
|
1318
1306
|
if result.is_err() {
|
|
1319
|
-
let _ = conn.execute_batch(
|
|
1320
|
-
|
|
1307
|
+
let _ = conn.execute_batch(
|
|
1308
|
+
"ROLLBACK TO SAVEPOINT honker_sched_update; \
|
|
1309
|
+
RELEASE SAVEPOINT honker_sched_update",
|
|
1310
|
+
);
|
|
1321
1311
|
result?;
|
|
1322
1312
|
}
|
|
1323
1313
|
conn.execute_batch("RELEASE SAVEPOINT honker_sched_update")?;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
//! acquire, non-blocking try_acquire, and release.
|
|
25
25
|
//! - [`Readers`] — bounded pool of reader connections that open
|
|
26
26
|
//! lazily up to a max.
|
|
27
|
-
//! - [`UpdateWatcher`] —
|
|
27
|
+
//! - [`UpdateWatcher`] — PRAGMA-polling thread that fires a
|
|
28
28
|
//! callback on every database commit. Uses `PRAGMA data_version`
|
|
29
29
|
//! for precise change detection, with a periodic stat identity check
|
|
30
30
|
//! to detect file replacement. Bindings wrap this to surface wake
|
|
@@ -59,13 +59,13 @@ use std::time::{Duration, Instant};
|
|
|
59
59
|
|
|
60
60
|
/// Which backend drives the update-detection loop.
|
|
61
61
|
///
|
|
62
|
-
/// `Polling` is the default:
|
|
62
|
+
/// `Polling` is the default: `PRAGMA data_version` loop, proven
|
|
63
63
|
/// correct across all platforms. The optional backends are **experimental**
|
|
64
64
|
/// — they must first prove equivalence to the polling path before
|
|
65
65
|
/// being relied on for correctness.
|
|
66
66
|
#[derive(Debug, Clone, Default)]
|
|
67
67
|
pub enum WatcherBackend {
|
|
68
|
-
/// Default:
|
|
68
|
+
/// Default: `PRAGMA data_version` polling loop.
|
|
69
69
|
#[default]
|
|
70
70
|
Polling,
|
|
71
71
|
/// OS kernel filesystem notifications (experimental).
|
|
@@ -89,11 +89,40 @@ pub enum WatcherBackend {
|
|
|
89
89
|
ShmFastPath,
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
pub const DEFAULT_WATCHER_POLL_INTERVAL: Duration = Duration::from_millis(1);
|
|
93
|
+
|
|
92
94
|
/// Configuration passed to [`UpdateWatcher::spawn_with_config`] and
|
|
93
95
|
/// [`SharedUpdateWatcher::new_with_config`].
|
|
94
|
-
#[derive(Debug, Clone
|
|
96
|
+
#[derive(Debug, Clone)]
|
|
95
97
|
pub struct WatcherConfig {
|
|
96
98
|
pub backend: WatcherBackend,
|
|
99
|
+
pub poll_interval: Duration,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
impl Default for WatcherConfig {
|
|
103
|
+
fn default() -> Self {
|
|
104
|
+
Self {
|
|
105
|
+
backend: WatcherBackend::default(),
|
|
106
|
+
poll_interval: DEFAULT_WATCHER_POLL_INTERVAL,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
impl WatcherConfig {
|
|
112
|
+
pub fn with_backend(backend: WatcherBackend) -> Self {
|
|
113
|
+
Self {
|
|
114
|
+
backend,
|
|
115
|
+
..Self::default()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pub fn with_poll_interval(mut self, poll_interval: Duration) -> Result<Self, String> {
|
|
120
|
+
if poll_interval.is_zero() {
|
|
121
|
+
return Err("watcher poll interval must be positive".to_string());
|
|
122
|
+
}
|
|
123
|
+
self.poll_interval = poll_interval;
|
|
124
|
+
Ok(self)
|
|
125
|
+
}
|
|
97
126
|
}
|
|
98
127
|
|
|
99
128
|
impl WatcherBackend {
|
|
@@ -680,9 +709,9 @@ fn is_transient_lock_error(e: &rusqlite::Error) -> bool {
|
|
|
680
709
|
///
|
|
681
710
|
/// Three-layer defensive architecture:
|
|
682
711
|
///
|
|
683
|
-
/// 1. **Fast path
|
|
712
|
+
/// 1. **Fast path:** `PRAGMA data_version`. Compare the
|
|
684
713
|
/// integer to last seen value. Notify on change. (~3.5 µs/call.)
|
|
685
|
-
/// 2. **Error recovery
|
|
714
|
+
/// 2. **Error recovery:** If the query fails,
|
|
686
715
|
/// reconnect the SQLite connection and force one wake.
|
|
687
716
|
/// 3. **Identity check (about every 100 ms):** `stat(db_path)` to compare
|
|
688
717
|
/// `(dev, ino)`. If the file was replaced, panic with a clear
|
|
@@ -692,6 +721,7 @@ pub(crate) fn run_poll_loop<F>(
|
|
|
692
721
|
on_change: F,
|
|
693
722
|
stop: Arc<AtomicBool>,
|
|
694
723
|
ready: std::sync::mpsc::SyncSender<()>,
|
|
724
|
+
poll_interval: Duration,
|
|
695
725
|
) where
|
|
696
726
|
F: Fn(),
|
|
697
727
|
{
|
|
@@ -724,7 +754,7 @@ pub(crate) fn run_poll_loop<F>(
|
|
|
724
754
|
drop(ready);
|
|
725
755
|
|
|
726
756
|
while !stop.load(Ordering::Acquire) {
|
|
727
|
-
std::thread::sleep(
|
|
757
|
+
std::thread::sleep(poll_interval);
|
|
728
758
|
|
|
729
759
|
// Path 1: PRAGMA data_version (fast path)
|
|
730
760
|
if let Some(ref c) = conn {
|
|
@@ -836,7 +866,9 @@ impl UpdateWatcher {
|
|
|
836
866
|
let handle = std::thread::Builder::new()
|
|
837
867
|
.name("honker-update-poll".into())
|
|
838
868
|
.spawn(move || match config.backend {
|
|
839
|
-
WatcherBackend::Polling =>
|
|
869
|
+
WatcherBackend::Polling => {
|
|
870
|
+
run_poll_loop(db_path, on_change, stop_t, ready_tx, config.poll_interval)
|
|
871
|
+
}
|
|
840
872
|
#[cfg(feature = "kernel-watcher")]
|
|
841
873
|
WatcherBackend::KernelWatch => {
|
|
842
874
|
kernel_watcher::run_kernel_watch_loop(db_path, on_change, stop_t, ready_tx);
|
|
@@ -2329,6 +2361,7 @@ while True:
|
|
|
2329
2361
|
},
|
|
2330
2362
|
WatcherConfig {
|
|
2331
2363
|
backend: WatcherBackend::KernelWatch,
|
|
2364
|
+
..WatcherConfig::default()
|
|
2332
2365
|
},
|
|
2333
2366
|
);
|
|
2334
2367
|
|
|
@@ -2407,6 +2440,7 @@ while True:
|
|
|
2407
2440
|
},
|
|
2408
2441
|
WatcherConfig {
|
|
2409
2442
|
backend: WatcherBackend::ShmFastPath,
|
|
2443
|
+
..WatcherConfig::default()
|
|
2410
2444
|
},
|
|
2411
2445
|
);
|
|
2412
2446
|
|
|
@@ -2487,7 +2521,7 @@ while True:
|
|
|
2487
2521
|
move || {
|
|
2488
2522
|
count_t.fetch_add(1, AO::Relaxed);
|
|
2489
2523
|
},
|
|
2490
|
-
WatcherConfig
|
|
2524
|
+
WatcherConfig::with_backend(backend),
|
|
2491
2525
|
);
|
|
2492
2526
|
|
|
2493
2527
|
// Drain init wakes (covers shm + kernel setup) before baseline.
|
|
@@ -2704,7 +2738,7 @@ while True:
|
|
|
2704
2738
|
.expect("wake_times mutex poisoned")
|
|
2705
2739
|
.push(std::time::Instant::now());
|
|
2706
2740
|
},
|
|
2707
|
-
WatcherConfig
|
|
2741
|
+
WatcherConfig::with_backend(backend),
|
|
2708
2742
|
);
|
|
2709
2743
|
|
|
2710
2744
|
// Drain initialization wakes.
|
|
@@ -2768,7 +2802,10 @@ while True:
|
|
|
2768
2802
|
ignore = "notify/kqueue can drop the watcher thread under CI load; functional kernel watcher tests still run"
|
|
2769
2803
|
)]
|
|
2770
2804
|
#[cfg(feature = "kernel-watcher")]
|
|
2771
|
-
#[cfg_attr(
|
|
2805
|
+
#[cfg_attr(
|
|
2806
|
+
target_os = "macos",
|
|
2807
|
+
ignore = "kqueue under CI load may deliver zero wakes"
|
|
2808
|
+
)]
|
|
2772
2809
|
fn kernel_watcher_wake_latency_is_event_driven() {
|
|
2773
2810
|
let tmp = std::env::temp_dir().join(format!(
|
|
2774
2811
|
"honker-kw-lat-{}-{}",
|
|
@@ -2872,6 +2909,7 @@ while True:
|
|
|
2872
2909
|
|| {},
|
|
2873
2910
|
WatcherConfig {
|
|
2874
2911
|
backend: WatcherBackend::KernelWatch,
|
|
2912
|
+
..WatcherConfig::default()
|
|
2875
2913
|
},
|
|
2876
2914
|
);
|
|
2877
2915
|
|
|
@@ -2931,6 +2969,14 @@ while True:
|
|
|
2931
2969
|
));
|
|
2932
2970
|
}
|
|
2933
2971
|
|
|
2972
|
+
#[test]
|
|
2973
|
+
fn watcher_config_rejects_zero_poll_interval() {
|
|
2974
|
+
let err = WatcherConfig::default()
|
|
2975
|
+
.with_poll_interval(Duration::from_millis(0))
|
|
2976
|
+
.unwrap_err();
|
|
2977
|
+
assert_eq!(err, "watcher poll interval must be positive");
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2934
2980
|
#[test]
|
|
2935
2981
|
#[cfg(not(feature = "kernel-watcher"))]
|
|
2936
2982
|
fn watcher_backend_parse_rejects_uncompiled_kernel() {
|
|
@@ -3062,8 +3108,11 @@ while True:
|
|
|
3062
3108
|
f.write_all(&buf).unwrap();
|
|
3063
3109
|
}
|
|
3064
3110
|
|
|
3065
|
-
let watcher =
|
|
3066
|
-
|
|
3111
|
+
let watcher = UpdateWatcher::spawn_with_config(
|
|
3112
|
+
tmp.clone(),
|
|
3113
|
+
|| {},
|
|
3114
|
+
WatcherConfig::with_backend(backend),
|
|
3115
|
+
);
|
|
3067
3116
|
// Generous initial wait so the watcher has snapshotted the
|
|
3068
3117
|
// initial inode under CI scheduling pressure.
|
|
3069
3118
|
std::thread::sleep(Duration::from_millis(300));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "honker-extension"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
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,7 +23,7 @@ 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.2.
|
|
26
|
+
honker-core = { path = "../honker-core", version = "0.2.4", default-features = false }
|
|
27
27
|
# "loadable_extension" feature makes rusqlite usable from inside a
|
|
28
28
|
# sqlite3_extension_init entry point.
|
|
29
29
|
rusqlite = { version = "0.39.0", features = ["functions", "hooks", "loadable_extension"] }
|
|
@@ -69,12 +69,17 @@ static NEXT_SQL_WATCHER_ID: AtomicU64 = AtomicU64::new(1);
|
|
|
69
69
|
fn open_watcher_handle(
|
|
70
70
|
db_path: &str,
|
|
71
71
|
backend: Option<&str>,
|
|
72
|
+
watcher_poll_interval_ms: Option<u64>,
|
|
72
73
|
) -> std::result::Result<HonkerWatcherHandle, String> {
|
|
73
74
|
let backend = honker_core::WatcherBackend::parse(backend.filter(|s| !s.is_empty()))?;
|
|
74
75
|
backend.probe(PathBuf::from(db_path).as_path())?;
|
|
76
|
+
let mut config = honker_core::WatcherConfig::with_backend(backend);
|
|
77
|
+
if let Some(ms) = watcher_poll_interval_ms {
|
|
78
|
+
config = config.with_poll_interval(Duration::from_millis(ms))?;
|
|
79
|
+
}
|
|
75
80
|
let shared = Arc::new(honker_core::SharedUpdateWatcher::new_with_config(
|
|
76
81
|
PathBuf::from(db_path),
|
|
77
|
-
|
|
82
|
+
config,
|
|
78
83
|
));
|
|
79
84
|
let (sub_id, rx) = shared.subscribe();
|
|
80
85
|
Ok(HonkerWatcherHandle { shared, sub_id, rx })
|
|
@@ -88,7 +93,7 @@ fn attach_watcher_sql_functions(conn: &Connection) -> Result<()> {
|
|
|
88
93
|
|ctx| {
|
|
89
94
|
let db_path: String = ctx.get(0)?;
|
|
90
95
|
let backend: Option<String> = ctx.get(1)?;
|
|
91
|
-
let handle = open_watcher_handle(&db_path, backend.as_deref()).map_err(|e| {
|
|
96
|
+
let handle = open_watcher_handle(&db_path, backend.as_deref(), None).map_err(|e| {
|
|
92
97
|
rusqlite::Error::UserFunctionError(Box::new(std::io::Error::other(e)))
|
|
93
98
|
})?;
|
|
94
99
|
let id = NEXT_SQL_WATCHER_ID.fetch_add(1, Ordering::Relaxed);
|
|
@@ -96,6 +101,24 @@ fn attach_watcher_sql_functions(conn: &Connection) -> Result<()> {
|
|
|
96
101
|
Ok(id as i64)
|
|
97
102
|
},
|
|
98
103
|
)?;
|
|
104
|
+
conn.create_scalar_function(
|
|
105
|
+
"honker_update_watcher_open",
|
|
106
|
+
3,
|
|
107
|
+
FunctionFlags::SQLITE_UTF8,
|
|
108
|
+
|ctx| {
|
|
109
|
+
let db_path: String = ctx.get(0)?;
|
|
110
|
+
let backend: Option<String> = ctx.get(1)?;
|
|
111
|
+
let poll_interval_ms: Option<i64> = ctx.get(2)?;
|
|
112
|
+
let poll_interval_ms = poll_interval_ms.map(|ms| ms.max(0) as u64);
|
|
113
|
+
let handle = open_watcher_handle(&db_path, backend.as_deref(), poll_interval_ms)
|
|
114
|
+
.map_err(|e| {
|
|
115
|
+
rusqlite::Error::UserFunctionError(Box::new(std::io::Error::other(e)))
|
|
116
|
+
})?;
|
|
117
|
+
let id = NEXT_SQL_WATCHER_ID.fetch_add(1, Ordering::Relaxed);
|
|
118
|
+
SQL_WATCHERS.lock().unwrap().insert(id, handle);
|
|
119
|
+
Ok(id as i64)
|
|
120
|
+
},
|
|
121
|
+
)?;
|
|
99
122
|
conn.create_scalar_function(
|
|
100
123
|
"honker_update_watcher_wait",
|
|
101
124
|
2,
|
|
@@ -266,7 +289,46 @@ pub unsafe extern "C" fn honker_watcher_open(
|
|
|
266
289
|
.to_str()
|
|
267
290
|
.map_err(|e| format!("invalid db_path UTF-8: {e}"))?;
|
|
268
291
|
let backend = unsafe { cstr_to_string(backend) }?;
|
|
269
|
-
let handle = open_watcher_handle(path, backend.as_deref())?;
|
|
292
|
+
let handle = open_watcher_handle(path, backend.as_deref(), None)?;
|
|
293
|
+
Ok(Box::into_raw(Box::new(handle)))
|
|
294
|
+
})) {
|
|
295
|
+
Ok(Ok(ptr)) => ptr,
|
|
296
|
+
Ok(Err(err)) => {
|
|
297
|
+
unsafe { write_error(err_buf, err_buf_len, &err) };
|
|
298
|
+
ptr::null_mut()
|
|
299
|
+
}
|
|
300
|
+
Err(payload) => {
|
|
301
|
+
let err = panic_error(payload).to_string();
|
|
302
|
+
unsafe { write_error(err_buf, err_buf_len, &err) };
|
|
303
|
+
ptr::null_mut()
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Open a core-backed update watcher over `db_path` with options.
|
|
309
|
+
///
|
|
310
|
+
/// `watcher_poll_interval_ms` must be positive. Use `honker_watcher_open`
|
|
311
|
+
/// for the default 1 ms cadence.
|
|
312
|
+
///
|
|
313
|
+
/// # Safety
|
|
314
|
+
/// All pointers must be valid NUL-terminated strings when non-null.
|
|
315
|
+
#[unsafe(no_mangle)]
|
|
316
|
+
pub unsafe extern "C" fn honker_watcher_open_v2(
|
|
317
|
+
db_path: *const c_char,
|
|
318
|
+
backend: *const c_char,
|
|
319
|
+
watcher_poll_interval_ms: u64,
|
|
320
|
+
err_buf: *mut c_char,
|
|
321
|
+
err_buf_len: usize,
|
|
322
|
+
) -> *mut HonkerWatcherHandle {
|
|
323
|
+
match catch_unwind(AssertUnwindSafe(|| {
|
|
324
|
+
if db_path.is_null() {
|
|
325
|
+
return Err("db_path is null".to_string());
|
|
326
|
+
}
|
|
327
|
+
let path = unsafe { CStr::from_ptr(db_path) }
|
|
328
|
+
.to_str()
|
|
329
|
+
.map_err(|e| format!("invalid db_path UTF-8: {e}"))?;
|
|
330
|
+
let backend = unsafe { cstr_to_string(backend) }?;
|
|
331
|
+
let handle = open_watcher_handle(path, backend.as_deref(), Some(watcher_poll_interval_ms))?;
|
|
270
332
|
Ok(Box::into_raw(Box::new(handle)))
|
|
271
333
|
})) {
|
|
272
334
|
Ok(Ok(ptr)) => ptr,
|
data/lib/honker/version.rb
CHANGED
data/lib/honker.rb
CHANGED
|
@@ -113,11 +113,11 @@ module Honker
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
class CoreWatcher
|
|
116
|
-
def initialize(db_path, extension_path, backend)
|
|
116
|
+
def initialize(db_path, extension_path, backend, watcher_poll_interval_ms)
|
|
117
117
|
@lib = Fiddle.dlopen(extension_path)
|
|
118
118
|
@open = Fiddle::Function.new(
|
|
119
|
-
@lib["
|
|
120
|
-
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T],
|
|
119
|
+
@lib["honker_watcher_open_v2"],
|
|
120
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T],
|
|
121
121
|
Fiddle::TYPE_VOIDP,
|
|
122
122
|
)
|
|
123
123
|
@wait = Fiddle::Function.new(
|
|
@@ -131,7 +131,13 @@ module Honker
|
|
|
131
131
|
Fiddle::TYPE_VOID,
|
|
132
132
|
)
|
|
133
133
|
err = "\0" * 1024
|
|
134
|
-
@handle = @open.call(
|
|
134
|
+
@handle = @open.call(
|
|
135
|
+
db_path.to_s,
|
|
136
|
+
backend.to_s,
|
|
137
|
+
watcher_poll_interval_ms || 1,
|
|
138
|
+
err,
|
|
139
|
+
err.bytesize,
|
|
140
|
+
)
|
|
135
141
|
return unless @handle.to_i.zero?
|
|
136
142
|
|
|
137
143
|
raise ArgumentError, err.delete_suffix("\0").split("\0", 2).first
|
|
@@ -171,10 +177,14 @@ module Honker
|
|
|
171
177
|
attr_reader :db
|
|
172
178
|
|
|
173
179
|
def initialize(path, extension_path: nil, watcher_backend: nil,
|
|
180
|
+
watcher_poll_interval_ms: nil,
|
|
174
181
|
extension_resolver: ExtensionResolver.new)
|
|
175
182
|
unless watcher_backend.nil? || watcher_backend.is_a?(String)
|
|
176
183
|
raise ArgumentError, "unknown watcher backend"
|
|
177
184
|
end
|
|
185
|
+
unless watcher_poll_interval_ms.nil? || watcher_poll_interval_ms.to_i.positive?
|
|
186
|
+
raise ArgumentError, "watcher_poll_interval_ms must be positive"
|
|
187
|
+
end
|
|
178
188
|
|
|
179
189
|
resolved_extension = extension_resolver.resolve(extension_path)
|
|
180
190
|
@db = SQLite3::Database.new(path)
|
|
@@ -186,7 +196,7 @@ module Honker
|
|
|
186
196
|
@db.enable_load_extension(false)
|
|
187
197
|
@db.execute_batch(DEFAULT_PRAGMAS)
|
|
188
198
|
@db.execute("SELECT honker_bootstrap()")
|
|
189
|
-
@watcher = CoreWatcher.new(path, resolved_extension, watcher_backend)
|
|
199
|
+
@watcher = CoreWatcher.new(path, resolved_extension, watcher_backend, watcher_poll_interval_ms)
|
|
190
200
|
end
|
|
191
201
|
|
|
192
202
|
def close
|