nimbus-eth1/nimbus/db/aristo/aristo_transaction.nim

421 lines
14 KiB
Nim

# nimbus-eth1
# Copyright (c) 2021 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed
# except according to those terms.
## Aristo DB -- Transaction interface
## ==================================
##
{.push raises: [].}
import
std/[sets, tables],
chronicles,
eth/common,
stew/results,
"."/[aristo_delete, aristo_desc, aristo_get, aristo_hashify,
aristo_hike, aristo_init, aristo_layer, aristo_merge, aristo_nearby]
logScope:
topics = "aristo-tx"
type
AristoTxRef* = ref object
## This descriptor replaces the `AristoDbRef` one for transaction based
## database operations and management.
parent: AristoTxRef ## Parent transaction (if any)
db: AristoDbRef ## Database access
# ------------------------------------------------------------------------------
# Public functions: Constructor/destructor
# ------------------------------------------------------------------------------
proc to*(
db: AristoDbRef; # `init()` result
T: type AristoTxRef; # Type discriminator
): T =
## Embed the database descritor `db` into the transaction based one. After
## this operation, the argument descriptor should not be used anymore.
##
## The function will return a new transaction descriptor unless the stack of
## the argument `db` is already filled (e.g. using `push()` on the `db`.)
if db.stack.len == 0:
return AristoTxRef(db: db)
proc to*(
rc: Result[AristoDbRef,AristoError]; # `init()` result
T: type AristoTxRef; # Type discriminator
): Result[T, AristoError] =
## Variant of `to()` which passes on any constructor errors.
##
## Example:
## ::
## let rc = AristoDbRef.init(BackendRocksDB,"/var/tmp/rdb").to(AristoTxRef)
## ...
## let tdb = rc.value
## ...
##
if rc.isErr:
return err(rc.error)
let tdb = rc.value.to(AristoTxRef)
if tdb.isNil:
return err(TxDbStackNonEmpty)
ok tdb
proc done*(
tdb: AristoTxRef; # Database, transaction wrapper
flush = false; # Delete persistent data (if supported)
): Result[void,AristoError]
{.discardable.} =
## Database and transaction handle destructor. The `flush` argument is passed
## on to the database backend destructor. When used in the `BackendRocksDB`
## database, a `true` value for `flush` will wipe the entire database from
## the hard disc.
##
## Note that the function argument `tdb` must not have any pending open
## transaction layers, i.e. `tdb.isBase()` must return `true`.
if not tdb.parent.isNil or tdb.db.isNil:
return err(TxBaseHandleExpected)
tdb.db.finish flush
ok()
# ------------------------------------------------------------------------------
# Public functions: Classifiers
# ------------------------------------------------------------------------------
proc isBase*(tdb: AristoTxRef): bool =
## The function returns `true` if the argument handle `tdb` is the one that
## was returned from the `to()` constructor. A handle where this function
## returns `true` is called a *base level* handle.
##
## A *base level* handle may be a valid argument for the `begin()` function
## but not for either `commit()` ot `rollback()`.
tdb.parent.isNil and not tdb.db.isNil
proc isTop*(tdb: AristoTxRef): bool =
## If the function returns `true` for the argument handle `tdb`, then this
## handle can be used on any of the following functions.
not tdb.parent.isNil and not tdb.db.isNil
# ------------------------------------------------------------------------------
# Public functions: Transaction frame
# ------------------------------------------------------------------------------
proc begin*(
tdb: AristoTxRef; # Database, transaction wrapper
): Result[AristoTxRef,(VertexID,AristoError)] =
## Starts a new transaction. If successful, the function will return a new
## handle (or descriptor) which replaces the argument handle `tdb`. This
## argument handle `tdb` is rendered invalid for as long as the new
## transaction handle is valid. While valid, this new handle is called a
## *top level* handle.
##
## If the argument `tdb` is a *base level* or a *top level* handle, this
## function succeeds. Otherwise it will return the error
## `TxValidHandleExpected`.
##
## Example:
## ::
## proc doSomething(tdb: AristoTxRef) =
## let tx = tdb.begin.value # will crash on failure
## defer: tx.rollback()
## ...
## tx.commit()
##
if tdb.db.isNil:
return err((VertexID(0),TxValidHandleExpected))
tdb.db.push()
let pTx = AristoTxRef(parent: tdb, db: tdb.db)
tdb.db = AristoDbRef(nil)
ok pTx
proc rollback*(
tdb: AristoTxRef; # Database, transaction wrapper
): Result[AristoTxRef,(VertexID,AristoError)]
{.discardable.} =
## Given a *top level* handle, this function discards all database operations
## performed through this handle and returns the previous one which becomes
## either the *top level* or the *base level* handle, again.
##
if tdb.db.isNil or tdb.parent.isNil:
return err((VertexID(0),TxTopHandleExpected))
block:
let rc = tdb.db.pop(merge = false)
if rc.isErr:
return err(rc.error)
let pTx = tdb.parent
pTx.db = tdb.db
tdb.parent = AristoTxRef(nil)
tdb.db = AristoDbRef(nil)
ok pTx
proc commit*(
tdb: AristoTxRef; # Database, transaction wrapper
hashify = false; # Always calc Merkle hashes if `true`
): Result[AristoTxRef,(VertexID,AristoError)]
{.discardable.} =
## Given a *top level* handle, this function accepts all database operations
## performed through this handle and merges it to the previous layer. It
## returns this previous layer which becomes either the *top level* or the
## *base level* handle, again.
##
## If the function return value is a *base level* handle, all the accumulated
## prevoius database operations will have been hashified and successfully
## stored on the persistent database.
##
## If the argument `hashify` is set `true`, the function will always hashify
## (i.e. calculate Merkle hashes) regardless of whether it is stored on the
## backend.
##
if tdb.db.isNil or tdb.parent.isNil:
return err((VertexID(0),TxTopHandleExpected))
block:
let rc = tdb.db.pop(merge = true)
if rc.isErr:
return err(rc.error)
let pTx = tdb.parent
pTx.db = tdb.db
# Hashify and save (if any)
if hashify or pTx.parent.isNil:
let rc = tdb.db.hashify()
if rc.isErr:
return err(rc.error)
if pTx.parent.isNil:
let rc = tdb.db.save()
if rc.isErr:
return err(rc.error)
tdb.db = AristoDbRef(nil)
tdb.parent = AristoTxRef(nil)
ok pTx
proc collapse*(
tdb: AristoTxRef; # Database, transaction wrapper
commit: bool; # Commit is `true`, otherwise roll back
): Result[AristoTxRef,(VertexID,AristoError)] =
## Variation of `commit()` or `rollback()` performing the equivalent of
## ::
## while tx.isTop:
## let rc =
## if commit: tx.commit()
## else: tx.rollback()
## ...
## tx = rc.value
##
if tdb.db.isNil or tdb.parent.isNil:
return err((VertexID(0),TxTopHandleExpected))
# Get base layer
var pTx = tdb.parent
while not pTx.parent.isNil:
pTx = pTx.parent
pTx.db = tdb.db
# Hashify and save, or complete rollback
if commit:
block:
let rc = tdb.db.hashify()
if rc.isErr:
return err(rc.error)
block:
let rc = tdb.db.save()
if rc.isErr:
return err(rc.error)
else:
let rc = tdb.db.retool(flushStack = true)
if rc.isErr:
return err((VertexID(0),rc.error))
tdb.db = AristoDbRef(nil)
tdb.parent = AristoTxRef(nil)
ok pTx
# ------------------------------------------------------------------------------
# Public functions: DB manipulations
# ------------------------------------------------------------------------------
proc put*(
tdb: AristoTxRef; # Database, transaction wrapper
leaf: LeafTiePayload; # Leaf item to add to the database
): Result[bool,AristoError] =
## Add leaf entry to transaction layer.
if tdb.db.isNil or tdb.parent.isNil:
return err(TxTopHandleExpected)
let report = tdb.db.merge @[leaf]
if report.error != AristoError(0):
return err(report.error)
ok(0 < report.merged)
proc del*(
tdb: AristoTxRef; # Database, transaction wrapper
leaf: LeafTie; # `Patricia Trie` path root-to-leaf
): Result[void,(VertexID,AristoError)] =
## Delete leaf entry from transaction layer.
if tdb.db.isNil or tdb.parent.isNil:
return err((VertexID(0),TxTopHandleExpected))
tdb.db.delete leaf
proc get*(
tdb: AristoTxRef; # Database, transaction wrapper
leaf: LeafTie; # `Patricia Trie` path root-to-leaf
): Result[PayloadRef,(VertexID,AristoError)] =
## Get leaf entry from database filtered through the transaction layer.
if tdb.db.isNil or tdb.parent.isNil:
return err((VertexID(0),TxTopHandleExpected))
let hike = leaf.hikeUp tdb.db
if hike.error != AristoError(0):
let vid = if hike.legs.len == 0: VertexID(0) else: hike.legs[^1].wp.vid
return err((vid,hike.error))
ok hike.legs[^1].wp.vtx.lData
proc key*(
tdb: AristoTxRef; # Database, transaction wrapper
vid: VertexID;
): Result[HashKey,(VertexID,AristoError)] =
## Get the Merkle hash key for the argument vertex ID `vid`. This function
## hashifies (i.e. calculates Merkle hashe keys) unless available on the
## requested vertex ID.
##
if tdb.db.isNil or tdb.parent.isNil:
return err((VertexID(0),TxTopHandleExpected))
if tdb.db.top.kMap.hasKey vid:
block:
let key = tdb.db.top.kMap.getOrVoid(vid).key
if key.isValid:
return ok(key)
let rc = tdb.db.hashify()
if rc.isErr:
return err(rc.error)
block:
let key = tdb.db.top.kMap.getOrVoid(vid).key
if key.isValid:
return ok(key)
return err((vid,TxCacheKeyFetchFail))
block:
let rc = tdb.db.getKeyBackend vid
if rc.isOk:
return ok(rc.value)
return err((vid,TxBeKeyFetchFail))
proc rootKey*(
tdb: AristoTxRef; # Database, transaction wrapper
): Result[HashKey,(VertexID,AristoError)] =
## Get the Merkle hash key for the main state root (with vertex ID `1`.)
tdb.key VertexID(1)
proc changeLog*(
tdb: AristoTxRef; # Database, transaction wrapper
clear = false; # Delete history
): seq[AristoChangeLogRef] =
## Get the save history, i.e. the changed states before the database was
## updated on disc. If the argument `chear` is set `true`, the history log
## on the descriptor is cleared.
##
## The argument `tdb` must be a *top level* descriptor, i.e. `tdb.isTop()`
## returns `true`. Otherwise the function `changeLog()` always returns an
## empty list.
##
if tdb.db.isNil or tdb.parent.isNil:
return
result = tdb.db.history
if clear:
tdb.db.history.setlen(0)
# ------------------------------------------------------------------------------
# Public functions: DB traversal
# ------------------------------------------------------------------------------
proc right*(
lty: LeafTie; # Some `Patricia Trie` path
tdb: AristoTxRef; # Database, transaction wrapper
): Result[LeafTie,(VertexID,AristoError)] =
## Finds the next leaf to the right (if any.) For details see
## `aristo_nearby.right()`.
if tdb.db.isNil or tdb.parent.isNil:
return err((VertexID(0),TxTopHandleExpected))
lty.right tdb.db
proc left*(
lty: LeafTie; # Some `Patricia Trie` path
tdb: AristoTxRef; # Database, transaction wrapper
): Result[LeafTie,(VertexID,AristoError)] =
## Finds the next leaf to the left (if any.) For details see
## `aristo_nearby.left()`.
if tdb.db.isNil or tdb.parent.isNil:
return err((VertexID(0),TxTopHandleExpected))
lty.left tdb.db
# ------------------------------------------------------------------------------
# Public helpers, miscellaneous
# ------------------------------------------------------------------------------
proc level*(
tdb: AristoTxRef; # Database, transaction wrapper
): (int,int) =
## This function returns the nesting level of the transaction and the length
## of the internal stack. Both values must be equal (otherwise there would
## be an internal error.)
##
## The argument `tdb` must be a *top level* or *base level* descriptor, i.e.
## `tdb.isTop() or tdb.isBase()` evaluate `true`. Otherwise `(-1,-1)` is
## returned.
##
if tdb.db.isNil:
return (-1,-1)
if tdb.parent.isNil:
return (0, tdb.db.stack.len)
# Count base layer
var
count = 1
pTx = tdb.parent
while not pTx.parent.isNil:
count.inc
pTx = pTx.parent
(count, tdb.db.stack.len)
proc db*(
tdb: AristoTxRef; # Database, transaction wrapper
): AristoDbRef =
## Getter, provides access to the Aristo database cache and backend.
##
## The getter directive returns a valid object reference if the argument
## `tdb` is a *top level* or *base level* descriptor, i.e.
## `tdb.isTop() or tdb.isBase()` evaluate `true`.
##
tdb.db
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------