lora-ruby 0.18.0 → 0.19.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/README.md +16 -6
- data/lib/lora_ruby/version.rb +1 -1
- data/src/gvl.rs +38 -0
- data/src/lib.rs +23 -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: 9219caa445bf7016b0f028045898a8d5f19aed79ef26dec23a37fddeec143a8a
|
|
4
|
+
data.tar.gz: 27bbd9ee828ee1b8ce9b81e17fbf20e6d6ed3226165f542428575c55c0bd55f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5524d305cbd6d90db822fc1eda50ad8c9589cf74b9df4c05015a577c91b2b7daf7dc2ddb8ebb6ddc17b3e2243057150f8011d7b28cf9a0ac1f883cd6504c8c8a
|
|
7
|
+
data.tar.gz: 1d06df901d7c280d740c132d65a3643446390e9f781eedc403f4c5106290cb1093347ee0766b211a4563a76a9ce9a0d7172bc44b55a8ddd70c655aa0068ca722
|
data/README.md
CHANGED
|
@@ -206,14 +206,24 @@ db = LoraRuby::Database.open_wal(
|
|
|
206
206
|
|
|
207
207
|
## Concurrency (GVL release)
|
|
208
208
|
|
|
209
|
-
`
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
Every engine call that can block (`execute`, `explain`, `profile`, `clear`,
|
|
210
|
+
snapshot save/load, opening and closing a database) runs under
|
|
211
|
+
`rb_thread_call_without_gvl`, so other Ruby threads run while the engine is
|
|
212
|
+
busy. `node_count` and `relationship_count` read a snapshot without waiting
|
|
213
|
+
and keep the GVL. Auto-commit reads can overlap on engine snapshots; writes
|
|
214
|
+
serialize on the engine's writer lock.
|
|
215
|
+
|
|
216
|
+
A write waiting for that lock waits with the GVL released, and the Ruby
|
|
217
|
+
binding has no interactive transactions: each write takes the lock and
|
|
218
|
+
releases it within one call. So a waiting writer never blocks the writer
|
|
219
|
+
holding the lock, however many threads write at once.
|
|
212
220
|
|
|
213
221
|
The engine has no cancellation hook, so we pass a `NULL` unblock
|
|
214
|
-
function. A thread interrupted mid-query (`Thread#kill`) will
|
|
215
|
-
the interrupt **after** the current
|
|
216
|
-
|
|
222
|
+
function. A thread interrupted mid-query (`Thread#kill`, `Ctrl-C`) will
|
|
223
|
+
observe the interrupt **after** the current call finishes, including a
|
|
224
|
+
write that was still waiting for the writer lock (it runs, then the
|
|
225
|
+
interrupt fires). Keep queries short if you rely on cooperative
|
|
226
|
+
cancellation.
|
|
217
227
|
|
|
218
228
|
## Local development
|
|
219
229
|
|
data/lib/lora_ruby/version.rb
CHANGED
|
@@ -10,5 +10,5 @@ module LoraRuby
|
|
|
10
10
|
# Guard against redefinition so re-requiring this file (or loading
|
|
11
11
|
# both paths) doesn't emit a "warning: already initialized constant"
|
|
12
12
|
# when the native extension loads second with the identical value.
|
|
13
|
-
VERSION = "0.
|
|
13
|
+
VERSION = "0.19.0" unless const_defined?(:VERSION)
|
|
14
14
|
end
|
data/src/gvl.rs
CHANGED
|
@@ -4,6 +4,36 @@ use std::ffi::c_void;
|
|
|
4
4
|
use std::fmt;
|
|
5
5
|
use std::mem::MaybeUninit;
|
|
6
6
|
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
7
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
8
|
+
|
|
9
|
+
/// Calls running with the GVL released right now, process-wide.
|
|
10
|
+
static RELEASED: AtomicUsize = AtomicUsize::new(0);
|
|
11
|
+
|
|
12
|
+
/// Whether no thread runs a GVL-free call. Called with the GVL held, this
|
|
13
|
+
/// means no other Ruby thread can hold or take the engine's writer lock
|
|
14
|
+
/// before the caller does: taking it needs either the GVL or a GVL-free
|
|
15
|
+
/// call (the binding has no interactive transactions or streams, and it
|
|
16
|
+
/// is not Ractor-safe, so there is one GVL). Background threads of a
|
|
17
|
+
/// persistent database are not covered.
|
|
18
|
+
pub(crate) fn nothing_released() -> bool {
|
|
19
|
+
RELEASED.load(Ordering::SeqCst) == 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Counts a call in [`RELEASED`] while alive.
|
|
23
|
+
struct Released;
|
|
24
|
+
|
|
25
|
+
impl Released {
|
|
26
|
+
fn enter() -> Self {
|
|
27
|
+
RELEASED.fetch_add(1, Ordering::SeqCst);
|
|
28
|
+
Self
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Drop for Released {
|
|
33
|
+
fn drop(&mut self) {
|
|
34
|
+
RELEASED.fetch_sub(1, Ordering::SeqCst);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
7
37
|
|
|
8
38
|
pub(crate) struct GvlPanic {
|
|
9
39
|
payload: Box<dyn std::any::Any + Send>,
|
|
@@ -37,6 +67,13 @@ impl fmt::Display for GvlPanic {
|
|
|
37
67
|
|
|
38
68
|
/// Run `f` with Ruby's Global VM Lock released.
|
|
39
69
|
///
|
|
70
|
+
/// Every engine call that can wait for the writer lock goes through here.
|
|
71
|
+
/// The binding has no interactive transactions: a writer takes the lock
|
|
72
|
+
/// and releases it inside one call, entirely GVL-free, so the holder never
|
|
73
|
+
/// needs the GVL to finish and a waiter always sees it released. Waiting
|
|
74
|
+
/// with the GVL held would freeze every Ruby thread for the wait (and, with
|
|
75
|
+
/// a lock held across Ruby calls, deadlock).
|
|
76
|
+
///
|
|
40
77
|
/// Semantics match `rb_thread_call_without_gvl` — other Ruby threads can
|
|
41
78
|
/// progress while `f` runs. The closure MUST NOT touch Ruby state (no
|
|
42
79
|
/// `Value`s, no allocations into the Ruby heap), which we arrange by
|
|
@@ -73,6 +110,7 @@ where
|
|
|
73
110
|
func: Some(f),
|
|
74
111
|
result: MaybeUninit::uninit(),
|
|
75
112
|
};
|
|
113
|
+
let _released = Released::enter();
|
|
76
114
|
|
|
77
115
|
unsafe {
|
|
78
116
|
rb_sys::rb_thread_call_without_gvl(
|
data/src/lib.rs
CHANGED
|
@@ -183,17 +183,35 @@ fn database_open_wal(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusErro
|
|
|
183
183
|
Ok(Database::from_db(db))
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/// `clear` takes the engine's writer lock, so it waits for any write in
|
|
187
|
+
/// flight. That wait runs with the GVL released like every other engine
|
|
188
|
+
/// call: holding it would freeze every Ruby thread until the running
|
|
189
|
+
/// writer committed. When no write can be in flight (an in-memory database
|
|
190
|
+
/// and no GVL-free call running, see [`gvl::nothing_released`]) the lock
|
|
191
|
+
/// is free and clear runs at once, so it skips the GVL round trip.
|
|
186
192
|
fn database_clear(ruby: &Ruby, rb_self: &Database) -> Result<(), MagnusError> {
|
|
187
|
-
database_inner(ruby, rb_self)
|
|
188
|
-
|
|
193
|
+
let db = database_inner(ruby, rb_self)?;
|
|
194
|
+
if db.wal().is_none() && gvl::nothing_released() {
|
|
195
|
+
return db.try_clear().map_err(|e| query_error_from_anyhow(ruby, e));
|
|
196
|
+
}
|
|
197
|
+
without_gvl_lora_result(ruby, move || db.try_clear())
|
|
189
198
|
}
|
|
190
199
|
|
|
200
|
+
/// Dropping the last handle to a persistent database flushes and closes its
|
|
201
|
+
/// WAL, so that drop runs with the GVL released like any other engine
|
|
202
|
+
/// call. An in-memory database has nothing to flush and drops in place.
|
|
191
203
|
fn database_close(ruby: &Ruby, rb_self: &Database) -> Result<(), MagnusError> {
|
|
192
|
-
let
|
|
204
|
+
let db = rb_self
|
|
193
205
|
.db
|
|
194
206
|
.lock()
|
|
195
|
-
.map_err(|_| query_error(ruby, "database lock poisoned"))
|
|
196
|
-
|
|
207
|
+
.map_err(|_| query_error(ruby, "database lock poisoned"))?
|
|
208
|
+
.take();
|
|
209
|
+
match db {
|
|
210
|
+
Some(db) if db.wal().is_some() => {
|
|
211
|
+
without_gvl(move || drop(db)).map_err(|panic| query_error(ruby, panic.to_string()))?
|
|
212
|
+
}
|
|
213
|
+
_ => {}
|
|
214
|
+
}
|
|
197
215
|
Ok(())
|
|
198
216
|
}
|
|
199
217
|
|