Skip to content

Commit 81839aa

Browse files
glittersharkrschmukler
authored andcommitted
feat(): Allow creating database snapshots
Add a `create_snapshot` function, that creates a RocksDB database snapshot from the given database handle and returns it. There's a pretty fantastic amount of horribly unsafe Rust going on here - primarily caused by the fact that the rust-rocksdb snapshot type is, unlike Iterator or ColumnFamily or any of the other types that really *should* be, parametrized by the lifetime of a borrowed reference to its owner database. The abstraction provided by Rustler doesn't allow non-static lifetime-parametrized struct types to be returned back to Erlang as resources, for some technical type-mismatch reasons but also practically because relying on the Erlang garbage collector to free our stuff doesn't fit at *all* with rust's lifetime model. The workaround here is to use `mem::transmute` (I told you it was crazy...) to turn the snapshot reference from a `Snapshot<'a>` to a `Snapshot<'static>`, and pass *that*, along with a held ARC reference to the owning DB, to Erlang, and then when the Erlang GC decides it's time to free the snapshot, go *back* to a first unbounded, then elided lifetime to force the snapshot to be dropped. I've done some cursory checking and regular operation (creating a snapshot from a process then letting that process die) appears to not leak the snapshots, but we should keep an eye on this to make sure we don't have any leaks (or worse) in the future. As a further note, the bit that parses out the argument for the NIFs that now accept *either* a DB or a snapshot is a little mucky - that should be revisited at a later date to see if we can clean it up a little.
1 parent baad4ab commit 81839aa

File tree

7 files changed

+355
-53
lines changed

7 files changed

+355
-53
lines changed

lib/rox.ex

+40-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule Rox do
44
55
"""
66

7-
alias __MODULE__.{DB,ColumnFamily,Native,Utils,Cursor}
7+
alias __MODULE__.{DB, ColumnFamily, Native, Utils, Cursor, Snapshot}
88

99
@type compaction_style :: :level | :universal | :fifo | :none
1010
@type compression_type :: :snappy | :zlib | :bzip2 | :lz4 | :lz4h | :none
@@ -139,6 +139,20 @@ defmodule Rox do
139139
end
140140
end
141141

142+
@doc """
143+
Creates a point-in-time snapshot of the given `DB`.
144+
145+
Snapshots are read only views of the database at a point in time - no changes to the database will
146+
be reflected in the view of the snapshot.
147+
148+
"""
149+
@spec create_snapshot(DB.t) :: {:ok, Snapshot.t} | {:error, any}
150+
def create_snapshot(db) do
151+
with {:ok, snapshot} <- Native.create_snapshot(db.resource) do
152+
{:ok, Snapshot.wrap_resource(db, snapshot)}
153+
end
154+
end
155+
142156
@doc """
143157
Create a column family in `db` with `name` and `opts`.
144158
@@ -151,17 +165,23 @@ defmodule Rox do
151165
end
152166

153167
@doc """
154-
Gets an existing `ColumnFamily.t` from the database.
168+
Gets an existing `ColumnFamily.t` from the database or snapshot.
155169
156-
The column family must have been created via `create_cf/2` or from `open/3` with the `auto_create_column_families` option.
170+
The column family must have been created via `create_cf/2` or from `open/3` with the
171+
`auto_create_column_families` option.
157172
158173
"""
159-
@spec cf_handle(DB.t, ColumnFamily.name) :: {:ok, ColumnFamily.t} | {:error, any}
174+
@spec cf_handle(DB.t | Snapshot.t, ColumnFamily.name) :: {:ok, ColumnFamily.t} | {:error, any}
160175
def cf_handle(%DB{resource: raw_db} = db, name) do
161176
with {:ok, result} <- Native.cf_handle(raw_db, name) do
162177
{:ok, ColumnFamily.wrap_resource(db, result, name)}
163178
end
164179
end
180+
def cf_handle(%Snapshot{db: %DB{resource: raw_db}} = snapshot, name) do
181+
with {:ok, result} <- Native.cf_handle(raw_db, name) do
182+
{:ok, ColumnFamily.wrap_resource(snapshot, result, name)}
183+
end
184+
end
165185

166186

167187
@doc """
@@ -181,15 +201,19 @@ defmodule Rox do
181201

182202

183203
@doc """
184-
Get a key/value pair in the databse or column family with the specified `key`.
204+
Get a key/value pair in the given column family of the given snapshot or database with the
205+
specified `key`.
185206
186207
Optionally takes a list of `read_options`.
187208
188209
For non-binary terms that were stored, they will be automatically decoded.
189210
190211
"""
191-
@spec get(DB.t | ColumnFamily.t, key, read_options) :: {:ok, binary} | {:ok, value} | :not_found | {:error, any}
192-
def get(db_or_cf, key, opts \\ [])
212+
@spec get(DB.t | ColumnFamily.t | Snapshot.t, key, read_options)
213+
:: {:ok, value}
214+
| :not_found
215+
| {:error, any}
216+
def get(db_snapshot_or_cf, key, opts \\ [])
193217
def get(%DB{resource: db}, key, opts) when is_binary(key) and is_list(opts) do
194218
Native.get(db, key, to_map(opts))
195219
|> Utils.decode
@@ -198,6 +222,10 @@ defmodule Rox do
198222
Native.get_cf(db, cf, key, to_map(opts))
199223
|> Utils.decode
200224
end
225+
def get(%Snapshot{resource: snapshot}, key, opts) when is_binary(key) and is_list(opts) do
226+
Native.get(snapshot, key, to_map(opts))
227+
|> Utils.decode
228+
end
201229

202230
@doc """
203231
Returns a `Cursor.t` which will iterate records from the provided database or
@@ -226,6 +254,11 @@ defmodule Rox do
226254
Cursor.wrap_resource(resource, mode)
227255
end
228256
end
257+
def stream(%Snapshot{resource: snapshot}, mode) do
258+
with {:ok, resource} <- Native.iterate(snapshot, mode) do
259+
Cursor.wrap_resource(resource, mode)
260+
end
261+
end
229262

230263

231264
@doc """

lib/rox/column_family.ex

+20-7
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
defmodule Rox.ColumnFamily do
22
@moduledoc """
33
Struct module representing a handle for a column family within a database.
4-
4+
55
For working with the column family, see the functions in the top level
66
`Rox` module.
7-
7+
88
Implements the `Collectable` and `Enumerable` protocols.
99
1010
"""
1111

12-
alias Rox.DB
12+
alias Rox.{DB, Snapshot}
1313

1414
@typedoc "A reference to a RocksDB column family"
1515
@type t :: %__MODULE__{
16-
db_resource: binary, cf_resource: binary,
17-
db_reference: reference, name: binary,
16+
db_resource: binary,
17+
cf_resource: binary,
18+
db_reference: reference,
19+
name: binary,
1820
}
1921
defstruct [:db_reference, :db_resource, :cf_resource, :name]
2022

@@ -23,8 +25,19 @@ defmodule Rox.ColumnFamily do
2325
@doc false
2426
def wrap_resource(%DB{resource: db_resource, reference: db_reference}, resource, name) do
2527
%__MODULE__{
26-
db_resource: db_resource, db_reference: db_reference,
27-
cf_resource: resource, name: name
28+
db_resource: db_resource,
29+
db_reference: db_reference,
30+
cf_resource: resource,
31+
name: name
32+
}
33+
end
34+
35+
def wrap_resource(%Snapshot{resource: db_resource, reference: db_reference}, resource, name) do
36+
%__MODULE__{
37+
db_resource: db_resource,
38+
db_reference: db_reference,
39+
cf_resource: resource,
40+
name: name
2841
}
2942
end
3043

lib/rox/db.ex

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
defmodule Rox.DB do
22
@moduledoc """
33
Struct module representing a handle for a database.
4-
4+
55
For working with the database, see the functions in the top
66
level `Rox` module.
7-
7+
88
Implements the `Collectable` and `Enumerable` protocols.
99
1010
"""

lib/rox/native.ex

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ defmodule Rox.Native do
1111
end
1212
end
1313

14+
def create_snapshot(_) do
15+
case :erlang.phash2(1, 1) do
16+
0 -> raise "Nif not loaded"
17+
1 -> {:ok, ""}
18+
2 -> {:error, ""}
19+
end
20+
end
21+
1422
def count(_) do
1523
case :erlang.phash2(1, 1) do
1624
0 -> raise "Nif not loaded"

lib/rox/snapshot.ex

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
defmodule Rox.Snapshot do
2+
@moduledoc """
3+
Struct module representing a handle for a database snapshot.
4+
5+
Snapshots support all read operations that a `Rox.DB` supports, and implement `Enumerable`, but
6+
not `Collectable`
7+
8+
"""
9+
10+
alias Rox.DB
11+
12+
13+
14+
@typedoc "A reference to a RocksDB database snapshot"
15+
@type t :: %__MODULE__{
16+
resource: binary,
17+
reference: reference,
18+
db: DB.t
19+
}
20+
21+
@enforce_keys [:resource, :reference, :db]
22+
defstruct [
23+
:resource,
24+
:reference,
25+
:db
26+
]
27+
28+
@doc false
29+
def wrap_resource(%DB{} = db, resource) do
30+
%__MODULE__{resource: resource, reference: make_ref(), db: db}
31+
end
32+
33+
defimpl Inspect do
34+
import Inspect.Algebra
35+
36+
def inspect(handle, opts) do
37+
"#Rox.Snapshot<#{to_doc(handle.reference, opts)}>"
38+
end
39+
end
40+
41+
defimpl Enumerable do
42+
def count(snapshot), do: {:ok, Rox.count(snapshot)}
43+
44+
def member?(snapshot, {key, val}) do
45+
with {:ok, stored_val} <- Rox.get(snapshot, key) do
46+
stored_val == {:ok, val}
47+
else
48+
_ -> {:ok, false}
49+
end
50+
end
51+
def member?(_, _), do: {:ok, false}
52+
53+
def reduce(snapshot, cmd, fun) do
54+
Rox.stream(snapshot)
55+
|> Enumerable.reduce(cmd, fun)
56+
end
57+
end
58+
end

0 commit comments

Comments
 (0)