Coverage report: /home/ellis/comp/core/lib/rdb/db.lisp
Kind | Covered | All | % |
expression | 240 | 1048 | 22.9 |
branch | 4 | 24 | 16.7 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; rdb/db.lisp --- RDB Database API
3
;; RocksDB Implementation of OBJ/DB protocol.
7
;; The DB protocol is also partially implemented by the low-level structures
10
;; It is safe to call most functions on the same underlying Alien RocksDB
11
;; object from multiple threads. Other objects such as WriteBatch and Iterator
12
;; /may/ require a Lisp-side synchronization.
16
;; RocksDB has several variations on the concept of 'transaction':
19
When using a TransactionDB, all keys that are written are locked internally by
20
RocksDB to perform conflict detection. If a key cannot be locked, the
21
operation will return an error. When the transaction is committed, it is
22
guaranteed to succeed as long as the database is able to be written to.
24
A TransactionDB can be better for workloads with heavy concurrency compared to
25
an OptimisticTransactionDB. However, there is a small locking overhead when
26
TransactionDB is used. A TransactionDB will do conflict checking for all write
27
operations (Put, Delete and Merge), including writes performed outside a
33
The WriteBatch holds a sequence of edits to be made to the database - these
34
edits within the batch are applied in order when written.
36
Apart from its atomicity benefits, WriteBatch may also be used to speed up
37
bulk updates by placing lots of individual mutations into the same batch.
43
The WBWI (Write Batch With Index) encapsulates a WriteBatch and an Index into
44
that WriteBatch. The index in use is a Skip List. The purpose of the WBWI is
45
to sit above the DB, and offer the same basic operations as the DB,
46
i.e. Writes - Put, Delete, and Merge, and Reads - Get, and newIterator.
48
Write operations on the WBWI are serialized into the WriteBatch (of the WBWI)
49
rather than acting directly on the DB. The WriteBatch can later be written
50
atomically to the DB by calling db.write(wbwi).
52
Read operations can either be solely against the
53
WriteBatch (e.g. GetFromBatch), or they can be read-through operations. A
54
read-through operation, (e.g. GetFromBatchAndDB), first tries to read from the
55
WriteBatch, if there is no updated entry in the WriteBatch then it
56
subsequently reads from the DB.
58
The WBWI can be used as a component if one wishes to build Transaction
59
Semantics atop RocksDB. The WBWI by itself isolates the Write Path to a local
60
in-memory store and allows you to RYOW (Read-Your-Own-Writes) before data is
61
atomically written to the database.
63
It is a key component in RocksDB's Pessimistic and Optimistic Transaction
68
;; WBWIs are ideal as a transaction building block and should be used to build
69
;; higher-level transaction objects.
73
;; Snapshots capture a point-in-time view of a RocksDB instance at the time of
74
;; creation. Snapshots do not persist across DB sessions and are internally
75
;; stored in a linked-list.
79
;; Checkpoints allow us to take a snapshot of a running RocksDB instance like
80
;; Snapshots, but in a separate directory. Checkpoints persist across DB
81
;; sessions. Checkpoints can be opened read-only or as read-write and be used
82
;; for both full and incremental backups (as long as backups are on the same
87
;; Backups are built on top of Checkpoints.
89
;; Backup Engines control a single directory that can store any number of
90
;; backups. It uses a custom on-disk format as shown below.
92
#| directory structure
102
└── 000007_1498774076_590.sst
109
(defvar *rocksdb-backend-options* '(columns temp path (open . t)
111
sap merge-op comparator prefix-op logger))
113
;; TODO 2024-12-31: may want to have a :STORE backend-option to allow a fresh
114
;; db to be backlined to a parent store instance.
115
(defvar *rdb-backend-options* (append *rocksdb-backend-options* '(backup secondary snapshots checkpoints)))
117
(defvar *rdb-default-column-name* "default")
119
(defmethods set-database-backend-option
120
(((db rdb) (key (eql :close)) (val (eql :auto)))
121
"Arrange for SHUTDOWN-DB to be called when there are no more references to DB."
122
(sb-ext:finalize db (lambda () (shutdown-db db))))
123
(((db rdb) (key (eql :merge-op)) val)
124
"Assign a MERGE-OP to this database."
125
(setf (db-opt db :merge-operator :push t) val))
126
(((db rdb) (key (eql :comparator)) val)
127
"Assign a custom COMPARATOR to this database."
128
(setf (db-opt db :comparator :push t) val))
129
(((db rdb) (key (eql :prefix-op)) val)
130
"Assign a custom SLICETRANSFORM to this database to be used as a prefix
132
(setf (db-opt db :prefix-extractor :push t) val))
133
(((db rdb) (key (eql :logger)) val)
134
(setf (db-opt db :info-log :push t) val)))
136
(set-database-backend :rocksdb *rocksdb-backend-options*
137
(lambda () (load-rocksdb *save-database-backend-on-load*)))
139
(set-database-backend :rdb *rdb-backend-options*
140
(lambda () (db::%load-database-backend :rocksdb)))
142
(defmethod load-opts ((db rdb) &key backfill)
143
(with-latest-options (name db) (db-opts cf-names cf-opts)
145
(loop for name across cf-names
146
for opt across cf-opts
148
(let ((cf-opts (make-rdb-opts)))
149
(setf (sap cf-opts) opt)
150
(when (eq backfill :full) (backfill-opts cf-opts :full t))
151
(make-rdb-cf name :opts cf-opts)))
153
(setf (db-opts db) (make-rdb-opts* db-opts))
154
(when backfill (backfill-opts (db-opts db) :full (eq backfill :full)))
157
(defmethod make-db ((engine (eql :rocksdb)) &rest initargs &key
162
(opts (default-rdb-opts))
164
(declare (ignore engine initargs))
166
(set-db-opt opts :merge-operator merge-op :push t))
168
(set-db-opt opts :prefix-extractor prefix-op :push t))
170
(set-db-opt opts :info-log logger :push t))
172
:name (or name (namestring path) (string-downcase (gensym "rocksdb")))
177
(defmethod query-db ((db rdb) (query (eql :get)) &key key column &allow-other-keys)
178
(declare (ignore query))
179
(get-val db key :column column))
182
(defclass rdb-column-family (rdb-column)
183
((cf :initarg :cf :type rdb-cf :accessor cf))
184
(:default-initargs :cf (make-rdb-cf (symbol-name (gensym "#"))))
185
(:documentation "High-level Lisp-side RocksDB Column Family base class. Implements the COLUMN
186
protocol and contains a CF slot which contains an RDB-CF structure
187
object. (SAP CF) is the raw pointer."))
189
(defaccessor name ((self rdb-column-family)) (name (cf self)))
190
(defaccessor sap ((self rdb-column-family)) (sap (cf self)))
191
(defaccessor column-opts ((self rdb-column-family)) (rdb-cf-opts (cf self)))
193
(defun schema-from-rdb-column-families (columns)
194
"Convert a sequence of RDB-COLUMN-FAMILYs to a SCHEMA."
198
(make-field :name (keywordicate (name x)) :type (column-type x)))
201
(defmethod destroy-column ((self rdb-column-family) &optional error)
202
(destroy-column (cf self) error))
204
(defmethod close-column ((self rdb-column-family) &optional error)
205
(close-column (cf self) error))
207
(defmethod load-field ((self rdb-column-family) (field field))
208
(let ((type (field-type field))
209
(ctype (column-type self)))
212
(atom (if (atom ctype)
213
(setf ctype (cons ctype type))
214
(setf (cdr ctype) type)))
215
(list (setf (car ctype) (car type)
217
(if (and (listp (cdr type))
218
(= 1 (length (cdr type))))
223
(defmethod change-class ((self field) (new-class (eql 'rdb-column-family)) &key)
224
(make-instance new-class :cf (make-rdb-cf (field-name self)) :type (field-type self)))
226
(defmethod change-class ((self rdb-cf) (new-class (eql 'rdb-column-family)) &key)
227
(make-instance new-class :cf self))
229
(defmethod change-class ((self column) (new-class (eql 'rdb-column-family)) &key name)
230
(let ((ret (make-instance new-class :type (column-type self))))
231
(when name (setf (name ret) name))
235
(defclass rdb-database (database)
236
((txn :initform nil :type (or null rdb-transaction-db) :initarg :txn :accessor transaction-db)
237
(backup :initform nil :type (or null rdb-backup-engine) :initarg :backup :accessor db-backup)
238
(snapshots :initform (make-array 0 :element-type 'rdb-snapshot :adjustable t)
239
:type (vector rdb-snapshot)
241
:accessor db-snapshots)
242
(checkpoints :initform (make-array 0 :element-type 'rdb-checkpoint :adjustable t)
243
:type (vector rdb-checkpoint)
244
:initarg :checkpoints
245
:accessor db-checkpoints)
246
(secondary :initform nil :type (or null rdb-secondary-db) :initarg :secondary :accessor secondary-db)
247
(columns :initarg :columns :accessor columns))
249
:db (make-db :rocksdb :opts (default-rdb-opts))
250
;; Note that we don't pre-populate this slot with the 'default' column
251
;; which is present on creation of a RocksDB database. Usually there isn't
252
;; much need to access this column directly as you can just access the
253
;; database directly, which will access the default column internally.
254
:columns (make-array 0 :element-type 'rdb-column-family
258
(defmethods set-database-backend-option
259
(((db rdb-database) (key (eql :close)) (val (eql :auto)))
260
"Arrange for SHUTDOWN-DB to be called when there are no more references to DB."
261
(sb-ext:finalize db (lambda () (close-db db))))
262
(((db rdb-database) (key (eql :merge-op)) val)
263
"Assign a MERGE-OP to this database."
264
(setf (db-opt db :merge-operator :push t) val))
265
(((db rdb-database) (key (eql :comparator)) val)
266
"Assign a custom COMPARATOR to this database."
267
(setf (db-opt (db db) :comparator) val))
268
(((db rdb-database) (key (eql :prefix-op)) val)
269
"Assign a custom SLICETRANSFORM to this database to be used as a prefix
271
(setf (db-opt (db db) :prefix-extractor :push t) val)))
273
(defmethod load-opts ((self rdb-database) &key (backfill t))
274
;; order is determined by RocksDB
276
(map 'vector (lambda (x) (make-instance 'rdb-column-family :cf x))
277
(load-opts (db self) :backfill backfill)))
280
(defmethod repair-db ((self rdb-database) &key)
281
(repair-db (db self)))
283
(defmethod merge-columns ((self rdb-database) (columns vector))
284
(loop for c across columns
285
do (if-let ((found (find-column c self)))
286
(setf (aref (columns self) (position found (columns self))) c)
287
(vector-push-extend c (columns self)))))
289
(defmethod backfill-opts ((self rdb-database) &key)
290
(backfill-opts (db-opts self)))
292
(defmethod reset ((self rdb-database) &key (columns t) (opts t))
296
(make-array 0 :element-type 'rdb-column-family
300
(setf (db-opts self) (if (eql t opts) (default-rdb-opts) opts))))
302
(defmethod open-column ((self rdb-database) (col string) &key)
303
(open-column (db self) (cf (find-column col self))))
305
(defmethod open-column ((self rdb-database) (col symbol) &key)
306
(open-column (db self) (cf (find-column (string-downcase col) self))))
308
(defmethod open-column ((self rdb-database) (col rdb-column-family) &key)
309
(open-column (db self) (cf col)))
311
(defmethod open-columns ((self rdb-database) &rest columns)
313
(open-column self c)))
315
(defmethod find-column ((cf string) (self rdb-database) &key)
316
(find cf (columns self) :key 'name :test 'equal))
318
(defmethod add-column ((cf rdb-cf) (db rdb-database))
319
(vector-push-extend (make-instance 'rdb-column-family :cf cf) (columns db)))
321
(defmethod open-with-columns ((db rdb-database) &rest names)
327
collect (if-let ((col (find-column n db)))
330
(make-instance 'rdb-column-family
334
(multiple-value-bind (db-sap cfs) (open-cfs-raw (db-opts db) (name db)
335
(loop for c across cols
337
(loop for c across cols
338
collect (sap (column-opts c))))
339
(setf (sap db) db-sap)
340
(loop for c across cfs
341
do (when-let ((col (find-column (name c) db)))
342
(setf (sap (cf col)) c)))
345
(defmethod open-columns* ((self rdb-database))
346
(let ((names) (opts))
347
(loop for c across (columns self)
348
do (push (name c) names)
349
do (push (sap (column-opts c)) opts))
352
(unless (member *rdb-default-column-name* names :test 'string=)
353
(push *rdb-default-column-name* names)
354
(push (sap (db-opts self)) opts))
355
(multiple-value-bind (db cfs)
356
(open-cfs-raw (sap (db-opts self)) (name self) names opts)
358
(let ((len (length names)))
361
for cf = (deref cfs i)
362
do (when-let ((c (find-column (pop names) self)))
366
(defmethod close-columns ((self rdb-database))
367
(loop for cf across (columns self)
368
;; unless (string= (name cf) *rdb-default-column-name*)
369
do (close-column cf)))
371
(defmethods insert-key
372
(((self rdb-database) key val &key column)
373
(if-let ((column (and column (find-column column self))))
374
(if-let ((sap (sap column)))
380
(rocksdb-writeoptions-create))
381
(rdb-error "column-family is not open"))
382
(put-key self key val)))
383
(((self rdb-database) (key string) (val string) &key column)
384
(if-let ((column (and column (find-column column self))))
385
(if-let ((sap (sap column)))
389
(string-to-octets key)
390
(string-to-octets val)
391
(rocksdb-writeoptions-create))
392
(rdb-error "column-family is not open"))
393
(put-key self key val)))
394
(((self rdb-database) (key string) val &key column)
395
(if-let ((column (and column (find-column column self))))
396
(if-let ((sap (sap column)))
400
(string-to-octets key)
402
(rocksdb-writeoptions-create))
403
(rdb-error "column-family is not open"))
404
(put-key self key val)))
405
(((self rdb-database) key (val string) &key column)
406
(if-let ((column (and column (find-column column self))))
407
(if-let ((sap (sap column)))
412
(string-to-octets val)
413
(rocksdb-writeoptions-create))
414
(rdb-error "column-family is not open"))
415
(put-key self key val)))
416
(((self rdb) (key string) (val string) &key column)
417
(insert-key self (string-to-octets key) (string-to-octets val) :column column))
418
(((self rdb) (key string) val &key column)
419
(insert-key self (string-to-octets key) val :column column))
420
(((self rdb) key (val string) &key column)
421
(insert-key self key (string-to-octets val) :column column)))
423
(defmethod insert-kv ((self rdb) (kv kv) &key column (opts (rocksdb-writeoptions-create)))
425
(let ((column (etypecase column
427
(t (find column (columns self)
430
(put-cf-raw (sap self)
437
(defmethod iter ((self rdb-database) &key column (opts (rocksdb-readoptions-create)))
439
(rdb-column-family (iter (db self) :cf (cf column) :opts opts))
440
(null (iter (db self) :opts opts))
441
(symbol (iter (db self) :cf (cf (find-column column self)) :opts opts))
442
(simple-string (iter (db self) :cf (cf (find-column column self)) :opts opts))
443
(rdb-cf (iter (db self) :cf column :opts opts))
444
(t (iter (db self) :opts opts :cf column))))
447
(((self rdb-database) (key string) &key (opts (rocksdb-readoptions-create)) column)
448
(let ((sap (sap self)))
450
(get-cf-str-raw sap (sap (find-column column self)) key opts)
451
(get-kv-str-raw sap key opts))))
452
(((self rdb-database) key &key (opts (rocksdb-readoptions-create)) column)
453
(let ((sap (sap self)))
455
(get-cf-raw sap (sap (find-column column self)) key opts)
456
(get-kv-raw sap key opts)))))
458
(defmethod multi-get ((self rdb-database) keys &key (data-type 'octet-vector) (opts (rocksdb-readoptions-create)) columns)
459
(multi-get (db self) keys :data-type data-type :opts opts :cf (mapcar 'cf columns)))
461
(defmethod create-column ((db rdb-database) (col rdb-column-family))
462
(if (equal (name col) *rdb-default-column-name*)
463
(rdb-default-column-warning "ignoring attempt to create 'default' column-family: ~A" col)
464
(setf (sap col) (create-cf-raw (sap db) (name col) (sap (column-opts col)))))
465
;; (open-column db col)
468
(defmethod create-columns ((self rdb-database))
469
(if (null (sap self))
470
(warn 'db-missing :message "ignoring attempt to create column-families before opening")
471
(loop for cf across (columns self)
472
do (create-column self cf))))
474
(defmethod find-column ((cf string) (self rdb-database) &key)
475
"Find a column by name."
476
(find cf (columns self) :key 'name :test 'equal))
478
(defmethod find-column ((cf symbol) (self rdb-database) &key)
479
(find (string-downcase cf) (columns self) :key 'name :test 'string=))
481
(defmethod find-column ((col rdb-column-family) (self rdb-database) &key)
482
(find (string-downcase (name col)) (columns self) :key 'name :test 'string=))
484
(defmethod (setf find-column) ((new rdb-column-family) (cf string) (self rdb-database) &key)
485
"Find and replace a column by name."
486
(nsubstitute new (find-column cf self) (columns self)))
488
(defmethod database-version ((self rdb-database))
489
"Return the version tag or nil if unmarked"
490
(when-let ((db (and #1=(db self) (sap #1#))))
491
(rocksdb-property-value db "rocksdb.current-super-version-number")))
493
(defaccessor name ((self rdb-database)) (name (db self)))
494
(defaccessor sap ((self rdb-database)) (sap (db self)))
495
(defaccessor db-opts ((self rdb-database)) (db-opts (db self)))
497
((self rdb-database) key) (db-opt (db-opts self) key)
498
(new (self rdb-database) key &key push)
499
(prog1 (setf (db-opt (db-opts self) key) new)
500
(when push (push-sap (db-opts self) key))))
503
(((self rdb-database) (name string))
504
(db-prop (db self) name))
505
(((self rdb-database) (name symbol))
506
(db-prop (db self) (string-downcase (concatenate 'string "rocksdb." (symbol-name name))))))
508
(defmethod push-opts ((self rdb-database))
509
(with-slots (opts) (db self)
512
(defmethod print-stats ((self rdb-database) &optional stream)
513
(print-stats (db self) stream))
515
(defmethod db-metadata ((self rdb-database) &optional type)
516
(db-metadata (db self) type))
518
(defmethod db-stats ((self rdb-database) &optional (type (rocksdb-statistics-level "all")))
519
(db-stats (db self) type))
521
(defmethod ingest-db ((self rdb-database) files &key (opts (rocksdb-ingestexternalfileoptions-create))
524
(ingest-db (db self) files :opts opts :column (find-column column self))
525
(ingest-db (db self) files :opts opts)))
528
(((engine (eql :rdb)) &rest initargs &key columns &allow-other-keys)
529
(declare (ignore engine))
530
(remf initargs :columns)
531
(let ((db (make-instance 'rdb-database :db (apply 'make-db :rocksdb initargs))))
532
(when columns (setf (columns db) (coerce (mapcar (lambda (x) (cf x)) columns) 'vector)))
534
(((engine (eql :rdb-backup)) &key path (db *db*))
535
(setf (db-backup db) (backup-db db :path path)))
536
(((engine (eql :rdb-transaction)) &key path opts (db *db*))
537
(setf (transaction-db db) (open-transaction-db db :opts opts :path path)))
538
(((engine (eql :rdb-secondary)) &key path opts (db *db*))
539
(setf (secondary-db db) (open-secondary-db db :opts opts :path path))))
541
(defmethod derive-schema ((self rdb-database))
543
(loop for c across (columns self)
544
collect (cf-to-field (cf c)))))
546
(defmethod open-db ((self rdb-database)) (open-db (db self)) self)
547
(defmethod open-transaction-db ((self rdb-database) &key path (opts (rocksdb-transactiondb-options-create)) optimistic)
548
(setf (transaction-db self) (open-transaction-db (db self) :opts opts :path path :optimistic optimistic)))
550
(defmethod open-backup-engine ((self rdb-database) &key path)
551
(setf (db-backup self) (open-backup-engine (db self) :path path)))
553
(defmethod open-secondary-db ((self rdb-database) &key path opts)
554
(setf (secondary-db self) (open-secondary-db (db self) :opts opts :path path)))
556
(defmethod open-checkpoint-db ((self rdb-database) &key path)
557
(vector-push-extend (%make-checkpoint (sap self) path) (db-checkpoints self)))
559
(defmethod snapshot-db ((self rdb-database))
560
(vector-push-extend (snapshot-db (db self)) (db-snapshots self)))
562
(defmethod flush-db ((self rdb-database) &rest args &key &allow-other-keys) (apply 'flush-db (db self) args))
564
(defmethod close-db ((self rdb-database) &key)
566
(close-db (db self)))
568
(defmethod db-closed-p ((self rdb-database)) (db-closed-p (db self)))
569
(defmethod db-open-p ((self rdb-database)) (db-open-p (db self)))
571
(defmethod destroy-db ((self rdb-database))
572
(destroy-db (db self)))
574
(defmethod close-backup-engine ((self rdb-database))
575
(with-slots (backup) self
576
(unless (null backup)
577
(setf backup (close-backup-engine backup)))))
579
(defmethod close-transaction-db ((self rdb-database))
580
(when-let ((sap (transaction-db self)))
581
(close-transaction-db sap)))
583
(defmethod shutdown-db ((self rdb-database) &key)
584
(close-backup-engine self)
585
(close-transaction-db self)
587
(shutdown-db (db self)))
589
(defmethod get-value (elt (self rdb-database))
590
(get-value elt (db self)))
592
(defmethod put-key ((self rdb-database) key val)
593
(put-key (db self) key val))
595
(defmethod put-kv ((self rdb-database) (kv kv))
596
(put-kv (db self) kv))
598
(defmethod delete-key ((self rdb-database) key &key)
599
(delete-key (db self) key))
601
(defmethod merge-key ((self rdb-database) key val &key (opts (rocksdb-writeoptions-create)))
602
(merge-key (db self) key val :opts opts))
604
(defmethod merge-kv ((self rdb-database) kv &key (opts (rocksdb-writeoptions-create)))
605
(merge-kv-raw (sap self) (kv-key kv) (kv-val kv) opts))
607
(defmethod add-column (col (self rdb-database))
608
(vector-push-extend col (coerce (columns self) 'vector)))
610
(defmethod destroy-columns ((self rdb-database))
611
(with-slots (columns) self
612
(loop for cf across columns
613
do (setf cf (destroy-column cf)))))
615
(defmethod load-schema ((self rdb-database) (schema schema))
616
"Load SCHEMA into rdb database object SELF. This will add any missing rdb-cfs
617
and update existing key/value types for cfs with the same name. Existing cfs
618
only get their type slots updated on non-nil values."
619
(loop for field across (fields schema)
620
do (if-let ((col (find-column (field-name field) self)))
621
(load-field col field)
624
(make-instance 'rdb-column-family :cf (make-rdb-cf (field-name field)) :type (field-type field))
627
finally (return self)))
630
(defmethod make-transaction ((self rdb-database)
631
&key (write-opts (rocksdb-writeoptions-create))
636
(opts (rocksdb-transaction-options-create))
637
(db-opts (rocksdb-transactiondb-options-create)))
639
(let ((txn-db (or (transaction-db self)
640
(setf (transaction-db self)
641
(open-transaction-db self :opts db-opts :path path :optimistic optimistic)))))
642
(let ((obj (make-transaction txn-db :write-opts write-opts
645
(when name (setf (name obj) name))
648
(defmethod execute-transaction ((self rdb-database) txn &key)
649
(prog1 (commit-transaction txn)
650
(rocksdb-transaction-destroy txn)))
653
(defclass rdb-collection (database-collection)
654
((collection :initform (coerce nil db::*database-collection-type*))))