slatedb 0.2.0 → 0.4.4
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 +140 -8
- data/ext/slatedb/Cargo.toml +8 -7
- data/ext/slatedb/src/admin.rs +22 -3
- data/ext/slatedb/src/database.rs +182 -41
- 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 +85 -25
- data/ext/slatedb/src/snapshot.rs +34 -3
- data/ext/slatedb/src/transaction.rs +50 -9
- data/ext/slatedb/src/utils.rs +46 -4
- data/ext/slatedb/src/write_batch.rs +6 -1
- data/lib/slatedb/database.rb +116 -24
- data/lib/slatedb/metrics.rb +20 -0
- data/lib/slatedb/reader.rb +45 -10
- data/lib/slatedb/snapshot.rb +15 -7
- data/lib/slatedb/transaction.rb +44 -7
- 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: 5ea58f5cc108fef4066004dce3db7641d979589c6afa56023845b4e568198630
|
|
4
|
+
data.tar.gz: c5e52bded1c840e6d51ba420b673c5f03e3f9450dd78476dc129a45cab02b11b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 715e35df65d25a1487f648718ec72e593fed5e22289f47c2017aa8eac24ccda5d5df00151ec618b1d732550be2f8b6ac5d8136b9358d18c197ae11b5e437e8a9
|
|
7
|
+
data.tar.gz: 92780ae93284906f92a4e4550d885c281af32f7cd1f91642e6ca9ebbe1f114d574d20a17be3f0605b5caaaafe64e27e70f12a96b5fcc6255fd8f0e210f0fa9b1
|
data/README.md
CHANGED
|
@@ -8,10 +8,6 @@ Ruby bindings for [SlateDB](https://slatedb.io), a cloud-native embedded key-val
|
|
|
8
8
|
|
|
9
9
|
These bindings are still in early development, and while SlateDB itself is used in Production, these bindings have yet to be. Contributions are welcome!
|
|
10
10
|
|
|
11
|
-
### TODO
|
|
12
|
-
|
|
13
|
-
- [ ] Cross-compile native extensions
|
|
14
|
-
|
|
15
11
|
## Installation
|
|
16
12
|
|
|
17
13
|
Add this line to your application's Gemfile:
|
|
@@ -32,8 +28,7 @@ Or install it yourself as:
|
|
|
32
28
|
gem install slatedb
|
|
33
29
|
```
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
> This gem currently requires a working Rust toolchain to install until the dependencies are cross-compiled.
|
|
31
|
+
Precompiled native gems are published for common Linux and Darwin platforms. Building from source still requires a Rust toolchain.
|
|
37
32
|
|
|
38
33
|
## Usage
|
|
39
34
|
|
|
@@ -138,6 +133,34 @@ db.put("key", "value", ttl: 60_000) # expires in 60 seconds
|
|
|
138
133
|
|
|
139
134
|
# Don't wait for durability
|
|
140
135
|
db.put("key", "value", await_durable: false)
|
|
136
|
+
|
|
137
|
+
# Supply an explicit sequence number (SlateDB >= 0.13.0)
|
|
138
|
+
db.put("key", "value", seqnum: 42)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### User-Supplied Sequence Numbers
|
|
142
|
+
|
|
143
|
+
By default SlateDB assigns a monotonically increasing sequence number to every
|
|
144
|
+
write. Since SlateDB 0.13.0 you can instead supply your own via `seqnum:`. The
|
|
145
|
+
value must be **strictly greater** than the current maximum sequence number, or
|
|
146
|
+
the write is rejected with `SlateDb::InvalidArgumentError`. This is useful when
|
|
147
|
+
replaying an external log or coordinating sequence numbers across systems.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
db.put("key", "value", seqnum: 1_000)
|
|
151
|
+
db.delete("old", seqnum: 1_001)
|
|
152
|
+
db.merge("counter", "5", seqnum: 1_002) # requires a merge operator
|
|
153
|
+
db.write(batch, seqnum: 1_003) # applied across the batch
|
|
154
|
+
db.batch(seqnum: 1_004) { |b| b.put("k", "v") }
|
|
155
|
+
|
|
156
|
+
# The sequence number is reflected in the stored record
|
|
157
|
+
db.put("key", "value", seqnum: 2_000)
|
|
158
|
+
db.get_key_value("key")[:seq] # => 2000
|
|
159
|
+
|
|
160
|
+
# On a transaction it is supplied at commit time
|
|
161
|
+
txn = db.begin_transaction
|
|
162
|
+
txn.put("k", "v")
|
|
163
|
+
txn.commit(seqnum: 3_000)
|
|
141
164
|
```
|
|
142
165
|
|
|
143
166
|
#### Get Options
|
|
@@ -151,11 +174,37 @@ db.get("key", durability_filter: "remote")
|
|
|
151
174
|
db.get("key", dirty: true)
|
|
152
175
|
```
|
|
153
176
|
|
|
177
|
+
#### Key-Value Metadata
|
|
178
|
+
|
|
179
|
+
SlateDB can return the full key-value record, including storage metadata:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
db.put("key", "value")
|
|
183
|
+
entry = db.get_key_value("key")
|
|
184
|
+
# => { key: "key", value: "value", seq: 1, create_ts: 1_765_000_000_000, expire_ts: nil }
|
|
185
|
+
|
|
186
|
+
entry[:value] # => "value"
|
|
187
|
+
entry[:seq] # SlateDB sequence number
|
|
188
|
+
entry[:create_ts] # creation timestamp in milliseconds
|
|
189
|
+
entry[:expire_ts] # expiration timestamp in milliseconds, or nil
|
|
190
|
+
|
|
191
|
+
# Alias for the same API
|
|
192
|
+
db.get_entry("key")
|
|
193
|
+
|
|
194
|
+
# The same read options accepted by #get are supported
|
|
195
|
+
db.get_key_value("key", durability_filter: "memory", cache_blocks: false)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Missing keys return `nil`, matching `#get`.
|
|
199
|
+
|
|
154
200
|
#### Delete Options
|
|
155
201
|
|
|
156
202
|
```ruby
|
|
157
203
|
# Don't wait for durability
|
|
158
204
|
db.delete("key", await_durable: false)
|
|
205
|
+
|
|
206
|
+
# Supply an explicit sequence number (SlateDB >= 0.13.0)
|
|
207
|
+
db.delete("key", seqnum: 42)
|
|
159
208
|
```
|
|
160
209
|
|
|
161
210
|
### Scanning
|
|
@@ -173,6 +222,11 @@ db.scan("a", "z").each do |key, value|
|
|
|
173
222
|
puts "#{key}: #{value}"
|
|
174
223
|
end
|
|
175
224
|
|
|
225
|
+
# Scan in descending key order
|
|
226
|
+
db.scan("a", "z", order: :desc).each do |key, value|
|
|
227
|
+
puts "#{key}: #{value}"
|
|
228
|
+
end
|
|
229
|
+
|
|
176
230
|
# Use Enumerable methods
|
|
177
231
|
keys = db.scan("user:").map { |k, v| k }
|
|
178
232
|
users = db.scan("user:").select { |k, v| v.include?("active") }
|
|
@@ -196,6 +250,11 @@ db.scan_prefix("order:") do |key, value|
|
|
|
196
250
|
puts "#{key}: #{value}"
|
|
197
251
|
end
|
|
198
252
|
|
|
253
|
+
# Prefix scans can also run in descending key order
|
|
254
|
+
db.scan_prefix("user:", order: :desc).each do |key, value|
|
|
255
|
+
puts "#{key}: #{value}"
|
|
256
|
+
end
|
|
257
|
+
|
|
199
258
|
# Works with transactions, snapshots, and readers too
|
|
200
259
|
db.transaction do |txn|
|
|
201
260
|
txn.scan_prefix("item:").each do |k, v|
|
|
@@ -204,6 +263,27 @@ db.transaction do |txn|
|
|
|
204
263
|
end
|
|
205
264
|
```
|
|
206
265
|
|
|
266
|
+
You can narrow a prefix scan to a sub-range using the `from:` (inclusive) and
|
|
267
|
+
`to:` (exclusive) options (SlateDB >= 0.14.0). Both are key *suffixes* that are
|
|
268
|
+
appended to the prefix, so they let you paginate or resume within a prefix
|
|
269
|
+
without building full-key ranges by hand. The scan never escapes the prefix.
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
# Keys "user:100" (inclusive) up to "user:200" (exclusive)
|
|
273
|
+
db.scan_prefix("user:", from: "100", to: "200").each do |key, value|
|
|
274
|
+
puts "#{key}: #{value}"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Only a lower bound: from "user:500" to the end of the prefix
|
|
278
|
+
db.scan_prefix("user:", from: "500")
|
|
279
|
+
|
|
280
|
+
# Only an upper bound: from the start of the prefix up to (but not including) "user:100"
|
|
281
|
+
db.scan_prefix("user:", to: "100")
|
|
282
|
+
|
|
283
|
+
# Sub-ranges compose with order: and work on transactions, snapshots, and readers
|
|
284
|
+
db.scan_prefix("user:", from: "100", to: "200", order: :desc)
|
|
285
|
+
```
|
|
286
|
+
|
|
207
287
|
### Merge Operations
|
|
208
288
|
|
|
209
289
|
Merge operations allow you to combine values without reading them first, useful for counters, append-only logs, and similar patterns:
|
|
@@ -423,12 +503,33 @@ SlateDb::Reader.open("/tmp/mydb", url: "s3://bucket/path") do |reader|
|
|
|
423
503
|
end
|
|
424
504
|
end
|
|
425
505
|
|
|
426
|
-
# Open at a specific checkpoint
|
|
506
|
+
# Open at a specific checkpoint (pins the reader to that checkpoint's state)
|
|
427
507
|
SlateDb::Reader.open("/tmp/mydb",
|
|
428
508
|
url: "s3://bucket/path",
|
|
429
509
|
checkpoint_id: "uuid-here") do |reader|
|
|
430
510
|
reader.get("key")
|
|
431
511
|
end
|
|
512
|
+
|
|
513
|
+
# Follow the latest state without managing a checkpoint (SlateDB >= 0.15.0).
|
|
514
|
+
# This performs no object-store writes but offers no protection from garbage
|
|
515
|
+
# collection, so it suits read-only or mirrored databases. By default (neither
|
|
516
|
+
# checkpoint_id nor follow_latest set) the reader creates and periodically
|
|
517
|
+
# refreshes its own checkpoint instead.
|
|
518
|
+
SlateDb::Reader.open("/tmp/mydb",
|
|
519
|
+
url: "s3://bucket/path",
|
|
520
|
+
follow_latest: true) do |reader|
|
|
521
|
+
reader.get("key")
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Enable the reader's on-disk cache and cap its open file handles
|
|
525
|
+
# (max_open_file_handles, added in SlateDB 0.13.0, only takes effect when
|
|
526
|
+
# cache_root is set, since that is what enables the cached object store).
|
|
527
|
+
SlateDb::Reader.open("/tmp/mydb",
|
|
528
|
+
url: "s3://bucket/path",
|
|
529
|
+
cache_root: "/var/cache/slatedb",
|
|
530
|
+
max_open_file_handles: 256) do |reader|
|
|
531
|
+
reader.get("key")
|
|
532
|
+
end
|
|
432
533
|
```
|
|
433
534
|
|
|
434
535
|
### Admin Operations
|
|
@@ -527,9 +628,40 @@ Exception hierarchy:
|
|
|
527
628
|
|
|
528
629
|
## Requirements
|
|
529
630
|
|
|
530
|
-
- Ruby 3.
|
|
631
|
+
- Ruby 3.3+
|
|
531
632
|
- Rust toolchain (for building from source)
|
|
532
633
|
|
|
634
|
+
## Releasing
|
|
635
|
+
|
|
636
|
+
Releases publish a generic `ruby` platform gem and **precompiled native gems**
|
|
637
|
+
for six platforms (`x86_64-linux`, `aarch64-linux`, `x86_64-linux-musl`,
|
|
638
|
+
`aarch64-linux-musl`, `arm64-darwin`, `x86_64-darwin`) to
|
|
639
|
+
[rubygems.org](https://rubygems.org/gems/slatedb). The generic gem keeps
|
|
640
|
+
RubyGems' latest-version metadata aligned; supported platforms should still
|
|
641
|
+
resolve to their matching precompiled native gem.
|
|
642
|
+
|
|
643
|
+
### Cutting a release
|
|
644
|
+
|
|
645
|
+
The gem version comes from `lib/slatedb/version.rb`, not from the tag. Use the
|
|
646
|
+
mise helper to bump, commit, tag, and push in one step (must be on `main` with a
|
|
647
|
+
clean working tree):
|
|
648
|
+
|
|
649
|
+
```bash
|
|
650
|
+
mise run release:cut 0.4.3
|
|
651
|
+
# or: mise run release:cut 0.4.3 --no-push
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
That updates `SlateDb::VERSION`, commits `Release v0.4.3`, creates tag `v0.4.3`,
|
|
655
|
+
and pushes the commit + tag (unless `--no-push`). The tag triggers the release
|
|
656
|
+
pipeline.
|
|
657
|
+
|
|
658
|
+
The `release:verify-tag` step fails the build if the tag and
|
|
659
|
+
`SlateDb::VERSION` disagree, so the tag (`v0.4.3`) must match `version.rb`
|
|
660
|
+
(`0.4.3`).
|
|
661
|
+
|
|
662
|
+
To exercise packaging without publishing, trigger a manual build on the
|
|
663
|
+
`slatedb-rb-release` pipeline or set `DRY_RUN=true`.
|
|
664
|
+
|
|
533
665
|
## Development
|
|
534
666
|
|
|
535
667
|
After checking out the repo, run:
|
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.15.0"
|
|
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
|
})
|
|
@@ -217,6 +230,7 @@ impl Admin {
|
|
|
217
230
|
Some(GarbageCollectorDirectoryOptions {
|
|
218
231
|
interval: default_opts.as_ref().and_then(|o| o.interval),
|
|
219
232
|
min_age: std::time::Duration::from_millis(ms),
|
|
233
|
+
dry_run: default_opts.as_ref().map(|o| o.dry_run).unwrap_or(false),
|
|
220
234
|
})
|
|
221
235
|
} else {
|
|
222
236
|
default_opts
|
|
@@ -230,12 +244,17 @@ impl Admin {
|
|
|
230
244
|
default_opts.manifest_options,
|
|
231
245
|
),
|
|
232
246
|
wal_options: make_dir_opts(wal_min_age, min_age, default_opts.wal_options),
|
|
247
|
+
wal_fence_options: default_opts.wal_fence_options,
|
|
233
248
|
compacted_options: make_dir_opts(
|
|
234
249
|
compacted_min_age,
|
|
235
250
|
min_age,
|
|
236
251
|
default_opts.compacted_options,
|
|
237
252
|
),
|
|
238
253
|
compactions_options: default_opts.compactions_options,
|
|
254
|
+
detach_options: default_opts.detach_options,
|
|
255
|
+
metric_level: default_opts.metric_level,
|
|
256
|
+
boundary_files_enabled: default_opts.boundary_files_enabled,
|
|
257
|
+
object_store_max_retries: default_opts.object_store_max_retries,
|
|
239
258
|
}
|
|
240
259
|
};
|
|
241
260
|
|