slatedb 0.1.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.
@@ -0,0 +1,457 @@
1
+ use std::sync::Arc;
2
+
3
+ use magnus::prelude::*;
4
+ use magnus::{function, method, Error, RHash, Ruby};
5
+ use slatedb::config::{DurabilityLevel, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions};
6
+ use slatedb::object_store::memory::InMemory;
7
+ use slatedb::{Db, IsolationLevel};
8
+
9
+ use crate::errors::{invalid_argument_error, map_error};
10
+ use crate::iterator::Iterator;
11
+ use crate::runtime::block_on;
12
+ use crate::snapshot::Snapshot;
13
+ use crate::transaction::Transaction;
14
+ use crate::utils::get_optional;
15
+ use crate::write_batch::WriteBatch;
16
+
17
+ /// Ruby wrapper for SlateDB database.
18
+ ///
19
+ /// This struct is exposed to Ruby as `SlateDb::Database`.
20
+ #[magnus::wrap(class = "SlateDb::Database", free_immediately, size)]
21
+ pub struct Database {
22
+ inner: Arc<Db>,
23
+ }
24
+
25
+ impl Database {
26
+ /// Open a database at the given path.
27
+ ///
28
+ /// # Arguments
29
+ /// * `path` - The path identifier for the database
30
+ /// * `url` - Optional object store URL (e.g., "s3://bucket/path")
31
+ ///
32
+ /// # Returns
33
+ /// A new Database instance
34
+ pub fn open(path: String, url: Option<String>) -> Result<Self, Error> {
35
+ let db = block_on(async {
36
+ let object_store: Arc<dyn object_store::ObjectStore> = if let Some(ref url) = url {
37
+ Db::resolve_object_store(url).map_err(map_error)?
38
+ } else {
39
+ // Use in-memory store for local testing
40
+ Arc::new(InMemory::new())
41
+ };
42
+
43
+ Db::builder(path, object_store)
44
+ .build()
45
+ .await
46
+ .map_err(map_error)
47
+ })?;
48
+
49
+ Ok(Self {
50
+ inner: Arc::new(db),
51
+ })
52
+ }
53
+
54
+ /// Get a value by key.
55
+ ///
56
+ /// # Arguments
57
+ /// * `key` - The key to look up
58
+ ///
59
+ /// # Returns
60
+ /// The value as a String, or nil if not found
61
+ pub fn get(&self, key: String) -> Result<Option<String>, Error> {
62
+ if key.is_empty() {
63
+ return Err(invalid_argument_error("key cannot be empty"));
64
+ }
65
+
66
+ let opts = ReadOptions::default();
67
+
68
+ let result = block_on(async { self.inner.get_with_options(key.as_bytes(), &opts).await })
69
+ .map_err(map_error)?;
70
+
71
+ Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
72
+ }
73
+
74
+ /// Get a value by key with options.
75
+ ///
76
+ /// # Arguments
77
+ /// * `key` - The key to look up
78
+ /// * `kwargs` - Keyword arguments (durability_filter, dirty, cache_blocks)
79
+ ///
80
+ /// # Returns
81
+ /// The value as a String, or nil if not found
82
+ pub fn get_with_options(&self, key: String, kwargs: RHash) -> Result<Option<String>, Error> {
83
+ if key.is_empty() {
84
+ return Err(invalid_argument_error("key cannot be empty"));
85
+ }
86
+
87
+ let mut opts = ReadOptions::default();
88
+
89
+ // Parse durability_filter
90
+ if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
91
+ opts.durability_filter = match df.as_str() {
92
+ "remote" => DurabilityLevel::Remote,
93
+ "memory" => DurabilityLevel::Memory,
94
+ other => {
95
+ return Err(invalid_argument_error(&format!(
96
+ "invalid durability_filter: {} (expected 'remote' or 'memory')",
97
+ other
98
+ )))
99
+ }
100
+ };
101
+ }
102
+
103
+ // Parse dirty
104
+ if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
105
+ opts.dirty = dirty;
106
+ }
107
+
108
+ let result = block_on(async { self.inner.get_with_options(key.as_bytes(), &opts).await })
109
+ .map_err(map_error)?;
110
+
111
+ Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
112
+ }
113
+
114
+ /// Get a value by key as raw bytes.
115
+ ///
116
+ /// # Arguments
117
+ /// * `key` - The key to look up
118
+ ///
119
+ /// # Returns
120
+ /// The value as bytes, or nil if not found
121
+ pub fn get_bytes(&self, key: String) -> Result<Option<Vec<u8>>, Error> {
122
+ if key.is_empty() {
123
+ return Err(invalid_argument_error("key cannot be empty"));
124
+ }
125
+
126
+ let opts = ReadOptions::default();
127
+
128
+ let result = block_on(async { self.inner.get_with_options(key.as_bytes(), &opts).await })
129
+ .map_err(map_error)?;
130
+
131
+ Ok(result.map(|b| b.to_vec()))
132
+ }
133
+
134
+ /// Store a key-value pair.
135
+ ///
136
+ /// # Arguments
137
+ /// * `key` - The key to store
138
+ /// * `value` - The value to store
139
+ pub fn put(&self, key: String, value: String) -> Result<(), Error> {
140
+ if key.is_empty() {
141
+ return Err(invalid_argument_error("key cannot be empty"));
142
+ }
143
+
144
+ let put_opts = PutOptions { ttl: Ttl::Default };
145
+
146
+ let write_opts = WriteOptions {
147
+ await_durable: true,
148
+ };
149
+
150
+ block_on(async {
151
+ self.inner
152
+ .put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
153
+ .await
154
+ })
155
+ .map_err(map_error)?;
156
+
157
+ Ok(())
158
+ }
159
+
160
+ /// Store a key-value pair with options.
161
+ ///
162
+ /// # Arguments
163
+ /// * `key` - The key to store
164
+ /// * `value` - The value to store
165
+ /// * `kwargs` - Keyword arguments (ttl, await_durable)
166
+ pub fn put_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
167
+ if key.is_empty() {
168
+ return Err(invalid_argument_error("key cannot be empty"));
169
+ }
170
+
171
+ // Parse ttl
172
+ let ttl = get_optional::<u64>(&kwargs, "ttl")?;
173
+ let put_opts = PutOptions {
174
+ ttl: match ttl {
175
+ Some(ms) => Ttl::ExpireAfter(ms),
176
+ None => Ttl::Default,
177
+ },
178
+ };
179
+
180
+ // Parse await_durable
181
+ let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
182
+ let write_opts = WriteOptions { await_durable };
183
+
184
+ block_on(async {
185
+ self.inner
186
+ .put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
187
+ .await
188
+ })
189
+ .map_err(map_error)?;
190
+
191
+ Ok(())
192
+ }
193
+
194
+ /// Delete a key.
195
+ ///
196
+ /// # Arguments
197
+ /// * `key` - The key to delete
198
+ pub fn delete(&self, key: String) -> Result<(), Error> {
199
+ if key.is_empty() {
200
+ return Err(invalid_argument_error("key cannot be empty"));
201
+ }
202
+
203
+ let write_opts = WriteOptions {
204
+ await_durable: true,
205
+ };
206
+
207
+ block_on(async {
208
+ self.inner
209
+ .delete_with_options(key.as_bytes(), &write_opts)
210
+ .await
211
+ })
212
+ .map_err(map_error)?;
213
+
214
+ Ok(())
215
+ }
216
+
217
+ /// Delete a key with options.
218
+ ///
219
+ /// # Arguments
220
+ /// * `key` - The key to delete
221
+ /// * `kwargs` - Keyword arguments (await_durable)
222
+ pub fn delete_with_options(&self, key: String, kwargs: RHash) -> Result<(), Error> {
223
+ if key.is_empty() {
224
+ return Err(invalid_argument_error("key cannot be empty"));
225
+ }
226
+
227
+ let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
228
+ let write_opts = WriteOptions { await_durable };
229
+
230
+ block_on(async {
231
+ self.inner
232
+ .delete_with_options(key.as_bytes(), &write_opts)
233
+ .await
234
+ })
235
+ .map_err(map_error)?;
236
+
237
+ Ok(())
238
+ }
239
+
240
+ /// Scan a range of keys.
241
+ ///
242
+ /// # Arguments
243
+ /// * `start` - The start key (inclusive)
244
+ /// * `end_key` - Optional end key (exclusive). If not provided, scans to end.
245
+ ///
246
+ /// # Returns
247
+ /// An Iterator over key-value pairs
248
+ pub fn scan(&self, start: String, end_key: Option<String>) -> Result<Iterator, Error> {
249
+ if start.is_empty() {
250
+ return Err(invalid_argument_error("start key cannot be empty"));
251
+ }
252
+
253
+ let opts = ScanOptions::default();
254
+
255
+ let start_bytes = start.into_bytes();
256
+ let end_bytes = end_key.map(|e| e.into_bytes());
257
+
258
+ let iter = block_on(async {
259
+ let range = match end_bytes {
260
+ Some(end) => self.inner.scan_with_options(start_bytes..end, &opts).await,
261
+ None => self.inner.scan_with_options(start_bytes.., &opts).await,
262
+ };
263
+ range.map_err(map_error)
264
+ })?;
265
+
266
+ Ok(Iterator::new(iter))
267
+ }
268
+
269
+ /// Scan a range of keys with options.
270
+ ///
271
+ /// # Arguments
272
+ /// * `start` - The start key (inclusive)
273
+ /// * `end_key` - Optional end key (exclusive)
274
+ /// * `kwargs` - Keyword arguments (durability_filter, dirty, read_ahead_bytes, cache_blocks, max_fetch_tasks)
275
+ ///
276
+ /// # Returns
277
+ /// An Iterator over key-value pairs
278
+ pub fn scan_with_options(
279
+ &self,
280
+ start: String,
281
+ end_key: Option<String>,
282
+ kwargs: RHash,
283
+ ) -> Result<Iterator, Error> {
284
+ if start.is_empty() {
285
+ return Err(invalid_argument_error("start key cannot be empty"));
286
+ }
287
+
288
+ let mut opts = ScanOptions::default();
289
+
290
+ // Parse durability_filter
291
+ if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
292
+ opts.durability_filter = match df.as_str() {
293
+ "remote" => DurabilityLevel::Remote,
294
+ "memory" => DurabilityLevel::Memory,
295
+ other => {
296
+ return Err(invalid_argument_error(&format!(
297
+ "invalid durability_filter: {} (expected 'remote' or 'memory')",
298
+ other
299
+ )))
300
+ }
301
+ };
302
+ }
303
+
304
+ // Parse dirty
305
+ if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
306
+ opts.dirty = dirty;
307
+ }
308
+
309
+ // Parse read_ahead_bytes
310
+ if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
311
+ opts.read_ahead_bytes = rab;
312
+ }
313
+
314
+ // Parse cache_blocks
315
+ if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
316
+ opts.cache_blocks = cb;
317
+ }
318
+
319
+ // Parse max_fetch_tasks
320
+ if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
321
+ opts.max_fetch_tasks = mft;
322
+ }
323
+
324
+ let start_bytes = start.into_bytes();
325
+ let end_bytes = end_key.map(|e| e.into_bytes());
326
+
327
+ let iter = block_on(async {
328
+ let range = match end_bytes {
329
+ Some(end) => self.inner.scan_with_options(start_bytes..end, &opts).await,
330
+ None => self.inner.scan_with_options(start_bytes.., &opts).await,
331
+ };
332
+ range.map_err(map_error)
333
+ })?;
334
+
335
+ Ok(Iterator::new(iter))
336
+ }
337
+
338
+ /// Write a batch of operations atomically.
339
+ ///
340
+ /// # Arguments
341
+ /// * `batch` - The WriteBatch to write
342
+ pub fn write(&self, batch: &WriteBatch) -> Result<(), Error> {
343
+ let batch_inner = batch.take()?;
344
+
345
+ block_on(async { self.inner.write(batch_inner).await }).map_err(map_error)?;
346
+
347
+ Ok(())
348
+ }
349
+
350
+ /// Write a batch of operations atomically with options.
351
+ ///
352
+ /// # Arguments
353
+ /// * `batch` - The WriteBatch to write
354
+ /// * `kwargs` - Keyword arguments (await_durable)
355
+ pub fn write_with_options(&self, batch: &WriteBatch, kwargs: RHash) -> Result<(), Error> {
356
+ let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
357
+ let write_opts = WriteOptions { await_durable };
358
+
359
+ let batch_inner = batch.take()?;
360
+
361
+ block_on(async {
362
+ self.inner
363
+ .write_with_options(batch_inner, &write_opts)
364
+ .await
365
+ })
366
+ .map_err(map_error)?;
367
+
368
+ Ok(())
369
+ }
370
+
371
+ /// Begin a new transaction.
372
+ ///
373
+ /// # Arguments
374
+ /// * `isolation` - Optional isolation level ("snapshot" or "serializable")
375
+ ///
376
+ /// # Returns
377
+ /// A new Transaction instance
378
+ pub fn begin_transaction(&self, isolation: Option<String>) -> Result<Transaction, Error> {
379
+ let isolation_level = match isolation.as_deref().unwrap_or("snapshot") {
380
+ "snapshot" | "si" => IsolationLevel::Snapshot,
381
+ "serializable" | "ssi" | "serializable_snapshot" => {
382
+ IsolationLevel::SerializableSnapshot
383
+ }
384
+ other => {
385
+ return Err(invalid_argument_error(&format!(
386
+ "invalid isolation level: {} (expected 'snapshot' or 'serializable')",
387
+ other
388
+ )))
389
+ }
390
+ };
391
+
392
+ let txn = block_on(async { self.inner.begin(isolation_level).await }).map_err(map_error)?;
393
+
394
+ Ok(Transaction::new(txn))
395
+ }
396
+
397
+ /// Create a snapshot for consistent reads.
398
+ ///
399
+ /// # Returns
400
+ /// A new Snapshot instance
401
+ pub fn snapshot(&self) -> Result<Snapshot, Error> {
402
+ let snap = block_on(async { self.inner.snapshot().await }).map_err(map_error)?;
403
+
404
+ Ok(Snapshot::new(snap))
405
+ }
406
+
407
+ /// Flush the database to ensure durability.
408
+ pub fn flush(&self) -> Result<(), Error> {
409
+ block_on(async { self.inner.flush().await }).map_err(map_error)?;
410
+ Ok(())
411
+ }
412
+
413
+ /// Close the database.
414
+ pub fn close(&self) -> Result<(), Error> {
415
+ block_on(async { self.inner.close().await }).map_err(map_error)?;
416
+ Ok(())
417
+ }
418
+ }
419
+
420
+ /// Define the Database class on the SlateDb module.
421
+ pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
422
+ let class = module.define_class("Database", ruby.class_object())?;
423
+
424
+ // Class methods
425
+ class.define_singleton_method("_open", function!(Database::open, 2))?;
426
+
427
+ // Instance methods - simple versions
428
+ class.define_method("_get", method!(Database::get, 1))?;
429
+ class.define_method("_get_with_options", method!(Database::get_with_options, 2))?;
430
+ class.define_method("get_bytes", method!(Database::get_bytes, 1))?;
431
+ class.define_method("_put", method!(Database::put, 2))?;
432
+ class.define_method("_put_with_options", method!(Database::put_with_options, 3))?;
433
+ class.define_method("_delete", method!(Database::delete, 1))?;
434
+ class.define_method(
435
+ "_delete_with_options",
436
+ method!(Database::delete_with_options, 2),
437
+ )?;
438
+ class.define_method("_scan", method!(Database::scan, 2))?;
439
+ class.define_method(
440
+ "_scan_with_options",
441
+ method!(Database::scan_with_options, 3),
442
+ )?;
443
+ class.define_method("_write", method!(Database::write, 1))?;
444
+ class.define_method(
445
+ "_write_with_options",
446
+ method!(Database::write_with_options, 2),
447
+ )?;
448
+ class.define_method(
449
+ "_begin_transaction",
450
+ method!(Database::begin_transaction, 1),
451
+ )?;
452
+ class.define_method("_snapshot", method!(Database::snapshot, 0))?;
453
+ class.define_method("flush", method!(Database::flush, 0))?;
454
+ class.define_method("close", method!(Database::close, 0))?;
455
+
456
+ Ok(())
457
+ }
@@ -0,0 +1,144 @@
1
+ use magnus::prelude::*;
2
+ use magnus::{Error, ExceptionClass, Ruby};
3
+ use slatedb::Error as SlateError;
4
+ use slatedb::ErrorKind;
5
+ use std::cell::RefCell;
6
+
7
+ // Store exception classes in thread-local storage for access during error mapping
8
+ thread_local! {
9
+ static SLATE_DB_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
10
+ static TRANSACTION_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
11
+ static CLOSED_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
12
+ static UNAVAILABLE_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
13
+ static INVALID_ARGUMENT_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
14
+ static DATA_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
15
+ static INTERNAL_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
16
+ }
17
+
18
+ /// Define SlateDB exception classes under the SlateDb module.
19
+ ///
20
+ /// Exception hierarchy:
21
+ /// - SlateDb::Error (base class, inherits from StandardError)
22
+ /// - SlateDb::TransactionError
23
+ /// - SlateDb::ClosedError
24
+ /// - SlateDb::UnavailableError
25
+ /// - SlateDb::InvalidArgumentError
26
+ /// - SlateDb::DataError
27
+ /// - SlateDb::InternalError
28
+ pub fn define_exceptions(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
29
+ let standard_error = ruby.exception_standard_error();
30
+
31
+ // Define base SlateDb::Error
32
+ let slate_error = module.define_error("Error", standard_error)?;
33
+ SLATE_DB_ERROR.with(|cell| {
34
+ *cell.borrow_mut() = Some(slate_error);
35
+ });
36
+
37
+ // Define specific error types
38
+ let transaction_error = module.define_error("TransactionError", slate_error)?;
39
+ TRANSACTION_ERROR.with(|cell| {
40
+ *cell.borrow_mut() = Some(transaction_error);
41
+ });
42
+
43
+ let closed_error = module.define_error("ClosedError", slate_error)?;
44
+ CLOSED_ERROR.with(|cell| {
45
+ *cell.borrow_mut() = Some(closed_error);
46
+ });
47
+
48
+ let unavailable_error = module.define_error("UnavailableError", slate_error)?;
49
+ UNAVAILABLE_ERROR.with(|cell| {
50
+ *cell.borrow_mut() = Some(unavailable_error);
51
+ });
52
+
53
+ let invalid_argument_error = module.define_error("InvalidArgumentError", slate_error)?;
54
+ INVALID_ARGUMENT_ERROR.with(|cell| {
55
+ *cell.borrow_mut() = Some(invalid_argument_error);
56
+ });
57
+
58
+ let data_error = module.define_error("DataError", slate_error)?;
59
+ DATA_ERROR.with(|cell| {
60
+ *cell.borrow_mut() = Some(data_error);
61
+ });
62
+
63
+ let internal_error = module.define_error("InternalError", slate_error)?;
64
+ INTERNAL_ERROR.with(|cell| {
65
+ *cell.borrow_mut() = Some(internal_error);
66
+ });
67
+
68
+ Ok(())
69
+ }
70
+
71
+ /// Map a SlateDB error to the appropriate Ruby exception.
72
+ pub fn map_error(err: SlateError) -> Error {
73
+ let msg = format!("{}", err);
74
+ let ruby = Ruby::get().expect("Ruby runtime not available");
75
+
76
+ match err.kind() {
77
+ ErrorKind::Transaction => TRANSACTION_ERROR.with(|cell| {
78
+ cell.borrow()
79
+ .map(|exc| Error::new(exc, msg.clone()))
80
+ .unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
81
+ }),
82
+ ErrorKind::Closed(_) => CLOSED_ERROR.with(|cell| {
83
+ cell.borrow()
84
+ .map(|exc| Error::new(exc, msg.clone()))
85
+ .unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
86
+ }),
87
+ ErrorKind::Unavailable => UNAVAILABLE_ERROR.with(|cell| {
88
+ cell.borrow()
89
+ .map(|exc| Error::new(exc, msg.clone()))
90
+ .unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
91
+ }),
92
+ ErrorKind::Invalid => INVALID_ARGUMENT_ERROR.with(|cell| {
93
+ cell.borrow()
94
+ .map(|exc| Error::new(exc, msg.clone()))
95
+ .unwrap_or_else(|| Error::new(ruby.exception_arg_error(), msg.clone()))
96
+ }),
97
+ ErrorKind::Data => DATA_ERROR.with(|cell| {
98
+ cell.borrow()
99
+ .map(|exc| Error::new(exc, msg.clone()))
100
+ .unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
101
+ }),
102
+ ErrorKind::Internal => INTERNAL_ERROR.with(|cell| {
103
+ cell.borrow()
104
+ .map(|exc| Error::new(exc, msg.clone()))
105
+ .unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
106
+ }),
107
+ _ => INTERNAL_ERROR.with(|cell| {
108
+ cell.borrow()
109
+ .map(|exc| Error::new(exc, msg.clone()))
110
+ .unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
111
+ }),
112
+ }
113
+ }
114
+
115
+ /// Create an InvalidArgumentError with the given message.
116
+ pub fn invalid_argument_error(msg: &str) -> Error {
117
+ let ruby = Ruby::get().expect("Ruby runtime not available");
118
+ INVALID_ARGUMENT_ERROR.with(|cell| {
119
+ cell.borrow()
120
+ .map(|exc| Error::new(exc, msg.to_string()))
121
+ .unwrap_or_else(|| Error::new(ruby.exception_arg_error(), msg.to_string()))
122
+ })
123
+ }
124
+
125
+ /// Create an InternalError with the given message.
126
+ #[allow(dead_code)]
127
+ pub fn internal_error(msg: &str) -> Error {
128
+ let ruby = Ruby::get().expect("Ruby runtime not available");
129
+ INTERNAL_ERROR.with(|cell| {
130
+ cell.borrow()
131
+ .map(|exc| Error::new(exc, msg.to_string()))
132
+ .unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.to_string()))
133
+ })
134
+ }
135
+
136
+ /// Create a ClosedError with the given message.
137
+ pub fn closed_error(msg: &str) -> Error {
138
+ let ruby = Ruby::get().expect("Ruby runtime not available");
139
+ CLOSED_ERROR.with(|cell| {
140
+ cell.borrow()
141
+ .map(|exc| Error::new(exc, msg.to_string()))
142
+ .unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.to_string()))
143
+ })
144
+ }