slatedb 0.2.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/README.md +75 -1
- data/ext/slatedb/Cargo.toml +8 -7
- data/ext/slatedb/src/admin.rs +17 -3
- data/ext/slatedb/src/database.rs +178 -38
- data/ext/slatedb/src/iterator.rs +4 -1
- data/ext/slatedb/src/lib.rs +2 -0
- data/ext/slatedb/src/merge_ops.rs +1 -3
- data/ext/slatedb/src/metrics.rs +47 -0
- data/ext/slatedb/src/reader.rs +51 -11
- data/ext/slatedb/src/snapshot.rs +29 -0
- data/ext/slatedb/src/transaction.rs +45 -7
- data/ext/slatedb/src/write_batch.rs +6 -1
- data/lib/slatedb/database.rb +104 -24
- data/lib/slatedb/metrics.rb +20 -0
- data/lib/slatedb/reader.rb +16 -1
- data/lib/slatedb/transaction.rb +29 -0
- data/lib/slatedb/version.rb +1 -1
- data/lib/slatedb.rb +1 -0
- metadata +15 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e7afb38b40ead3216b671173af52bf1a30215979f8e850dbb1e4f7dabf823f3
|
|
4
|
+
data.tar.gz: '03923c7ffdd47ba2ea9d846e98af2f66ce8fc1287e3ed3e99d21e5c27db05a8b'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d693ec11b74c8eea9722a270f48ccf8d209f21138a24dac54fbfa5d8242e23233982ea1f94aa31a667d4c0902c524213d20a9872fb6fb538685446d0efbb4fd1
|
|
7
|
+
data.tar.gz: 2e1b75894800650de82f8671bd07eefc678b6cfaaf6259245fbf854f8faaa0b8651f5c0ba1f07f4be5a8b0d85b2b54ca19177d2801a1f0f91400dde42202477a
|
data/README.md
CHANGED
|
@@ -138,6 +138,34 @@ db.put("key", "value", ttl: 60_000) # expires in 60 seconds
|
|
|
138
138
|
|
|
139
139
|
# Don't wait for durability
|
|
140
140
|
db.put("key", "value", await_durable: false)
|
|
141
|
+
|
|
142
|
+
# Supply an explicit sequence number (SlateDB >= 0.13.0)
|
|
143
|
+
db.put("key", "value", seqnum: 42)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### User-Supplied Sequence Numbers
|
|
147
|
+
|
|
148
|
+
By default SlateDB assigns a monotonically increasing sequence number to every
|
|
149
|
+
write. Since SlateDB 0.13.0 you can instead supply your own via `seqnum:`. The
|
|
150
|
+
value must be **strictly greater** than the current maximum sequence number, or
|
|
151
|
+
the write is rejected with `SlateDb::InvalidArgumentError`. This is useful when
|
|
152
|
+
replaying an external log or coordinating sequence numbers across systems.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
db.put("key", "value", seqnum: 1_000)
|
|
156
|
+
db.delete("old", seqnum: 1_001)
|
|
157
|
+
db.merge("counter", "5", seqnum: 1_002) # requires a merge operator
|
|
158
|
+
db.write(batch, seqnum: 1_003) # applied across the batch
|
|
159
|
+
db.batch(seqnum: 1_004) { |b| b.put("k", "v") }
|
|
160
|
+
|
|
161
|
+
# The sequence number is reflected in the stored record
|
|
162
|
+
db.put("key", "value", seqnum: 2_000)
|
|
163
|
+
db.get_key_value("key")[:seq] # => 2000
|
|
164
|
+
|
|
165
|
+
# On a transaction it is supplied at commit time
|
|
166
|
+
txn = db.begin_transaction
|
|
167
|
+
txn.put("k", "v")
|
|
168
|
+
txn.commit(seqnum: 3_000)
|
|
141
169
|
```
|
|
142
170
|
|
|
143
171
|
#### Get Options
|
|
@@ -151,11 +179,37 @@ db.get("key", durability_filter: "remote")
|
|
|
151
179
|
db.get("key", dirty: true)
|
|
152
180
|
```
|
|
153
181
|
|
|
182
|
+
#### Key-Value Metadata
|
|
183
|
+
|
|
184
|
+
SlateDB can return the full key-value record, including storage metadata:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
db.put("key", "value")
|
|
188
|
+
entry = db.get_key_value("key")
|
|
189
|
+
# => { key: "key", value: "value", seq: 1, create_ts: 1_765_000_000_000, expire_ts: nil }
|
|
190
|
+
|
|
191
|
+
entry[:value] # => "value"
|
|
192
|
+
entry[:seq] # SlateDB sequence number
|
|
193
|
+
entry[:create_ts] # creation timestamp in milliseconds
|
|
194
|
+
entry[:expire_ts] # expiration timestamp in milliseconds, or nil
|
|
195
|
+
|
|
196
|
+
# Alias for the same API
|
|
197
|
+
db.get_entry("key")
|
|
198
|
+
|
|
199
|
+
# The same read options accepted by #get are supported
|
|
200
|
+
db.get_key_value("key", durability_filter: "memory", cache_blocks: false)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Missing keys return `nil`, matching `#get`.
|
|
204
|
+
|
|
154
205
|
#### Delete Options
|
|
155
206
|
|
|
156
207
|
```ruby
|
|
157
208
|
# Don't wait for durability
|
|
158
209
|
db.delete("key", await_durable: false)
|
|
210
|
+
|
|
211
|
+
# Supply an explicit sequence number (SlateDB >= 0.13.0)
|
|
212
|
+
db.delete("key", seqnum: 42)
|
|
159
213
|
```
|
|
160
214
|
|
|
161
215
|
### Scanning
|
|
@@ -173,6 +227,11 @@ db.scan("a", "z").each do |key, value|
|
|
|
173
227
|
puts "#{key}: #{value}"
|
|
174
228
|
end
|
|
175
229
|
|
|
230
|
+
# Scan in descending key order
|
|
231
|
+
db.scan("a", "z", order: :desc).each do |key, value|
|
|
232
|
+
puts "#{key}: #{value}"
|
|
233
|
+
end
|
|
234
|
+
|
|
176
235
|
# Use Enumerable methods
|
|
177
236
|
keys = db.scan("user:").map { |k, v| k }
|
|
178
237
|
users = db.scan("user:").select { |k, v| v.include?("active") }
|
|
@@ -196,6 +255,11 @@ db.scan_prefix("order:") do |key, value|
|
|
|
196
255
|
puts "#{key}: #{value}"
|
|
197
256
|
end
|
|
198
257
|
|
|
258
|
+
# Prefix scans can also run in descending key order
|
|
259
|
+
db.scan_prefix("user:", order: :desc).each do |key, value|
|
|
260
|
+
puts "#{key}: #{value}"
|
|
261
|
+
end
|
|
262
|
+
|
|
199
263
|
# Works with transactions, snapshots, and readers too
|
|
200
264
|
db.transaction do |txn|
|
|
201
265
|
txn.scan_prefix("item:").each do |k, v|
|
|
@@ -429,6 +493,16 @@ SlateDb::Reader.open("/tmp/mydb",
|
|
|
429
493
|
checkpoint_id: "uuid-here") do |reader|
|
|
430
494
|
reader.get("key")
|
|
431
495
|
end
|
|
496
|
+
|
|
497
|
+
# Enable the reader's on-disk cache and cap its open file handles
|
|
498
|
+
# (max_open_file_handles, added in SlateDB 0.13.0, only takes effect when
|
|
499
|
+
# cache_root is set, since that is what enables the cached object store).
|
|
500
|
+
SlateDb::Reader.open("/tmp/mydb",
|
|
501
|
+
url: "s3://bucket/path",
|
|
502
|
+
cache_root: "/var/cache/slatedb",
|
|
503
|
+
max_open_file_handles: 256) do |reader|
|
|
504
|
+
reader.get("key")
|
|
505
|
+
end
|
|
432
506
|
```
|
|
433
507
|
|
|
434
508
|
### Admin Operations
|
|
@@ -527,7 +601,7 @@ Exception hierarchy:
|
|
|
527
601
|
|
|
528
602
|
## Requirements
|
|
529
603
|
|
|
530
|
-
- Ruby 3.
|
|
604
|
+
- Ruby 3.3+
|
|
531
605
|
- Rust toolchain (for building from source)
|
|
532
606
|
|
|
533
607
|
## Development
|
data/ext/slatedb/Cargo.toml
CHANGED
|
@@ -11,12 +11,13 @@ name = "slatedb"
|
|
|
11
11
|
crate-type = ["cdylib"]
|
|
12
12
|
|
|
13
13
|
[dependencies]
|
|
14
|
-
slatedb = "0.
|
|
14
|
+
slatedb = "0.13.1"
|
|
15
15
|
magnus = { version = "0.8.2", features = ["rb-sys"] }
|
|
16
|
-
rb-sys = { version = "0.9.
|
|
17
|
-
tokio = { version = "1.
|
|
18
|
-
bytes = "1.11.
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
rb-sys = { version = "0.9.128", features = ["stable-api-compiled-fallback"] }
|
|
17
|
+
tokio = { version = "1.52.3", features = ["rt-multi-thread", "sync"] }
|
|
18
|
+
bytes = "1.11.1"
|
|
19
|
+
serde_json = "1.0.145"
|
|
20
|
+
url = "2.5.8"
|
|
21
|
+
once_cell = "1.21.4"
|
|
21
22
|
log = "0.4.29"
|
|
22
|
-
uuid = "1.
|
|
23
|
+
uuid = "1.23.1"
|
data/ext/slatedb/src/admin.rs
CHANGED
|
@@ -43,10 +43,18 @@ impl Admin {
|
|
|
43
43
|
/// # Returns
|
|
44
44
|
/// JSON string of the manifest, or None if no manifests exist.
|
|
45
45
|
pub fn read_manifest(&self, id: Option<u64>) -> Result<Option<String>, Error> {
|
|
46
|
-
block_on(async { self.inner.read_manifest(id).await }).map_err(|e| {
|
|
46
|
+
let manifest = block_on(async { self.inner.read_manifest(id).await }).map_err(|e| {
|
|
47
47
|
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
48
48
|
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
49
|
-
})
|
|
49
|
+
})?;
|
|
50
|
+
|
|
51
|
+
match manifest {
|
|
52
|
+
Some(manifest) => Ok(Some(serde_json::to_string(&manifest).map_err(|e| {
|
|
53
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
54
|
+
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
55
|
+
})?)),
|
|
56
|
+
None => Ok(None),
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
/// List manifests within an optional [start, end) range as JSON.
|
|
@@ -65,7 +73,12 @@ impl Admin {
|
|
|
65
73
|
(None, None) => 0..u64::MAX,
|
|
66
74
|
};
|
|
67
75
|
|
|
68
|
-
block_on(async { self.inner.list_manifests(range).await }).map_err(|e| {
|
|
76
|
+
let manifests = block_on(async { self.inner.list_manifests(range).await }).map_err(|e| {
|
|
77
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
78
|
+
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
79
|
+
})?;
|
|
80
|
+
|
|
81
|
+
serde_json::to_string(&manifests).map_err(|e| {
|
|
69
82
|
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
70
83
|
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
71
84
|
})
|
|
@@ -236,6 +249,7 @@ impl Admin {
|
|
|
236
249
|
default_opts.compacted_options,
|
|
237
250
|
),
|
|
238
251
|
compactions_options: default_opts.compactions_options,
|
|
252
|
+
detach_options: default_opts.detach_options,
|
|
239
253
|
}
|
|
240
254
|
};
|
|
241
255
|
|
data/ext/slatedb/src/database.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
use std::
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::sync::{Arc, Mutex};
|
|
2
3
|
|
|
3
4
|
use magnus::prelude::*;
|
|
4
5
|
use magnus::{function, method, Error, RHash, Ruby};
|
|
@@ -6,11 +7,12 @@ use slatedb::config::{
|
|
|
6
7
|
DurabilityLevel, MergeOptions, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions,
|
|
7
8
|
};
|
|
8
9
|
use slatedb::object_store::memory::InMemory;
|
|
9
|
-
use slatedb::{Db, IsolationLevel};
|
|
10
|
+
use slatedb::{Db, IsolationLevel, IterationOrder, KeyValue};
|
|
10
11
|
|
|
11
12
|
use crate::errors::invalid_argument_error;
|
|
12
13
|
use crate::iterator::Iterator;
|
|
13
14
|
use crate::merge_ops::{parse_merge_operator, parse_merge_operator_proc};
|
|
15
|
+
use crate::metrics::Metrics;
|
|
14
16
|
use crate::runtime::block_on_result;
|
|
15
17
|
use crate::snapshot::Snapshot;
|
|
16
18
|
use crate::transaction::Transaction;
|
|
@@ -23,9 +25,69 @@ use crate::write_batch::WriteBatch;
|
|
|
23
25
|
#[magnus::wrap(class = "SlateDb::Database", free_immediately, size)]
|
|
24
26
|
pub struct Database {
|
|
25
27
|
inner: Arc<Db>,
|
|
28
|
+
metrics: Arc<Mutex<HashMap<String, i64>>>,
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
impl Database {
|
|
32
|
+
fn increment_metric(&self, name: &str) {
|
|
33
|
+
let mut metrics = self.metrics.lock().expect("metrics mutex poisoned");
|
|
34
|
+
*metrics.entry(name.to_string()).or_insert(0) += 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn key_value_to_hash(kv: KeyValue) -> Result<RHash, Error> {
|
|
38
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
39
|
+
let hash = ruby.hash_new();
|
|
40
|
+
hash.aset(
|
|
41
|
+
ruby.to_symbol("key"),
|
|
42
|
+
String::from_utf8_lossy(&kv.key).to_string(),
|
|
43
|
+
)?;
|
|
44
|
+
hash.aset(
|
|
45
|
+
ruby.to_symbol("value"),
|
|
46
|
+
String::from_utf8_lossy(&kv.value).to_string(),
|
|
47
|
+
)?;
|
|
48
|
+
hash.aset(ruby.to_symbol("seq"), kv.seq)?;
|
|
49
|
+
hash.aset(ruby.to_symbol("create_ts"), kv.create_ts)?;
|
|
50
|
+
hash.aset(ruby.to_symbol("expire_ts"), kv.expire_ts)?;
|
|
51
|
+
Ok(hash)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn read_options_from_kwargs(kwargs: &RHash) -> Result<ReadOptions, Error> {
|
|
55
|
+
let mut opts = ReadOptions::default();
|
|
56
|
+
|
|
57
|
+
if let Some(df) = get_optional::<String>(kwargs, "durability_filter")? {
|
|
58
|
+
opts.durability_filter = match df.as_str() {
|
|
59
|
+
"remote" => DurabilityLevel::Remote,
|
|
60
|
+
"memory" => DurabilityLevel::Memory,
|
|
61
|
+
other => {
|
|
62
|
+
return Err(invalid_argument_error(&format!(
|
|
63
|
+
"invalid durability_filter: {} (expected 'remote' or 'memory')",
|
|
64
|
+
other
|
|
65
|
+
)))
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if let Some(dirty) = get_optional::<bool>(kwargs, "dirty")? {
|
|
71
|
+
opts.dirty = dirty;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if let Some(cb) = get_optional::<bool>(kwargs, "cache_blocks")? {
|
|
75
|
+
opts.cache_blocks = cb;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Ok(opts)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn write_options_from_kwargs(kwargs: &RHash) -> Result<WriteOptions, Error> {
|
|
82
|
+
let await_durable = get_optional::<bool>(kwargs, "await_durable")?.unwrap_or(true);
|
|
83
|
+
let seqnum = get_optional::<u64>(kwargs, "seqnum")?.unwrap_or(0);
|
|
84
|
+
|
|
85
|
+
Ok(WriteOptions {
|
|
86
|
+
await_durable,
|
|
87
|
+
seqnum,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
29
91
|
/// Open a database at the given path.
|
|
30
92
|
///
|
|
31
93
|
/// # Arguments
|
|
@@ -37,8 +99,7 @@ impl Database {
|
|
|
37
99
|
/// A new Database instance
|
|
38
100
|
pub fn open(path: String, url: Option<String>, kwargs: RHash) -> Result<Self, Error> {
|
|
39
101
|
// Try string-based merge operator first, then proc-based
|
|
40
|
-
let merge_operator = parse_merge_operator(&kwargs)?
|
|
41
|
-
.or(parse_merge_operator_proc(&kwargs)?);
|
|
102
|
+
let merge_operator = parse_merge_operator(&kwargs)?.or(parse_merge_operator_proc(&kwargs)?);
|
|
42
103
|
|
|
43
104
|
let db = block_on_result(async {
|
|
44
105
|
let object_store: Arc<dyn slatedb::object_store::ObjectStore> =
|
|
@@ -58,6 +119,7 @@ impl Database {
|
|
|
58
119
|
|
|
59
120
|
Ok(Self {
|
|
60
121
|
inner: Arc::new(db),
|
|
122
|
+
metrics: Arc::new(Mutex::new(HashMap::new())),
|
|
61
123
|
})
|
|
62
124
|
}
|
|
63
125
|
|
|
@@ -77,6 +139,7 @@ impl Database {
|
|
|
77
139
|
|
|
78
140
|
let result =
|
|
79
141
|
block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
|
|
142
|
+
self.increment_metric("db.get.count");
|
|
80
143
|
|
|
81
144
|
Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
|
|
82
145
|
}
|
|
@@ -94,31 +157,64 @@ impl Database {
|
|
|
94
157
|
return Err(invalid_argument_error("key cannot be empty"));
|
|
95
158
|
}
|
|
96
159
|
|
|
97
|
-
let
|
|
160
|
+
let opts = Self::read_options_from_kwargs(&kwargs)?;
|
|
98
161
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
162
|
+
let result =
|
|
163
|
+
block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
|
|
164
|
+
self.increment_metric("db.get_with_options.count");
|
|
165
|
+
|
|
166
|
+
Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Get a key-value pair with metadata by key.
|
|
170
|
+
///
|
|
171
|
+
/// # Arguments
|
|
172
|
+
/// * `key` - The key to look up
|
|
173
|
+
///
|
|
174
|
+
/// # Returns
|
|
175
|
+
/// A Hash with key, value, seq, create_ts, and expire_ts, or nil if not found
|
|
176
|
+
pub fn get_key_value(&self, key: String) -> Result<Option<RHash>, Error> {
|
|
177
|
+
if key.is_empty() {
|
|
178
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
111
179
|
}
|
|
112
180
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
181
|
+
let opts = ReadOptions::default();
|
|
182
|
+
let result = block_on_result(async {
|
|
183
|
+
self.inner
|
|
184
|
+
.get_key_value_with_options(key.as_bytes(), &opts)
|
|
185
|
+
.await
|
|
186
|
+
})?;
|
|
187
|
+
self.increment_metric("db.get_key_value.count");
|
|
188
|
+
|
|
189
|
+
result.map(Self::key_value_to_hash).transpose()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Get a key-value pair with metadata by key with options.
|
|
193
|
+
///
|
|
194
|
+
/// # Arguments
|
|
195
|
+
/// * `key` - The key to look up
|
|
196
|
+
/// * `kwargs` - Keyword arguments (durability_filter, dirty, cache_blocks)
|
|
197
|
+
///
|
|
198
|
+
/// # Returns
|
|
199
|
+
/// A Hash with key, value, seq, create_ts, and expire_ts, or nil if not found
|
|
200
|
+
pub fn get_key_value_with_options(
|
|
201
|
+
&self,
|
|
202
|
+
key: String,
|
|
203
|
+
kwargs: RHash,
|
|
204
|
+
) -> Result<Option<RHash>, Error> {
|
|
205
|
+
if key.is_empty() {
|
|
206
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
116
207
|
}
|
|
117
208
|
|
|
118
|
-
let
|
|
119
|
-
|
|
209
|
+
let opts = Self::read_options_from_kwargs(&kwargs)?;
|
|
210
|
+
let result = block_on_result(async {
|
|
211
|
+
self.inner
|
|
212
|
+
.get_key_value_with_options(key.as_bytes(), &opts)
|
|
213
|
+
.await
|
|
214
|
+
})?;
|
|
215
|
+
self.increment_metric("db.get_key_value_with_options.count");
|
|
120
216
|
|
|
121
|
-
|
|
217
|
+
result.map(Self::key_value_to_hash).transpose()
|
|
122
218
|
}
|
|
123
219
|
|
|
124
220
|
/// Get a value by key as raw bytes.
|
|
@@ -137,6 +233,7 @@ impl Database {
|
|
|
137
233
|
|
|
138
234
|
let result =
|
|
139
235
|
block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
|
|
236
|
+
self.increment_metric("db.get_bytes.count");
|
|
140
237
|
|
|
141
238
|
Ok(result.map(|b| b.to_vec()))
|
|
142
239
|
}
|
|
@@ -155,6 +252,7 @@ impl Database {
|
|
|
155
252
|
|
|
156
253
|
let write_opts = WriteOptions {
|
|
157
254
|
await_durable: true,
|
|
255
|
+
seqnum: 0,
|
|
158
256
|
};
|
|
159
257
|
|
|
160
258
|
block_on_result(async {
|
|
@@ -162,6 +260,7 @@ impl Database {
|
|
|
162
260
|
.put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
|
|
163
261
|
.await
|
|
164
262
|
})?;
|
|
263
|
+
self.increment_metric("db.put.count");
|
|
165
264
|
|
|
166
265
|
Ok(())
|
|
167
266
|
}
|
|
@@ -171,7 +270,7 @@ impl Database {
|
|
|
171
270
|
/// # Arguments
|
|
172
271
|
/// * `key` - The key to store
|
|
173
272
|
/// * `value` - The value to store
|
|
174
|
-
/// * `kwargs` - Keyword arguments (ttl, await_durable)
|
|
273
|
+
/// * `kwargs` - Keyword arguments (ttl, await_durable, seqnum)
|
|
175
274
|
pub fn put_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
|
|
176
275
|
if key.is_empty() {
|
|
177
276
|
return Err(invalid_argument_error("key cannot be empty"));
|
|
@@ -187,14 +286,14 @@ impl Database {
|
|
|
187
286
|
};
|
|
188
287
|
|
|
189
288
|
// Parse await_durable
|
|
190
|
-
let
|
|
191
|
-
let write_opts = WriteOptions { await_durable };
|
|
289
|
+
let write_opts = Self::write_options_from_kwargs(&kwargs)?;
|
|
192
290
|
|
|
193
291
|
block_on_result(async {
|
|
194
292
|
self.inner
|
|
195
293
|
.put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
|
|
196
294
|
.await
|
|
197
295
|
})?;
|
|
296
|
+
self.increment_metric("db.put_with_options.count");
|
|
198
297
|
|
|
199
298
|
Ok(())
|
|
200
299
|
}
|
|
@@ -210,6 +309,7 @@ impl Database {
|
|
|
210
309
|
|
|
211
310
|
let write_opts = WriteOptions {
|
|
212
311
|
await_durable: true,
|
|
312
|
+
seqnum: 0,
|
|
213
313
|
};
|
|
214
314
|
|
|
215
315
|
block_on_result(async {
|
|
@@ -217,6 +317,7 @@ impl Database {
|
|
|
217
317
|
.delete_with_options(key.as_bytes(), &write_opts)
|
|
218
318
|
.await
|
|
219
319
|
})?;
|
|
320
|
+
self.increment_metric("db.delete.count");
|
|
220
321
|
|
|
221
322
|
Ok(())
|
|
222
323
|
}
|
|
@@ -225,20 +326,20 @@ impl Database {
|
|
|
225
326
|
///
|
|
226
327
|
/// # Arguments
|
|
227
328
|
/// * `key` - The key to delete
|
|
228
|
-
/// * `kwargs` - Keyword arguments (await_durable)
|
|
329
|
+
/// * `kwargs` - Keyword arguments (await_durable, seqnum)
|
|
229
330
|
pub fn delete_with_options(&self, key: String, kwargs: RHash) -> Result<(), Error> {
|
|
230
331
|
if key.is_empty() {
|
|
231
332
|
return Err(invalid_argument_error("key cannot be empty"));
|
|
232
333
|
}
|
|
233
334
|
|
|
234
|
-
let
|
|
235
|
-
let write_opts = WriteOptions { await_durable };
|
|
335
|
+
let write_opts = Self::write_options_from_kwargs(&kwargs)?;
|
|
236
336
|
|
|
237
337
|
block_on_result(async {
|
|
238
338
|
self.inner
|
|
239
339
|
.delete_with_options(key.as_bytes(), &write_opts)
|
|
240
340
|
.await
|
|
241
341
|
})?;
|
|
342
|
+
self.increment_metric("db.delete_with_options.count");
|
|
242
343
|
|
|
243
344
|
Ok(())
|
|
244
345
|
}
|
|
@@ -325,6 +426,18 @@ impl Database {
|
|
|
325
426
|
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
326
427
|
opts.max_fetch_tasks = mft;
|
|
327
428
|
}
|
|
429
|
+
if let Some(order) = get_optional::<String>(&kwargs, "order")? {
|
|
430
|
+
opts.order = match order.as_str() {
|
|
431
|
+
"ascending" | "asc" => IterationOrder::Ascending,
|
|
432
|
+
"descending" | "desc" => IterationOrder::Descending,
|
|
433
|
+
other => {
|
|
434
|
+
return Err(invalid_argument_error(&format!(
|
|
435
|
+
"invalid order: {} (expected 'asc' or 'desc')",
|
|
436
|
+
other
|
|
437
|
+
)))
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
328
441
|
|
|
329
442
|
let start_bytes = start.into_bytes();
|
|
330
443
|
let end_bytes = end_key.map(|e| e.into_bytes());
|
|
@@ -408,6 +521,18 @@ impl Database {
|
|
|
408
521
|
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
409
522
|
opts.max_fetch_tasks = mft;
|
|
410
523
|
}
|
|
524
|
+
if let Some(order) = get_optional::<String>(&kwargs, "order")? {
|
|
525
|
+
opts.order = match order.as_str() {
|
|
526
|
+
"ascending" | "asc" => IterationOrder::Ascending,
|
|
527
|
+
"descending" | "desc" => IterationOrder::Descending,
|
|
528
|
+
other => {
|
|
529
|
+
return Err(invalid_argument_error(&format!(
|
|
530
|
+
"invalid order: {} (expected 'asc' or 'desc')",
|
|
531
|
+
other
|
|
532
|
+
)))
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
411
536
|
|
|
412
537
|
let iter = block_on_result(async {
|
|
413
538
|
self.inner
|
|
@@ -432,10 +557,9 @@ impl Database {
|
|
|
432
557
|
///
|
|
433
558
|
/// # Arguments
|
|
434
559
|
/// * `batch` - The WriteBatch to write
|
|
435
|
-
/// * `kwargs` - Keyword arguments (await_durable)
|
|
560
|
+
/// * `kwargs` - Keyword arguments (await_durable, seqnum)
|
|
436
561
|
pub fn write_with_options(&self, batch: &WriteBatch, kwargs: RHash) -> Result<(), Error> {
|
|
437
|
-
let
|
|
438
|
-
let write_opts = WriteOptions { await_durable };
|
|
562
|
+
let write_opts = Self::write_options_from_kwargs(&kwargs)?;
|
|
439
563
|
|
|
440
564
|
let batch_inner = batch.take()?;
|
|
441
565
|
|
|
@@ -462,6 +586,7 @@ impl Database {
|
|
|
462
586
|
|
|
463
587
|
let write_opts = WriteOptions {
|
|
464
588
|
await_durable: true,
|
|
589
|
+
seqnum: 0,
|
|
465
590
|
};
|
|
466
591
|
|
|
467
592
|
block_on_result(async {
|
|
@@ -478,8 +603,13 @@ impl Database {
|
|
|
478
603
|
/// # Arguments
|
|
479
604
|
/// * `key` - The key to merge into
|
|
480
605
|
/// * `value` - The merge operand to apply
|
|
481
|
-
/// * `kwargs` - Keyword arguments (ttl, await_durable)
|
|
482
|
-
pub fn merge_with_options(
|
|
606
|
+
/// * `kwargs` - Keyword arguments (ttl, await_durable, seqnum)
|
|
607
|
+
pub fn merge_with_options(
|
|
608
|
+
&self,
|
|
609
|
+
key: String,
|
|
610
|
+
value: String,
|
|
611
|
+
kwargs: RHash,
|
|
612
|
+
) -> Result<(), Error> {
|
|
483
613
|
if key.is_empty() {
|
|
484
614
|
return Err(invalid_argument_error("key cannot be empty"));
|
|
485
615
|
}
|
|
@@ -492,8 +622,7 @@ impl Database {
|
|
|
492
622
|
},
|
|
493
623
|
};
|
|
494
624
|
|
|
495
|
-
let
|
|
496
|
-
let write_opts = WriteOptions { await_durable };
|
|
625
|
+
let write_opts = Self::write_options_from_kwargs(&kwargs)?;
|
|
497
626
|
|
|
498
627
|
block_on_result(async {
|
|
499
628
|
self.inner
|
|
@@ -548,8 +677,8 @@ impl Database {
|
|
|
548
677
|
pub fn create_checkpoint(&self, kwargs: RHash) -> Result<RHash, Error> {
|
|
549
678
|
use slatedb::config::{CheckpointOptions, CheckpointScope};
|
|
550
679
|
|
|
551
|
-
let lifetime =
|
|
552
|
-
|
|
680
|
+
let lifetime =
|
|
681
|
+
get_optional::<u64>(&kwargs, "lifetime")?.map(std::time::Duration::from_millis);
|
|
553
682
|
let name = get_optional::<String>(&kwargs, "name")?;
|
|
554
683
|
|
|
555
684
|
let options = CheckpointOptions {
|
|
@@ -578,6 +707,11 @@ impl Database {
|
|
|
578
707
|
Ok(())
|
|
579
708
|
}
|
|
580
709
|
|
|
710
|
+
/// Return the database metrics registry.
|
|
711
|
+
pub fn metrics(&self) -> Result<Metrics, Error> {
|
|
712
|
+
Ok(Metrics::new(self.metrics.clone()))
|
|
713
|
+
}
|
|
714
|
+
|
|
581
715
|
/// Close the database.
|
|
582
716
|
pub fn close(&self) -> Result<(), Error> {
|
|
583
717
|
block_on_result(async { self.inner.close().await })?;
|
|
@@ -595,6 +729,11 @@ pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<()
|
|
|
595
729
|
// Instance methods - simple versions
|
|
596
730
|
class.define_method("_get", method!(Database::get, 1))?;
|
|
597
731
|
class.define_method("_get_with_options", method!(Database::get_with_options, 2))?;
|
|
732
|
+
class.define_method("_get_key_value", method!(Database::get_key_value, 1))?;
|
|
733
|
+
class.define_method(
|
|
734
|
+
"_get_key_value_with_options",
|
|
735
|
+
method!(Database::get_key_value_with_options, 2),
|
|
736
|
+
)?;
|
|
598
737
|
class.define_method("get_bytes", method!(Database::get_bytes, 1))?;
|
|
599
738
|
class.define_method("_put", method!(Database::put, 2))?;
|
|
600
739
|
class.define_method("_put_with_options", method!(Database::put_with_options, 3))?;
|
|
@@ -633,6 +772,7 @@ pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<()
|
|
|
633
772
|
method!(Database::create_checkpoint, 1),
|
|
634
773
|
)?;
|
|
635
774
|
class.define_method("flush", method!(Database::flush, 0))?;
|
|
775
|
+
class.define_method("_metrics", method!(Database::metrics, 0))?;
|
|
636
776
|
class.define_method("close", method!(Database::close, 0))?;
|
|
637
777
|
|
|
638
778
|
Ok(())
|
data/ext/slatedb/src/iterator.rs
CHANGED
|
@@ -98,7 +98,10 @@ impl Iterator {
|
|
|
98
98
|
let result = block_on(async {
|
|
99
99
|
let mut guard = inner.lock().await;
|
|
100
100
|
match guard.as_mut() {
|
|
101
|
-
Some(iter) => iter
|
|
101
|
+
Some(iter) => iter
|
|
102
|
+
.seek(key.as_bytes())
|
|
103
|
+
.await
|
|
104
|
+
.map_err(IteratorError::Slate),
|
|
102
105
|
None => Err(IteratorError::Closed),
|
|
103
106
|
}
|
|
104
107
|
});
|
data/ext/slatedb/src/lib.rs
CHANGED
|
@@ -21,6 +21,7 @@ mod database;
|
|
|
21
21
|
mod errors;
|
|
22
22
|
mod iterator;
|
|
23
23
|
mod merge_ops;
|
|
24
|
+
mod metrics;
|
|
24
25
|
mod reader;
|
|
25
26
|
mod runtime;
|
|
26
27
|
mod snapshot;
|
|
@@ -46,6 +47,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
46
47
|
snapshot::define_snapshot_class(ruby, &module)?;
|
|
47
48
|
reader::define_reader_class(ruby, &module)?;
|
|
48
49
|
admin::define_admin_class(ruby, &module)?;
|
|
50
|
+
metrics::define_metrics_class(ruby, &module)?;
|
|
49
51
|
|
|
50
52
|
Ok(())
|
|
51
53
|
}
|
|
@@ -149,9 +149,7 @@ impl RubyProcMergeOperator {
|
|
|
149
149
|
"Ruby merge operator called from non-Ruby thread, using fallback concatenation. \
|
|
150
150
|
This can happen during background compaction."
|
|
151
151
|
);
|
|
152
|
-
let mut result = existing_value
|
|
153
|
-
.map(|v| v.to_vec())
|
|
154
|
-
.unwrap_or_default();
|
|
152
|
+
let mut result = existing_value.map(|v| v.to_vec()).unwrap_or_default();
|
|
155
153
|
result.extend_from_slice(new_value);
|
|
156
154
|
Ok(Bytes::from(result))
|
|
157
155
|
}
|