slatedb 0.1.1 → 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/README.md +219 -4
- data/ext/slatedb/Cargo.toml +10 -10
- data/ext/slatedb/src/admin.rs +20 -5
- data/ext/slatedb/src/database.rs +376 -39
- data/ext/slatedb/src/iterator.rs +4 -1
- data/ext/slatedb/src/lib.rs +3 -0
- data/ext/slatedb/src/merge_ops.rs +240 -0
- data/ext/slatedb/src/metrics.rs +47 -0
- data/ext/slatedb/src/reader.rs +118 -7
- data/ext/slatedb/src/runtime.rs +52 -1
- data/ext/slatedb/src/snapshot.rs +105 -0
- data/ext/slatedb/src/transaction.rs +189 -9
- data/ext/slatedb/src/utils.rs +5 -4
- data/ext/slatedb/src/write_batch.rs +48 -1
- data/lib/slatedb/database.rb +205 -19
- data/lib/slatedb/metrics.rb +20 -0
- data/lib/slatedb/reader.rb +50 -1
- data/lib/slatedb/snapshot.rb +32 -0
- data/lib/slatedb/transaction.rb +96 -0
- data/lib/slatedb/version.rb +1 -1
- data/lib/slatedb/write_batch.rb +20 -0
- data/lib/slatedb.rb +1 -0
- metadata +16 -13
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
use std::thread;
|
|
3
|
+
|
|
4
|
+
use bytes::Bytes;
|
|
5
|
+
use log::error;
|
|
6
|
+
use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
7
|
+
use magnus::value::ReprValue;
|
|
8
|
+
use magnus::{Error, RHash, Ruby, Value};
|
|
9
|
+
use slatedb::{MergeOperator, MergeOperatorError};
|
|
10
|
+
|
|
11
|
+
use crate::errors::invalid_argument_error;
|
|
12
|
+
use crate::utils::get_optional;
|
|
13
|
+
|
|
14
|
+
struct StringConcatMergeOperator;
|
|
15
|
+
|
|
16
|
+
impl MergeOperator for StringConcatMergeOperator {
|
|
17
|
+
fn merge(
|
|
18
|
+
&self,
|
|
19
|
+
_key: &Bytes,
|
|
20
|
+
existing_value: Option<Bytes>,
|
|
21
|
+
value: Bytes,
|
|
22
|
+
) -> Result<Bytes, MergeOperatorError> {
|
|
23
|
+
let mut result = existing_value.unwrap_or_default().to_vec();
|
|
24
|
+
result.extend_from_slice(&value);
|
|
25
|
+
Ok(Bytes::from(result))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn merge_batch(
|
|
29
|
+
&self,
|
|
30
|
+
_key: &Bytes,
|
|
31
|
+
existing_value: Option<Bytes>,
|
|
32
|
+
operands: &[Bytes],
|
|
33
|
+
) -> Result<Bytes, MergeOperatorError> {
|
|
34
|
+
let mut result = existing_value.unwrap_or_default().to_vec();
|
|
35
|
+
for operand in operands {
|
|
36
|
+
result.extend_from_slice(operand);
|
|
37
|
+
}
|
|
38
|
+
Ok(Bytes::from(result))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// A merge operator that calls a Ruby block/proc.
|
|
43
|
+
///
|
|
44
|
+
/// This stores the raw Ruby VALUE and calls it via `with_gvl` when merge
|
|
45
|
+
/// operations are needed. The proc is called with (key, existing_value, new_value)
|
|
46
|
+
/// and should return the merged value as a String.
|
|
47
|
+
///
|
|
48
|
+
/// # Thread Safety
|
|
49
|
+
///
|
|
50
|
+
/// The Ruby proc can only be called from the Ruby thread that created this operator.
|
|
51
|
+
/// If the merge is called from a different thread (e.g., a Tokio worker thread during
|
|
52
|
+
/// background compaction), the merge will use a fallback string concatenation behavior.
|
|
53
|
+
///
|
|
54
|
+
/// # Safety
|
|
55
|
+
///
|
|
56
|
+
/// The Ruby proc must be kept alive (not garbage collected) for the lifetime
|
|
57
|
+
/// of this operator. This is typically handled by storing a reference to the
|
|
58
|
+
/// proc in the Ruby Database object.
|
|
59
|
+
pub struct RubyProcMergeOperator {
|
|
60
|
+
/// The raw Ruby VALUE of the proc. We store this as a raw value because
|
|
61
|
+
/// magnus::Value is not Send+Sync, but we need to be thread-safe.
|
|
62
|
+
/// We re-acquire the GVL before using it, which makes this safe.
|
|
63
|
+
proc_value: usize,
|
|
64
|
+
/// The thread ID of the Ruby thread that created this operator.
|
|
65
|
+
/// We can only safely call Ruby from this thread.
|
|
66
|
+
ruby_thread_id: thread::ThreadId,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// SAFETY: We only access the proc_value when we hold the GVL via the Ruby thread,
|
|
70
|
+
// which ensures thread-safe access to Ruby objects.
|
|
71
|
+
unsafe impl Send for RubyProcMergeOperator {}
|
|
72
|
+
unsafe impl Sync for RubyProcMergeOperator {}
|
|
73
|
+
|
|
74
|
+
impl RubyProcMergeOperator {
|
|
75
|
+
/// Create a new RubyProcMergeOperator from a Ruby proc/block.
|
|
76
|
+
///
|
|
77
|
+
/// # Safety
|
|
78
|
+
///
|
|
79
|
+
/// The caller must ensure the proc remains alive (not GC'd) for the
|
|
80
|
+
/// lifetime of this operator.
|
|
81
|
+
pub fn new(proc: Value) -> Self {
|
|
82
|
+
Self {
|
|
83
|
+
proc_value: proc.as_raw() as usize,
|
|
84
|
+
ruby_thread_id: thread::current().id(),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Check if we're on the Ruby thread that created this operator.
|
|
89
|
+
fn is_ruby_thread(&self) -> bool {
|
|
90
|
+
thread::current().id() == self.ruby_thread_id
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Call the Ruby proc with the given arguments.
|
|
94
|
+
/// This must only be called from the Ruby thread (after checking is_ruby_thread).
|
|
95
|
+
fn call_proc_on_ruby_thread(
|
|
96
|
+
&self,
|
|
97
|
+
key: &str,
|
|
98
|
+
existing_value: Option<&str>,
|
|
99
|
+
new_value: &str,
|
|
100
|
+
) -> Result<Bytes, MergeOperatorError> {
|
|
101
|
+
// We're on the Ruby thread, so we can use with_gvl
|
|
102
|
+
// Import here to avoid the circular dependency at module level
|
|
103
|
+
use crate::runtime::with_gvl;
|
|
104
|
+
|
|
105
|
+
let key_owned = key.to_string();
|
|
106
|
+
let existing_owned = existing_value.map(|s| s.to_string());
|
|
107
|
+
let new_owned = new_value.to_string();
|
|
108
|
+
|
|
109
|
+
with_gvl(|| {
|
|
110
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
111
|
+
|
|
112
|
+
// Reconstruct the proc Value from the raw pointer
|
|
113
|
+
let proc = unsafe { Value::from_raw(self.proc_value as _) };
|
|
114
|
+
|
|
115
|
+
// Build arguments: (key, existing_value, new_value)
|
|
116
|
+
let existing_arg: Value = match &existing_owned {
|
|
117
|
+
Some(s) => ruby.str_new(s).as_value(),
|
|
118
|
+
None => ruby.qnil().as_value(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Call the proc
|
|
122
|
+
let result: Result<String, magnus::Error> = proc.funcall(
|
|
123
|
+
"call",
|
|
124
|
+
(
|
|
125
|
+
ruby.str_new(&key_owned),
|
|
126
|
+
existing_arg,
|
|
127
|
+
ruby.str_new(&new_owned),
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
match result {
|
|
132
|
+
Ok(merged) => Ok(Bytes::from(merged)),
|
|
133
|
+
Err(e) => {
|
|
134
|
+
error!("Ruby merge operator error: {}", e);
|
|
135
|
+
Err(MergeOperatorError::EmptyBatch)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Fallback merge when we're not on the Ruby thread.
|
|
142
|
+
/// Uses simple concatenation as a safe default.
|
|
143
|
+
fn fallback_merge(
|
|
144
|
+
&self,
|
|
145
|
+
existing_value: Option<&Bytes>,
|
|
146
|
+
new_value: &Bytes,
|
|
147
|
+
) -> Result<Bytes, MergeOperatorError> {
|
|
148
|
+
error!(
|
|
149
|
+
"Ruby merge operator called from non-Ruby thread, using fallback concatenation. \
|
|
150
|
+
This can happen during background compaction."
|
|
151
|
+
);
|
|
152
|
+
let mut result = existing_value.map(|v| v.to_vec()).unwrap_or_default();
|
|
153
|
+
result.extend_from_slice(new_value);
|
|
154
|
+
Ok(Bytes::from(result))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Call the Ruby proc with the given arguments, handling thread safety.
|
|
158
|
+
fn call_proc(
|
|
159
|
+
&self,
|
|
160
|
+
key: &Bytes,
|
|
161
|
+
existing_value: Option<&Bytes>,
|
|
162
|
+
new_value: &Bytes,
|
|
163
|
+
) -> Result<Bytes, MergeOperatorError> {
|
|
164
|
+
let key_str = String::from_utf8_lossy(key);
|
|
165
|
+
let existing_str = existing_value.map(|v| String::from_utf8_lossy(v));
|
|
166
|
+
let new_str = String::from_utf8_lossy(new_value);
|
|
167
|
+
|
|
168
|
+
if self.is_ruby_thread() {
|
|
169
|
+
self.call_proc_on_ruby_thread(&key_str, existing_str.as_deref(), &new_str)
|
|
170
|
+
} else {
|
|
171
|
+
// We're on a worker thread, use fallback
|
|
172
|
+
self.fallback_merge(existing_value, new_value)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
impl MergeOperator for RubyProcMergeOperator {
|
|
178
|
+
fn merge(
|
|
179
|
+
&self,
|
|
180
|
+
key: &Bytes,
|
|
181
|
+
existing_value: Option<Bytes>,
|
|
182
|
+
value: Bytes,
|
|
183
|
+
) -> Result<Bytes, MergeOperatorError> {
|
|
184
|
+
self.call_proc(key, existing_value.as_ref(), &value)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fn merge_batch(
|
|
188
|
+
&self,
|
|
189
|
+
key: &Bytes,
|
|
190
|
+
existing_value: Option<Bytes>,
|
|
191
|
+
operands: &[Bytes],
|
|
192
|
+
) -> Result<Bytes, MergeOperatorError> {
|
|
193
|
+
// Apply operands one at a time through the Ruby proc
|
|
194
|
+
let mut current = existing_value;
|
|
195
|
+
for operand in operands {
|
|
196
|
+
current = Some(self.call_proc(key, current.as_ref(), operand)?);
|
|
197
|
+
}
|
|
198
|
+
Ok(current.unwrap_or_default())
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
pub fn parse_merge_operator(
|
|
203
|
+
kwargs: &RHash,
|
|
204
|
+
) -> Result<Option<Arc<dyn MergeOperator + Send + Sync>>, Error> {
|
|
205
|
+
let merge_operator = get_optional::<String>(kwargs, "merge_operator")?;
|
|
206
|
+
let Some(merge_operator) = merge_operator else {
|
|
207
|
+
return Ok(None);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
let operator: Arc<dyn MergeOperator + Send + Sync> = match merge_operator.as_str() {
|
|
211
|
+
"string_concat" | "concat" => Arc::new(StringConcatMergeOperator),
|
|
212
|
+
_ => {
|
|
213
|
+
return Err(invalid_argument_error(&format!(
|
|
214
|
+
"invalid merge_operator: {} (expected 'string_concat', 'concat', or use merge_operator_proc for a custom block)",
|
|
215
|
+
merge_operator
|
|
216
|
+
)))
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
Ok(Some(operator))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Parse a Ruby proc as a merge operator.
|
|
224
|
+
pub fn parse_merge_operator_proc(
|
|
225
|
+
kwargs: &RHash,
|
|
226
|
+
) -> Result<Option<Arc<dyn MergeOperator + Send + Sync>>, Error> {
|
|
227
|
+
let proc_value = get_optional::<Value>(kwargs, "merge_operator_proc")?;
|
|
228
|
+
let Some(proc) = proc_value else {
|
|
229
|
+
return Ok(None);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Verify it's callable
|
|
233
|
+
if !proc.respond_to("call", false).unwrap_or(false) {
|
|
234
|
+
return Err(invalid_argument_error(
|
|
235
|
+
"merge_operator_proc must respond to 'call'",
|
|
236
|
+
));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
Ok(Some(Arc::new(RubyProcMergeOperator::new(proc))))
|
|
240
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::sync::{Arc, Mutex};
|
|
3
|
+
|
|
4
|
+
use magnus::prelude::*;
|
|
5
|
+
use magnus::{method, Error, Ruby};
|
|
6
|
+
/// Ruby wrapper for SlateDB metrics registry.
|
|
7
|
+
///
|
|
8
|
+
/// This struct is exposed to Ruby as `SlateDb::Metrics`.
|
|
9
|
+
#[magnus::wrap(class = "SlateDb::Metrics", free_immediately, size)]
|
|
10
|
+
pub struct Metrics {
|
|
11
|
+
inner: Arc<Mutex<HashMap<String, i64>>>,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
impl Metrics {
|
|
15
|
+
pub fn new(inner: Arc<Mutex<HashMap<String, i64>>>) -> Self {
|
|
16
|
+
Self { inner }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Return a list of metric names.
|
|
20
|
+
pub fn names(&self) -> Result<magnus::RArray, Error> {
|
|
21
|
+
let metrics = self.inner.lock().expect("metrics mutex poisoned");
|
|
22
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
23
|
+
let result = ruby.ary_new_capa(metrics.len());
|
|
24
|
+
|
|
25
|
+
for name in metrics.keys() {
|
|
26
|
+
result.push(ruby.str_new(name))?;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Ok(result)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Get the current value of a metric by name.
|
|
33
|
+
pub fn get(&self, name: String) -> Result<Option<i64>, Error> {
|
|
34
|
+
let metrics = self.inner.lock().expect("metrics mutex poisoned");
|
|
35
|
+
Ok(metrics.get(&name).copied())
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Define the Metrics class on the SlateDb module.
|
|
40
|
+
pub fn define_metrics_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
41
|
+
let class = module.define_class("Metrics", ruby.class_object())?;
|
|
42
|
+
|
|
43
|
+
class.define_method("names", method!(Metrics::names, 0))?;
|
|
44
|
+
class.define_method("get", method!(Metrics::get, 1))?;
|
|
45
|
+
|
|
46
|
+
Ok(())
|
|
47
|
+
}
|
data/ext/slatedb/src/reader.rs
CHANGED
|
@@ -4,6 +4,7 @@ use magnus::prelude::*;
|
|
|
4
4
|
use magnus::{function, method, Error, RHash, Ruby};
|
|
5
5
|
use slatedb::config::{DbReaderOptions, DurabilityLevel, ReadOptions, ScanOptions};
|
|
6
6
|
use slatedb::DbReader;
|
|
7
|
+
use slatedb::IterationOrder;
|
|
7
8
|
|
|
8
9
|
use crate::errors::invalid_argument_error;
|
|
9
10
|
use crate::iterator::Iterator;
|
|
@@ -26,7 +27,10 @@ impl Reader {
|
|
|
26
27
|
/// * `path` - The path identifier for the database
|
|
27
28
|
/// * `url` - Optional object store URL
|
|
28
29
|
/// * `checkpoint_id` - Optional checkpoint UUID to read at
|
|
29
|
-
/// * `kwargs` - Additional options (manifest_poll_interval, checkpoint_lifetime,
|
|
30
|
+
/// * `kwargs` - Additional options (manifest_poll_interval, checkpoint_lifetime,
|
|
31
|
+
/// max_memtable_bytes, skip_wal_replay, cache_root, max_open_file_handles).
|
|
32
|
+
/// The local disk cache (and therefore `max_open_file_handles`) is only active
|
|
33
|
+
/// when `cache_root` is set.
|
|
30
34
|
pub fn open(
|
|
31
35
|
path: String,
|
|
32
36
|
url: Option<String>,
|
|
@@ -39,6 +43,9 @@ impl Reader {
|
|
|
39
43
|
let checkpoint_lifetime = get_optional::<u64>(&kwargs, "checkpoint_lifetime")?
|
|
40
44
|
.map(std::time::Duration::from_millis);
|
|
41
45
|
let max_memtable_bytes = get_optional::<u64>(&kwargs, "max_memtable_bytes")?;
|
|
46
|
+
let skip_wal_replay = get_optional::<bool>(&kwargs, "skip_wal_replay")?;
|
|
47
|
+
let max_open_file_handles = get_optional::<usize>(&kwargs, "max_open_file_handles")?;
|
|
48
|
+
let cache_root = get_optional::<String>(&kwargs, "cache_root")?;
|
|
42
49
|
|
|
43
50
|
// Parse checkpoint_id as UUID
|
|
44
51
|
let checkpoint_uuid =
|
|
@@ -51,11 +58,12 @@ impl Reader {
|
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
let reader = block_on_result(async {
|
|
54
|
-
let object_store: Arc<dyn object_store::ObjectStore> =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
let object_store: Arc<dyn slatedb::object_store::ObjectStore> =
|
|
62
|
+
if let Some(ref url) = url {
|
|
63
|
+
resolve_object_store(url)?
|
|
64
|
+
} else {
|
|
65
|
+
Arc::new(slatedb::object_store::memory::InMemory::new())
|
|
66
|
+
};
|
|
59
67
|
|
|
60
68
|
let mut options = DbReaderOptions::default();
|
|
61
69
|
if let Some(interval) = manifest_poll_interval {
|
|
@@ -67,7 +75,16 @@ impl Reader {
|
|
|
67
75
|
if let Some(max_bytes) = max_memtable_bytes {
|
|
68
76
|
options.max_memtable_bytes = max_bytes;
|
|
69
77
|
}
|
|
70
|
-
|
|
78
|
+
if let Some(skip_replay) = skip_wal_replay {
|
|
79
|
+
options.skip_wal_replay = skip_replay;
|
|
80
|
+
}
|
|
81
|
+
if let Some(ref root) = cache_root {
|
|
82
|
+
options.object_store_cache_options.root_folder =
|
|
83
|
+
Some(std::path::PathBuf::from(root));
|
|
84
|
+
}
|
|
85
|
+
if let Some(max_handles) = max_open_file_handles {
|
|
86
|
+
options.object_store_cache_options.max_open_file_handles = max_handles;
|
|
87
|
+
}
|
|
71
88
|
DbReader::open(path, object_store, checkpoint_uuid, options).await
|
|
72
89
|
})?;
|
|
73
90
|
|
|
@@ -111,6 +128,10 @@ impl Reader {
|
|
|
111
128
|
opts.dirty = dirty;
|
|
112
129
|
}
|
|
113
130
|
|
|
131
|
+
if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
|
|
132
|
+
opts.cache_blocks = cb;
|
|
133
|
+
}
|
|
134
|
+
|
|
114
135
|
let result =
|
|
115
136
|
block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
|
|
116
137
|
Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
|
|
@@ -186,6 +207,18 @@ impl Reader {
|
|
|
186
207
|
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
187
208
|
opts.max_fetch_tasks = mft;
|
|
188
209
|
}
|
|
210
|
+
if let Some(order) = get_optional::<String>(&kwargs, "order")? {
|
|
211
|
+
opts.order = match order.as_str() {
|
|
212
|
+
"ascending" | "asc" => IterationOrder::Ascending,
|
|
213
|
+
"descending" | "desc" => IterationOrder::Descending,
|
|
214
|
+
other => {
|
|
215
|
+
return Err(invalid_argument_error(&format!(
|
|
216
|
+
"invalid order: {} (expected 'asc' or 'desc')",
|
|
217
|
+
other
|
|
218
|
+
)))
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
189
222
|
|
|
190
223
|
let start_bytes = start.into_bytes();
|
|
191
224
|
let end_bytes = end_key.map(|e| e.into_bytes());
|
|
@@ -200,6 +233,79 @@ impl Reader {
|
|
|
200
233
|
Ok(Iterator::new(iter))
|
|
201
234
|
}
|
|
202
235
|
|
|
236
|
+
/// Scan all keys with a given prefix.
|
|
237
|
+
pub fn scan_prefix(&self, prefix: String) -> Result<Iterator, Error> {
|
|
238
|
+
if prefix.is_empty() {
|
|
239
|
+
return Err(invalid_argument_error("prefix cannot be empty"));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let iter = block_on_result(async { self.inner.scan_prefix(prefix.as_bytes()).await })?;
|
|
243
|
+
|
|
244
|
+
Ok(Iterator::new(iter))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Scan all keys with a given prefix with options.
|
|
248
|
+
pub fn scan_prefix_with_options(
|
|
249
|
+
&self,
|
|
250
|
+
prefix: String,
|
|
251
|
+
kwargs: RHash,
|
|
252
|
+
) -> Result<Iterator, Error> {
|
|
253
|
+
if prefix.is_empty() {
|
|
254
|
+
return Err(invalid_argument_error("prefix cannot be empty"));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let mut opts = ScanOptions::default();
|
|
258
|
+
|
|
259
|
+
if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
|
|
260
|
+
opts.durability_filter = match df.as_str() {
|
|
261
|
+
"remote" => DurabilityLevel::Remote,
|
|
262
|
+
"memory" => DurabilityLevel::Memory,
|
|
263
|
+
other => {
|
|
264
|
+
return Err(invalid_argument_error(&format!(
|
|
265
|
+
"invalid durability_filter: {} (expected 'remote' or 'memory')",
|
|
266
|
+
other
|
|
267
|
+
)))
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
|
|
273
|
+
opts.dirty = dirty;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
|
|
277
|
+
opts.read_ahead_bytes = rab;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
|
|
281
|
+
opts.cache_blocks = cb;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
285
|
+
opts.max_fetch_tasks = mft;
|
|
286
|
+
}
|
|
287
|
+
if let Some(order) = get_optional::<String>(&kwargs, "order")? {
|
|
288
|
+
opts.order = match order.as_str() {
|
|
289
|
+
"ascending" | "asc" => IterationOrder::Ascending,
|
|
290
|
+
"descending" | "desc" => IterationOrder::Descending,
|
|
291
|
+
other => {
|
|
292
|
+
return Err(invalid_argument_error(&format!(
|
|
293
|
+
"invalid order: {} (expected 'asc' or 'desc')",
|
|
294
|
+
other
|
|
295
|
+
)))
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let iter = block_on_result(async {
|
|
301
|
+
self.inner
|
|
302
|
+
.scan_prefix_with_options(prefix.as_bytes(), &opts)
|
|
303
|
+
.await
|
|
304
|
+
})?;
|
|
305
|
+
|
|
306
|
+
Ok(Iterator::new(iter))
|
|
307
|
+
}
|
|
308
|
+
|
|
203
309
|
/// Close the reader.
|
|
204
310
|
pub fn close(&self) -> Result<(), Error> {
|
|
205
311
|
block_on_result(async { self.inner.close().await })?;
|
|
@@ -220,6 +326,11 @@ pub fn define_reader_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(),
|
|
|
220
326
|
class.define_method("get_bytes", method!(Reader::get_bytes, 1))?;
|
|
221
327
|
class.define_method("_scan", method!(Reader::scan, 2))?;
|
|
222
328
|
class.define_method("_scan_with_options", method!(Reader::scan_with_options, 3))?;
|
|
329
|
+
class.define_method("_scan_prefix", method!(Reader::scan_prefix, 1))?;
|
|
330
|
+
class.define_method(
|
|
331
|
+
"_scan_prefix_with_options",
|
|
332
|
+
method!(Reader::scan_prefix_with_options, 2),
|
|
333
|
+
)?;
|
|
223
334
|
class.define_method("close", method!(Reader::close, 0))?;
|
|
224
335
|
|
|
225
336
|
Ok(())
|
data/ext/slatedb/src/runtime.rs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use magnus::Error;
|
|
2
2
|
use once_cell::sync::OnceCell;
|
|
3
|
-
use rb_sys::rb_thread_call_without_gvl;
|
|
3
|
+
use rb_sys::{rb_thread_call_with_gvl, rb_thread_call_without_gvl};
|
|
4
4
|
use slatedb::Error as SlateError;
|
|
5
5
|
use std::ffi::c_void;
|
|
6
6
|
use std::future::Future;
|
|
@@ -98,3 +98,54 @@ where
|
|
|
98
98
|
|
|
99
99
|
closure.result.expect("closure did not run")
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/// Execute a closure while holding the Ruby GVL.
|
|
103
|
+
///
|
|
104
|
+
/// This is the inverse of `without_gvl`. Use this when you need to call back
|
|
105
|
+
/// into Ruby from code that has previously released the GVL (e.g., inside
|
|
106
|
+
/// a future being executed by `block_on`).
|
|
107
|
+
///
|
|
108
|
+
/// # Safety
|
|
109
|
+
///
|
|
110
|
+
/// This function can ONLY be called from a Ruby thread that has previously
|
|
111
|
+
/// released the GVL via `without_gvl` or `rb_thread_call_without_gvl`.
|
|
112
|
+
/// Calling it from a non-Ruby thread (like a spawned Tokio task) will cause
|
|
113
|
+
/// a Ruby fatal error.
|
|
114
|
+
///
|
|
115
|
+
/// # Panics
|
|
116
|
+
///
|
|
117
|
+
/// Panics if called from a non-Ruby thread.
|
|
118
|
+
pub fn with_gvl<F, T>(f: F) -> T
|
|
119
|
+
where
|
|
120
|
+
F: FnOnce() -> T,
|
|
121
|
+
{
|
|
122
|
+
struct Closure<F, T> {
|
|
123
|
+
f: Option<F>,
|
|
124
|
+
result: Option<T>,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
extern "C" fn call_closure<F, T>(data: *mut c_void) -> *mut c_void
|
|
128
|
+
where
|
|
129
|
+
F: FnOnce() -> T,
|
|
130
|
+
{
|
|
131
|
+
let closure = unsafe { &mut *(data as *mut Closure<F, T>) };
|
|
132
|
+
if let Some(f) = closure.f.take() {
|
|
133
|
+
closure.result = Some(f());
|
|
134
|
+
}
|
|
135
|
+
std::ptr::null_mut()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let mut closure = Closure {
|
|
139
|
+
f: Some(f),
|
|
140
|
+
result: None,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
unsafe {
|
|
144
|
+
rb_thread_call_with_gvl(
|
|
145
|
+
Some(call_closure::<F, T>),
|
|
146
|
+
&mut closure as *mut _ as *mut c_void,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
closure.result.expect("closure did not run")
|
|
151
|
+
}
|
data/ext/slatedb/src/snapshot.rs
CHANGED
|
@@ -5,6 +5,7 @@ use magnus::prelude::*;
|
|
|
5
5
|
use magnus::{method, Error, RHash, Ruby};
|
|
6
6
|
use slatedb::config::{DurabilityLevel, ReadOptions, ScanOptions};
|
|
7
7
|
use slatedb::DbSnapshot;
|
|
8
|
+
use slatedb::IterationOrder;
|
|
8
9
|
|
|
9
10
|
use crate::errors::{closed_error, invalid_argument_error};
|
|
10
11
|
use crate::iterator::Iterator;
|
|
@@ -68,6 +69,10 @@ impl Snapshot {
|
|
|
68
69
|
opts.dirty = dirty;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
|
|
73
|
+
opts.cache_blocks = cb;
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
let guard = self.inner.borrow();
|
|
72
77
|
let snapshot = guard
|
|
73
78
|
.as_ref()
|
|
@@ -143,6 +148,18 @@ impl Snapshot {
|
|
|
143
148
|
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
144
149
|
opts.max_fetch_tasks = mft;
|
|
145
150
|
}
|
|
151
|
+
if let Some(order) = get_optional::<String>(&kwargs, "order")? {
|
|
152
|
+
opts.order = match order.as_str() {
|
|
153
|
+
"ascending" | "asc" => IterationOrder::Ascending,
|
|
154
|
+
"descending" | "desc" => IterationOrder::Descending,
|
|
155
|
+
other => {
|
|
156
|
+
return Err(invalid_argument_error(&format!(
|
|
157
|
+
"invalid order: {} (expected 'asc' or 'desc')",
|
|
158
|
+
other
|
|
159
|
+
)))
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
146
163
|
|
|
147
164
|
let guard = self.inner.borrow();
|
|
148
165
|
let snapshot = guard
|
|
@@ -162,6 +179,89 @@ impl Snapshot {
|
|
|
162
179
|
Ok(Iterator::new(iter))
|
|
163
180
|
}
|
|
164
181
|
|
|
182
|
+
/// Scan all keys with a given prefix from the snapshot.
|
|
183
|
+
pub fn scan_prefix(&self, prefix: String) -> Result<Iterator, Error> {
|
|
184
|
+
if prefix.is_empty() {
|
|
185
|
+
return Err(invalid_argument_error("prefix cannot be empty"));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let guard = self.inner.borrow();
|
|
189
|
+
let snapshot = guard
|
|
190
|
+
.as_ref()
|
|
191
|
+
.ok_or_else(|| closed_error("snapshot is closed"))?;
|
|
192
|
+
|
|
193
|
+
let iter = block_on_result(async { snapshot.scan_prefix(prefix.as_bytes()).await })?;
|
|
194
|
+
|
|
195
|
+
Ok(Iterator::new(iter))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Scan all keys with a given prefix with options from the snapshot.
|
|
199
|
+
pub fn scan_prefix_with_options(
|
|
200
|
+
&self,
|
|
201
|
+
prefix: String,
|
|
202
|
+
kwargs: RHash,
|
|
203
|
+
) -> Result<Iterator, Error> {
|
|
204
|
+
if prefix.is_empty() {
|
|
205
|
+
return Err(invalid_argument_error("prefix cannot be empty"));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let mut opts = ScanOptions::default();
|
|
209
|
+
|
|
210
|
+
if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
|
|
211
|
+
opts.durability_filter = match df.as_str() {
|
|
212
|
+
"remote" => DurabilityLevel::Remote,
|
|
213
|
+
"memory" => DurabilityLevel::Memory,
|
|
214
|
+
other => {
|
|
215
|
+
return Err(invalid_argument_error(&format!(
|
|
216
|
+
"invalid durability_filter: {} (expected 'remote' or 'memory')",
|
|
217
|
+
other
|
|
218
|
+
)))
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
|
|
224
|
+
opts.dirty = dirty;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
|
|
228
|
+
opts.read_ahead_bytes = rab;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
|
|
232
|
+
opts.cache_blocks = cb;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
236
|
+
opts.max_fetch_tasks = mft;
|
|
237
|
+
}
|
|
238
|
+
if let Some(order) = get_optional::<String>(&kwargs, "order")? {
|
|
239
|
+
opts.order = match order.as_str() {
|
|
240
|
+
"ascending" | "asc" => IterationOrder::Ascending,
|
|
241
|
+
"descending" | "desc" => IterationOrder::Descending,
|
|
242
|
+
other => {
|
|
243
|
+
return Err(invalid_argument_error(&format!(
|
|
244
|
+
"invalid order: {} (expected 'asc' or 'desc')",
|
|
245
|
+
other
|
|
246
|
+
)))
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let guard = self.inner.borrow();
|
|
252
|
+
let snapshot = guard
|
|
253
|
+
.as_ref()
|
|
254
|
+
.ok_or_else(|| closed_error("snapshot is closed"))?;
|
|
255
|
+
|
|
256
|
+
let iter = block_on_result(async {
|
|
257
|
+
snapshot
|
|
258
|
+
.scan_prefix_with_options(prefix.as_bytes(), &opts)
|
|
259
|
+
.await
|
|
260
|
+
})?;
|
|
261
|
+
|
|
262
|
+
Ok(Iterator::new(iter))
|
|
263
|
+
}
|
|
264
|
+
|
|
165
265
|
/// Close the snapshot and release resources.
|
|
166
266
|
pub fn close(&self) -> Result<(), Error> {
|
|
167
267
|
let _ = self.inner.borrow_mut().take();
|
|
@@ -186,6 +286,11 @@ pub fn define_snapshot_class(ruby: &Ruby, module: &magnus::RModule) -> Result<()
|
|
|
186
286
|
"_scan_with_options",
|
|
187
287
|
method!(Snapshot::scan_with_options, 3),
|
|
188
288
|
)?;
|
|
289
|
+
class.define_method("_scan_prefix", method!(Snapshot::scan_prefix, 1))?;
|
|
290
|
+
class.define_method(
|
|
291
|
+
"_scan_prefix_with_options",
|
|
292
|
+
method!(Snapshot::scan_prefix_with_options, 2),
|
|
293
|
+
)?;
|
|
189
294
|
class.define_method("close", method!(Snapshot::close, 0))?;
|
|
190
295
|
class.define_method("closed?", method!(Snapshot::is_closed, 0))?;
|
|
191
296
|
|