lora-ruby 0.8.5 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f72fa76f2d63847c463cc5c593bbe9e8995cf34a64bc8eec60e701a183160fa
4
- data.tar.gz: a76de1f7ca4da81c27ba3bd2053c40972a534b01b29bd31bf79bacb9f551855f
3
+ metadata.gz: f5e8fafa0e3463f9478a2fe5ee4508dcb8952ba61611b6ce49f8d0fc25ae2bb6
4
+ data.tar.gz: 653fd6d952de9430140bf6574ae86995e0750c387e308c9709bc8cf3be726408
5
5
  SHA512:
6
- metadata.gz: 53d140c867e9a6d5aec77ce394a19fa28f6506ae9f212648a7ab62788926c8f4e4c2589f311ff465e21f9c259ab5513d20814a645493549610b0d20b81a5d9d8
7
- data.tar.gz: ff1c852d76857f3381faf68347229687996253b921e69265793af586f64e75abb421598852ea5f8e15d759d8a7d2410c1308d0a8e894c8065c54ff27df56a336
6
+ metadata.gz: 626786c04ec31baef4f51e5ba58440ba9755bcff854325f65e88f35b689117634bde1f4edfb6cf55661c130f80e2a1075c853ccc9a765a5d12ce0f06a7ddfb37
7
+ data.tar.gz: e95004f92f2a18f0a8ade9d0a98963e2b56ebc1e038850658f007173d007652b7b3eee17d062d6163ff4f544c5a86ef1ec0b3d478d98eab793920aff9a3bf35b
@@ -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.8.5" unless const_defined?(:VERSION)
13
+ VERSION = "0.9.0" unless const_defined?(:VERSION)
14
14
  end
data/src/errors.rs CHANGED
@@ -9,18 +9,15 @@
9
9
  use lora_database::{LoraError, LoraErrorCode};
10
10
  use magnus::{prelude::*, Error as MagnusError, ExceptionClass, RModule, Ruby};
11
11
 
12
- pub(crate) fn lora_module(ruby: &Ruby) -> RModule {
13
- ruby.class_object()
14
- .const_get::<_, RModule>("LoraRuby")
15
- .expect("LoraRuby module is defined by `init` before any method runs")
16
- }
17
-
18
12
  pub(crate) fn lora_error_class(ruby: &Ruby, name: &str) -> ExceptionClass {
19
13
  // `const_get::<_, ExceptionClass>` converts the stored RClass into
20
14
  // an ExceptionClass — this is the sound path, because our subclasses
21
15
  // of StandardError retain the exception-class trait on the Ruby
22
16
  // side even though `define_class` typed them as RClass.
23
- lora_module(ruby)
17
+ let Ok(module) = ruby.class_object().const_get::<_, RModule>("LoraRuby") else {
18
+ return ruby.exception_standard_error();
19
+ };
20
+ module
24
21
  .const_get::<_, ExceptionClass>(name)
25
22
  .unwrap_or_else(|_| ruby.exception_standard_error())
26
23
  }
data/src/gvl.rs CHANGED
@@ -1,7 +1,39 @@
1
1
  //! Global VM Lock release primitive.
2
2
 
3
3
  use std::ffi::c_void;
4
+ use std::fmt;
4
5
  use std::mem::MaybeUninit;
6
+ use std::panic::{catch_unwind, AssertUnwindSafe};
7
+
8
+ pub(crate) struct GvlPanic {
9
+ payload: Box<dyn std::any::Any + Send>,
10
+ }
11
+
12
+ impl GvlPanic {
13
+ fn new(payload: Box<dyn std::any::Any + Send>) -> Self {
14
+ Self { payload }
15
+ }
16
+
17
+ fn message(&self) -> &str {
18
+ if let Some(s) = self.payload.downcast_ref::<&'static str>() {
19
+ s
20
+ } else if let Some(s) = self.payload.downcast_ref::<String>() {
21
+ s.as_str()
22
+ } else {
23
+ "non-string panic payload"
24
+ }
25
+ }
26
+ }
27
+
28
+ impl fmt::Display for GvlPanic {
29
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30
+ write!(
31
+ f,
32
+ "engine panicked while Ruby GVL was released: {}",
33
+ self.message()
34
+ )
35
+ }
36
+ }
5
37
 
6
38
  /// Run `f` with Ruby's Global VM Lock released.
7
39
  ///
@@ -11,7 +43,7 @@ use std::mem::MaybeUninit;
11
43
  /// keeping all such work on the calling thread. Everything inside
12
44
  /// `database_execute`'s closure is pure Rust on pre-extracted data, so
13
45
  /// this is sound.
14
- pub(crate) fn without_gvl<F, R>(f: F) -> R
46
+ pub(crate) fn without_gvl<F, R>(f: F) -> Result<R, GvlPanic>
15
47
  where
16
48
  F: FnOnce() -> R,
17
49
  F: Send,
@@ -19,7 +51,7 @@ where
19
51
  {
20
52
  struct Data<F, R> {
21
53
  func: Option<F>,
22
- result: MaybeUninit<R>,
54
+ result: MaybeUninit<std::thread::Result<R>>,
23
55
  }
24
56
 
25
57
  unsafe extern "C" fn trampoline<F, R>(data: *mut c_void) -> *mut c_void
@@ -27,11 +59,13 @@ where
27
59
  F: FnOnce() -> R,
28
60
  {
29
61
  let data = &mut *(data as *mut Data<F, R>);
30
- let f = data
31
- .func
32
- .take()
33
- .expect("without_gvl: closure already taken");
34
- data.result.write(f());
62
+ let result = match data.func.take() {
63
+ Some(f) => catch_unwind(AssertUnwindSafe(f)),
64
+ None => {
65
+ Err(Box::new("without_gvl: closure already taken") as Box<dyn std::any::Any + Send>)
66
+ }
67
+ };
68
+ data.result.write(result);
35
69
  std::ptr::null_mut()
36
70
  }
37
71
 
@@ -50,6 +84,6 @@ where
50
84
  None,
51
85
  std::ptr::null_mut(),
52
86
  );
53
- data.result.assume_init()
87
+ data.result.assume_init().map_err(GvlPanic::new)
54
88
  }
55
89
  }
data/src/lib.rs CHANGED
@@ -146,8 +146,7 @@ impl Database {
146
146
  // cost-equivalent.
147
147
  fn database_new(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusError> {
148
148
  let (database_name, options) = database_open_args(ruby, args)?;
149
- let db = without_gvl(move || open_database(database_name, options))
150
- .map_err(|e| query_error(ruby, e))?;
149
+ let db = without_gvl_string_result(ruby, move || open_database(database_name, options))?;
151
150
  Ok(Database::from_db(db))
152
151
  }
153
152
 
@@ -180,7 +179,7 @@ fn database_open_wal(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusErro
180
179
  ));
181
180
  }
182
181
  options.wal_dir = Some(wal_dir);
183
- let db = without_gvl(move || open_wal_database(options)).map_err(|e| query_error(ruby, e))?;
182
+ let db = without_gvl_string_result(ruby, move || open_wal_database(options))?;
184
183
  Ok(Database::from_db(db))
185
184
  }
186
185
 
@@ -229,8 +228,9 @@ fn database_save_snapshot(
229
228
  ) -> Result<RHash, MagnusError> {
230
229
  let (path, options) = snapshot_file_args(ruby, args)?;
231
230
  let db = database_inner(ruby, rb_self)?;
232
- let meta = without_gvl(move || db.save_snapshot_to_with_options(&path, &options))
233
- .map_err(|e| query_error_from_anyhow(ruby, e))?;
231
+ let meta = without_gvl_lora_result(ruby, move || {
232
+ db.save_snapshot_to_with_options(&path, &options)
233
+ })?;
234
234
  snapshot_meta_to_rhash(ruby, meta)
235
235
  }
236
236
 
@@ -241,9 +241,9 @@ fn database_load_snapshot(
241
241
  ) -> Result<RHash, MagnusError> {
242
242
  let (path, credentials) = snapshot_load_file_args(ruby, args)?;
243
243
  let db = database_inner(ruby, rb_self)?;
244
- let meta =
245
- without_gvl(move || db.load_snapshot_from_with_credentials(&path, credentials.as_ref()))
246
- .map_err(|e| query_error_from_anyhow(ruby, e))?;
244
+ let meta = without_gvl_lora_result(ruby, move || {
245
+ db.load_snapshot_from_with_credentials(&path, credentials.as_ref())
246
+ })?;
247
247
  snapshot_meta_to_rhash(ruby, meta)
248
248
  }
249
249
 
@@ -456,6 +456,31 @@ fn database_inner(
456
456
  .ok_or_else(|| query_error(ruby, "database is closed"))
457
457
  }
458
458
 
459
+ fn without_gvl_string_result<T>(
460
+ ruby: &Ruby,
461
+ f: impl FnOnce() -> Result<T, String> + Send,
462
+ ) -> Result<T, MagnusError>
463
+ where
464
+ T: Send,
465
+ {
466
+ without_gvl(f)
467
+ .map_err(|panic| query_error(ruby, panic.to_string()))?
468
+ .map_err(|e| query_error(ruby, e))
469
+ }
470
+
471
+ fn without_gvl_lora_result<T, E>(
472
+ ruby: &Ruby,
473
+ f: impl FnOnce() -> Result<T, E> + Send,
474
+ ) -> Result<T, MagnusError>
475
+ where
476
+ T: Send,
477
+ E: Into<lora_database::LoraError> + Send,
478
+ {
479
+ without_gvl(f)
480
+ .map_err(|panic| query_error(ruby, panic.to_string()))?
481
+ .map_err(|e| query_error_from_anyhow(ruby, e))
482
+ }
483
+
459
484
  /// `execute(query, params = nil)` — `-1` arity so `params` is optional and
460
485
  /// we can distinguish "not passed" from `nil`/`{}` (both map to empty
461
486
  /// params). Everything that touches Ruby values happens under the GVL;
@@ -494,17 +519,16 @@ fn database_execute(ruby: &Ruby, rb_self: &Database, args: &[Value]) -> Result<R
494
519
  // is pure Rust — no Ruby values cross the boundary — which keeps this
495
520
  // sound.
496
521
  let db = database_inner(ruby, rb_self)?;
497
- let exec_result = without_gvl(move || {
522
+ let exec_result = without_gvl_lora_result(ruby, move || {
498
523
  let options = ExecuteOptions {
499
524
  format: ResultFormat::RowArrays,
500
525
  };
501
526
  db.execute_with_params(&query, Some(options), params_map)
502
- });
527
+ })?;
503
528
 
504
529
  let row_arrays = match exec_result {
505
- Ok(QueryResult::RowArrays(r)) => r,
506
- Ok(_) => return Err(query_error(ruby, "expected RowArrays result")),
507
- Err(e) => return Err(query_error_from_anyhow(ruby, e)),
530
+ QueryResult::RowArrays(r) => r,
531
+ _ => return Err(query_error(ruby, "expected RowArrays result")),
508
532
  };
509
533
 
510
534
  let out = ruby.hash_new();
@@ -536,8 +560,7 @@ fn database_explain(ruby: &Ruby, rb_self: &Database, args: &[Value]) -> Result<R
536
560
  None => None,
537
561
  };
538
562
  let db = database_inner(ruby, rb_self)?;
539
- let plan = without_gvl(move || db.explain(&query, params_map))
540
- .map_err(|e| query_error_from_anyhow(ruby, e))?;
563
+ let plan = without_gvl_lora_result(ruby, move || db.explain(&query, params_map))?;
541
564
  query_plan_to_ruby(ruby, &plan)
542
565
  }
543
566
 
@@ -554,8 +577,7 @@ fn database_profile(ruby: &Ruby, rb_self: &Database, args: &[Value]) -> Result<R
554
577
  None => None,
555
578
  };
556
579
  let db = database_inner(ruby, rb_self)?;
557
- let prof = without_gvl(move || db.profile(&query, params_map))
558
- .map_err(|e| query_error_from_anyhow(ruby, e))?;
580
+ let prof = without_gvl_lora_result(ruby, move || db.profile(&query, params_map))?;
559
581
  query_profile_to_ruby(ruby, &prof)
560
582
  }
561
583
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lora-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.5
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LoraDB, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-06 00:00:00.000000000 Z
11
+ date: 2026-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys