diff --git a/config/extra/with-handholding.mk b/config/extra/with-handholding.mk index 170e0020bf..69ea87f6bb 100644 --- a/config/extra/with-handholding.mk +++ b/config/extra/with-handholding.mk @@ -5,4 +5,5 @@ CPPFLAGS+=-DFD_SCRATCH_USE_HANDHOLDING=1 CPPFLAGS+=-DFD_SPAD_USE_HANDHOLDING=1 CPPFLAGS+=-DFD_TOWER_USE_HANDHOLDING=1 CPPFLAGS+=-DFD_TXN_HANDHOLDING=1 +CPPFLAGS+=-DFD_FUNKIER_HANDHOLDING=1 CPPFLAGS+=-DFD_RUNTIME_ERR_HANDHOLDING=1 diff --git a/src/funkier/Local.mk b/src/funkier/Local.mk new file mode 100644 index 0000000000..7731c533cf --- /dev/null +++ b/src/funkier/Local.mk @@ -0,0 +1,12 @@ +$(call add-hdrs,fd_funkier_base.h fd_funkier_txn.h fd_funkier_rec.h fd_funkier_val.h fd_funkier_filemap.h fd_funkier.h) +$(call add-objs,fd_funkier_base fd_funkier_txn fd_funkier_rec fd_funkier_val fd_funkier_filemap fd_funkier,fd_funk) +$(call make-unit-test,test_funkier_base,test_funkier_base,fd_funk fd_util) +$(call make-unit-test,test_funkier,test_funkier,fd_funk fd_util) +$(call make-unit-test,test_funkier_concur,test_funkier_concur,fd_funk fd_util) +$(call make-unit-test,test_funkier_rec,test_funkier_rec test_funkier_common,fd_funk fd_util) +$(call make-unit-test,test_funkier_txn,test_funkier_txn test_funkier_common,fd_funk fd_util) +$(call make-unit-test,test_funkier_val,test_funkier_val test_funkier_common,fd_funk fd_util) +ifdef FD_HAS_HOSTED +$(call make-unit-test,test_funkier_txn2,test_funkier_txn2,fd_funk fd_util) +$(call make-unit-test,test_funkier_file,test_funkier_file,fd_funk fd_util) +endif diff --git a/src/funkier/fd_funkier.c b/src/funkier/fd_funkier.c new file mode 100644 index 0000000000..bd123504d0 --- /dev/null +++ b/src/funkier/fd_funkier.c @@ -0,0 +1,324 @@ +#include "fd_funkier.h" +#include + +ulong +fd_funkier_align( void ) { + return FD_FUNKIER_ALIGN; +} + +ulong +fd_funkier_footprint( ulong txn_max, + ulong rec_max ) { + + ulong l = FD_LAYOUT_INIT; + + l = FD_LAYOUT_APPEND( l, alignof(fd_funkier_t), sizeof(fd_funkier_t) ); + + ulong txn_chain_cnt = fd_funkier_txn_map_chain_cnt_est( txn_max ); + l = FD_LAYOUT_APPEND( l, fd_funkier_txn_map_align(), fd_funkier_txn_map_footprint( txn_chain_cnt ) ); + l = FD_LAYOUT_APPEND( l, fd_funkier_txn_pool_align(), fd_funkier_txn_pool_footprint() ); + l = FD_LAYOUT_APPEND( l, alignof(fd_funkier_txn_t), sizeof(fd_funkier_txn_t) * txn_max ); + + ulong rec_chain_cnt = fd_funkier_rec_map_chain_cnt_est( rec_max ); + l = FD_LAYOUT_APPEND( l, fd_funkier_rec_map_align(), fd_funkier_rec_map_footprint( rec_chain_cnt ) ); + l = FD_LAYOUT_APPEND( l, fd_funkier_rec_pool_align(), fd_funkier_rec_pool_footprint() ); + l = FD_LAYOUT_APPEND( l, alignof(fd_funkier_rec_t), sizeof(fd_funkier_rec_t) * rec_max ); + + l = FD_LAYOUT_APPEND( l, fd_alloc_align(), fd_alloc_footprint() ); + + return l; +} + +/* TODO: Consider letter user just passing a join of alloc to use, + inferring the backing wksp and cgroup_hint from that and then + allocating exclusively from that? */ + +void * +fd_funkier_new( void * shmem, + ulong wksp_tag, + ulong seed, + ulong txn_max, + ulong rec_max ) { + fd_funkier_t * funk = (fd_funkier_t *)shmem; + fd_wksp_t * wksp = fd_wksp_containing( funk ); + +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( !funk ) ) { + FD_LOG_WARNING(( "NULL funk" )); + return NULL; + } + + if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)funk, fd_funkier_align() ) ) ) { + FD_LOG_WARNING(( "misaligned funk" )); + return NULL; + } + + if( FD_UNLIKELY( !wksp_tag ) ) { + FD_LOG_WARNING(( "bad wksp_tag" )); + return NULL; + } + + if( FD_UNLIKELY( !wksp ) ) { + FD_LOG_WARNING(( "shmem must be part of a workspace" )); + return NULL; + } + + if( txn_max>FD_FUNKIER_TXN_IDX_NULL ) { /* See note in fd_funkier.h about this limit */ + FD_LOG_WARNING(( "txn_max too large for index compression" )); + return NULL; + } +#endif + + FD_SCRATCH_ALLOC_INIT( l, funk+1 ); + + ulong txn_chain_cnt = fd_funkier_txn_map_chain_cnt_est( txn_max ); + void * txn_map = FD_SCRATCH_ALLOC_APPEND( l, fd_funkier_txn_map_align(), fd_funkier_txn_map_footprint( txn_chain_cnt ) ); + void * txn_pool = FD_SCRATCH_ALLOC_APPEND( l, fd_funkier_txn_pool_align(), fd_funkier_txn_pool_footprint() ); + fd_funkier_txn_t * txn_ele = (fd_funkier_txn_t *)FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_funkier_txn_t), sizeof(fd_funkier_txn_t) * txn_max ); + + ulong rec_chain_cnt = fd_funkier_rec_map_chain_cnt_est( rec_max ); + void * rec_map = FD_SCRATCH_ALLOC_APPEND( l, fd_funkier_rec_map_align(), fd_funkier_rec_map_footprint( rec_chain_cnt ) ); + void * rec_pool = FD_SCRATCH_ALLOC_APPEND( l, fd_funkier_rec_pool_align(), fd_funkier_rec_pool_footprint() ); + fd_funkier_rec_t * rec_ele = (fd_funkier_rec_t *)FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_funkier_rec_t), sizeof(fd_funkier_rec_t) * rec_max ); + + void * alloc = FD_SCRATCH_ALLOC_APPEND( l, fd_alloc_align(), fd_alloc_footprint() ); + + FD_TEST( _l == (ulong)funk + fd_funkier_footprint( txn_max, rec_max ) ); + + fd_memset( funk, 0, sizeof(fd_funkier_t) ); + + funk->funk_gaddr = fd_wksp_gaddr_fast( wksp, funk ); + funk->wksp_tag = wksp_tag; + funk->seed = seed; + funk->cycle_tag = 3UL; /* various verify functions use tags 0-2 */ + + funk->txn_map_gaddr = fd_wksp_gaddr_fast( wksp, fd_funkier_txn_map_new( txn_map, txn_chain_cnt, seed ) ); + void * txn_pool2 = fd_funkier_txn_pool_new( txn_pool ); + funk->txn_pool_gaddr = fd_wksp_gaddr_fast( wksp, txn_pool2 ); + fd_funkier_txn_pool_t txn_join[1]; + fd_funkier_txn_pool_join( txn_join, txn_pool2, txn_ele, txn_max ); + fd_funkier_txn_pool_reset( txn_join, 0UL ); + funk->txn_ele_gaddr = fd_wksp_gaddr_fast( wksp, txn_ele ); + funk->txn_max = txn_max; + funk->child_head_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + funk->child_tail_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + + fd_funkier_txn_xid_set_root( funk->root ); + fd_funkier_txn_xid_set_root( funk->last_publish ); + + funk->rec_map_gaddr = fd_wksp_gaddr_fast( wksp, fd_funkier_rec_map_new( rec_map, rec_chain_cnt, seed ) ); + void * rec_pool2 = fd_funkier_rec_pool_new( rec_pool ); + funk->rec_pool_gaddr = fd_wksp_gaddr_fast( wksp, rec_pool2 ); + fd_funkier_rec_pool_t rec_join[1]; + fd_funkier_rec_pool_join( rec_join, rec_pool2, rec_ele, rec_max ); + fd_funkier_rec_pool_reset( rec_join, 0UL ); + funk->rec_ele_gaddr = fd_wksp_gaddr_fast( wksp, rec_ele ); + funk->rec_max = rec_max; + funk->rec_head_idx = FD_FUNKIER_REC_IDX_NULL; + funk->rec_tail_idx = FD_FUNKIER_REC_IDX_NULL; + + funk->alloc_gaddr = fd_wksp_gaddr_fast( wksp, fd_alloc_join( fd_alloc_new( alloc, wksp_tag ), 0UL ) ); + + FD_COMPILER_MFENCE(); + FD_VOLATILE( funk->magic ) = FD_FUNKIER_MAGIC; + FD_COMPILER_MFENCE(); + + return (void *)funk; +} + +fd_funkier_t * +fd_funkier_join( void * shfunk ) { + fd_funkier_t * funk = (fd_funkier_t *)shfunk; + +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( !funk ) ) { + FD_LOG_WARNING(( "NULL shfunk" )); + return NULL; + } + + if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)funk, fd_funkier_align() ) ) ) { + FD_LOG_WARNING(( "misaligned shfunk" )); + return NULL; + } + + fd_wksp_t * wksp = fd_wksp_containing( funk ); + if( FD_UNLIKELY( !wksp ) ) { + FD_LOG_WARNING(( "shfunk must be part of a workspace" )); + return NULL; + } + + if( FD_UNLIKELY( funk->magic!=FD_FUNKIER_MAGIC ) ) { + FD_LOG_WARNING(( "bad magic" )); + return NULL; + } +#endif + +#ifdef FD_FUNKIER_WKSP_PROTECT +#ifndef FD_FUNKIER_HANDHOLDING + fd_wksp_t * wksp = fd_wksp_containing( funk ); +#endif + fd_wksp_mprotect( wksp, 1 ); +#endif + + return funk; +} + +void * +fd_funkier_leave( fd_funkier_t * funk ) { + + if( FD_UNLIKELY( !funk ) ) { + FD_LOG_WARNING(( "NULL funk" )); + return NULL; + } + + return (void *)funk; +} + +void * +fd_funkier_delete( void * shfunk ) { + fd_funkier_t * funk = (fd_funkier_t *)shfunk; + +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( !funk ) ) { + FD_LOG_WARNING(( "NULL shfunk" )); + return NULL; + } + + if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)funk, fd_funkier_align() ) ) ) { + FD_LOG_WARNING(( "misaligned shfunk" )); + return NULL; + } + + fd_wksp_t * wksp = fd_wksp_containing( funk ); + if( FD_UNLIKELY( !wksp ) ) { + FD_LOG_WARNING(( "shfunk must be part of a workspace" )); + return NULL; + } + + if( FD_UNLIKELY( funk->magic!=FD_FUNKIER_MAGIC ) ) { + FD_LOG_WARNING(( "bad magic" )); + return NULL; + } +#endif + + FD_COMPILER_MFENCE(); + FD_VOLATILE( funk->magic ) = 0UL; + FD_COMPILER_MFENCE(); + + return funk; +} + +#ifdef FD_FUNKIER_HANDHOLDING +int +fd_funkier_verify( fd_funkier_t * funk ) { + +# define TEST(c) do { \ + if( FD_UNLIKELY( !(c) ) ) { FD_LOG_WARNING(( "FAIL: %s", #c )); return FD_FUNKIER_ERR_INVAL; } \ + } while(0) + + TEST( funk ); + + /* Test metadata */ + + TEST( funk->magic==FD_FUNKIER_MAGIC ); + + ulong funk_gaddr = funk->funk_gaddr; + TEST( funk_gaddr ); + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + TEST( wksp ); + TEST( fd_wksp_laddr_fast( wksp, funk_gaddr )==(void *)funk ); + TEST( fd_wksp_gaddr_fast( wksp, funk )==funk_gaddr ); + + ulong wksp_tag = fd_funkier_wksp_tag( funk ); + TEST( !!wksp_tag ); + + ulong seed = funk->seed; /* seed can be anything */ + + TEST( funk->cycle_tag>2UL ); + + /* Test transaction map */ + + ulong txn_max = funk->txn_max; + TEST( txn_max<=FD_FUNKIER_TXN_IDX_NULL ); + + ulong txn_map_gaddr = funk->txn_map_gaddr; + TEST( txn_map_gaddr ); + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( funk, wksp ); + ulong txn_chain_cnt = fd_funkier_txn_map_chain_cnt_est( txn_max ); + TEST( txn_chain_cnt==fd_funkier_txn_map_chain_cnt( &txn_map ) ); + TEST( seed==fd_funkier_txn_map_seed( &txn_map ) ); + + ulong child_head_idx = fd_funkier_txn_idx( funk->child_head_cidx ); + ulong child_tail_idx = fd_funkier_txn_idx( funk->child_tail_cidx ); + + int null_child_head = fd_funkier_txn_idx_is_null( child_head_idx ); + int null_child_tail = fd_funkier_txn_idx_is_null( child_tail_idx ); + + if( !txn_max ) TEST( null_child_head & null_child_tail ); + else { + if( null_child_head ) TEST( null_child_tail ); + else TEST( child_head_idxlast_publish; + TEST( last_publish ); /* Practically guaranteed */ + /* (*last_publish) only be root at creation and anything but root post + creation. But we don't know which situation applies here so this + could be anything. */ + + TEST( !fd_funkier_txn_verify( funk ) ); + + /* Test record map */ + + ulong rec_max = funk->rec_max; + TEST( rec_max<=FD_FUNKIER_TXN_IDX_NULL ); + + ulong rec_map_gaddr = funk->rec_map_gaddr; + TEST( rec_map_gaddr ); + fd_funkier_rec_map_t rec_map = fd_funkier_rec_map( funk, wksp ); + ulong rec_chain_cnt = fd_funkier_rec_map_chain_cnt_est( rec_max ); + TEST( rec_chain_cnt==fd_funkier_rec_map_chain_cnt( &rec_map ) ); + TEST( seed==fd_funkier_rec_map_seed( &rec_map ) ); + + ulong rec_head_idx = funk->rec_head_idx; + ulong rec_tail_idx = funk->rec_tail_idx; + + int null_rec_head = fd_funkier_rec_idx_is_null( rec_head_idx ); + int null_rec_tail = fd_funkier_rec_idx_is_null( rec_tail_idx ); + + if( !rec_max ) TEST( null_rec_head & null_rec_tail ); + else { + if( null_rec_head ) TEST( null_rec_tail ); + else TEST( rec_head_idxalloc_gaddr; + TEST( alloc_gaddr ); + fd_alloc_t * alloc = fd_funkier_alloc( funk, wksp ); + TEST( alloc ); + + TEST( !fd_funkier_val_verify( funk ) ); + +# undef TEST + + return FD_FUNKIER_SUCCESS; +} +#endif diff --git a/src/funkier/fd_funkier.h b/src/funkier/fd_funkier.h new file mode 100644 index 0000000000..37fe80ffb1 --- /dev/null +++ b/src/funkier/fd_funkier.h @@ -0,0 +1,488 @@ +#ifndef HEADER_fd_src_funk_fd_funkier_h +#define HEADER_fd_src_funk_fd_funkier_h + +/* Funk is a hybrid of a database and version control system designed + for ultra high performance blockchain applications. + + The data model is a flat table of records. A record is a xid/key-val + pair and records are fast O(1) indexable by their xid/key. xid is + short for "transaction id" and xids have a compile time fixed size + (e.g. 16 bytes). keys also have a compile time fixed size (e.g. + 40 bytes). Record values can vary in length from zero to a compile + time maximum size. The xid of all zeros is reserved for the "root" + transaction described below. Outside this, there are no + restrictions on what a record xid, key or val can be. Individual + records can be created, updated, and deleted arbitrarily. They are + just binary data as far as funk is concerned. + + The maximum number of records is practically only limited by the size + of the workspace memory backing it. At present, each record requires + 128 bytes of metadata (this includes records that are published and + records that are in the process of being updated). In other words, + about 13 GiB record metadata per hundred million records. The + maximum number of records that can be held by a funk instance is set + when that it was created (given the persistent and relocatable + properties described below though, it is straightforward to resize + this). + + The transaction model is richer than what is found in a regular + database. A transaction is a xid-"updates to parent transaction" + pair and transactions are fast O(1) indexable by xid. There is no + limitation on the number of updates in a transaction. Updates to the + record value are represented as the complete value record to make it + trivial to apply cryptographic operations like hashing to all updated + values in a transaction with file I/O, operating system calls, memory + data marshalling overhead, etc. + + Like records, the maximum number of transactions in preparation is + practically only limited by the size of the workspace memory backing + it. At present, a transaction requires 96 bytes of memory. As such, + it is practical to track a large number of forks during an extended + period of time of consensus failure in a block chain application + without using much workspace memory at all. The maximum number of + transactions that can be in preparation at any given time by a funk + instance is set when that it was created (as before, given the + persistent and relocatable properties described below, it is + straightforward to resize this). + + That is, a transaction is a compact representation of the entire + history of _all_ the database records up to that transaction. We can + trace a transaction's ancestors back to the "root" give the complete + history of all database records up to that transaction. The “root” + transaction is the ancestor of all transactions. The transaction + history is linear from the root transaction until the "last + published" transaction and cannot be modified. + + To start "preparing" a new transaction, we pick the new transaction's + xid (ideally unique among all transactions thus far) and fork off a + "parent" transaction. This operation virtually clones all database + records in the parent transaction, even if the parent itself has not + yet been "published". Given the above, the parent transaction can be + the last published transaction or another in-preparation transaction. + + Record inserts, reads, removes take place within the context + of a transaction, effectively isolating them to a private view of the + world. If a transaction is "cancelled", the changes to a record are + harmlessly discarded. Records in a transaction that has children + cannot be changed ("frozen"). + + As such, it is not possible to modify the records in transactions + strictly before the last published transaction. However, it is + possible to modify the records of the last published transaction if + there is no transactions in preparation. This is useful, for + example, loading up a transaction from a checkpointed state on + startup. A common idiom at start of a block though is to fork the + potential transaction of that block from its parent (freezing its + parent) and then fork a child of the potential transaction that will + hold updates to the block that are incrementally "merged" into the + potential transaction as block processing progresses. + + Critically, in-preparation transactions form a tree of dependent and + competing histories. This model matches blockchains, where + speculative work can proceed on several blocks at once long before + the blocks are finalized. When a transaction is published, all its + ancestors are also published, any competing histories are + cancelled, leaving only a linear history up to the published + transaction. There is no practical limitation on the complexity of + this tree. + + Under the hood, the database state is stored in NUMA and TLB + optimized shared memory (i.e. fd_wksp) such that various database + operations can be used concurrently by multiple threads distributed + arbitrarily over multiple processes zero copy. + + Database operations are at algorithmic minimums with reasonably high + performance implementations. Most are fast O(1) time and all are + small O(1) space (e.g. in complex transaction tree operations, there + is no use of dynamic allocation to hold temporaries and no use of + recursion to bound stack utilization at trivial levels). Further, + there are no explicit operating system calls and, given a well + optimized workspace (i.e. the wksp pages fit within a core's TLBs) no + implicit operating system calls. Critical operations (e.g. those + that actually might impact transaction history) are fortified against + memory corruption (e.g. robust against DoS attack by corrupting + transaction metadata to create loops in transaction trees or going + out of bounds in memory). Outside of record values, all memory used + is preallocated. And record values are O(1) lockfree concurrent + allocated via fd_alloc using the same wksp as funk (the + implementation is structured in layers that are straightforward to + retarget for particular applications as might be necessary). + + The shared memory used by a funk instance is within a workspace such + that it is also persistent and remotely inspectable. For example, a + process attached to a funk instance can be terminated and a new + process can resume exactly where the original process left off + instantly (e.g. no file I/O). Or a real-time monitor could be + visualizing the ongoing activity in a database non-invasively (e.g. + forks in flight, records updated by forks, etc). Or an auxiliary + process could be lazily and non-invasively writing all published + records to permanent storage in the background in parallel with + on-going operations. + + The records are further stored in the workspace memory relocatably. + For example, workspace memory could just be committed to a persistent + memory as is (or backed by NVMe or such directly), copied to a + different host, and processes on the new host could resume (indeed, + though it wouldn't be space efficient, the shared memory region is + usable as is as an on-disk checkpoint file). Or the workspace could + be resized and what not to handle large needs than when the database + was initially created and it all "just works". + + Limited concurrent (multithreaded) access is supported. As a + general rule, transaction level operations + (e.g. fd_funkier_txn_cancel and fd_funkier_txn_publish) have to be + single-threaded. In this case, no other access is allowed at the + same time. Purely record level operations are thread safe and can + be arbitrarily interleaved across multiple cpus. Specifically, + these are: + fd_funkier_rec_query_try + fd_funkier_rec_query_test + fd_funkier_rec_query_try_global + fd_funkier_rec_prepare + fd_funkier_rec_publish + fd_funkier_rec_cancel + fd_funkier_rec_remove +*/ + +//#include "fd_funkier_base.h" /* Includes ../util/fd_util.h */ +//#include "fd_funkier_txn.h" /* Includes fd_funkier_base.h */ +//#include "fd_funkier_rec.h" /* Includes fd_funkier_txn.h */ +#include "fd_funkier_val.h" /* Includes fd_funkier_rec.h */ + +/* FD_FUNKIER_ALIGN describe the alignment needed + for a funk. ALIGN should be a positive integer power of 2. + The footprint is dynamic depending on map sizes. */ + +#define FD_FUNKIER_ALIGN (4096UL) + +/* The details of a fd_funkier_private are exposed here to facilitate + inlining various operations. */ + +#define FD_FUNKIER_MAGIC (0xf17eda2ce7fc2c02UL) /* firedancer funk version 2 */ + +struct __attribute__((aligned(FD_FUNKIER_ALIGN))) fd_funkier_private { + + /* Metadata */ + + ulong magic; /* ==FD_FUNKIER_MAGIC */ + ulong funk_gaddr; /* wksp gaddr of this in the backing wksp, non-zero gaddr */ + ulong wksp_tag; /* Tag to use for wksp allocations, positive */ + ulong seed; /* Seed for various hashing function used under the hood, arbitrary */ + ulong cycle_tag; /* Next cycle_tag to use, used internally for various data integrity checks */ + + /* The funk transaction map stores the details about transactions + in preparation and their relationships to each other. This is a + fd_map_para/fd_pool_para and more details are given in fd_funkier_txn.h + + txn_max is the maximum number of transactions that can be in + preparation. Due to the use of compressed map indices to reduce + workspace memory footprint required, txn_max is at most + FD_FUNKIER_TXN_IDX_NULL (currently ~4B). This should be more than + ample for anticipated uses cases ... e.g. every single validator in + a pool of tens of thousands Solana validator had its own fork and + with no consensus ever being achieved, a funk with txn_max at the + limits of a compressed index will be chug along for days to weeks + before running out of indexing space. But if ever needing to + support more, it is straightforward to change the code to not use + index compression. Then, a funk (with a planet sized workspace + backing it) would survive a similar scenario for millions of years. + Presumably, if such a situation arose, in the weeks to eons while + there was consensus, somebody would notice and care enough to + intervene (if not it is probably irrelevant to the real world + anyway). + + txn_map_gaddr is the wksp gaddr of the fd_funkier_txn_map_t used by + this funk. + + child_{head,tail}_cidx are compressed txn map indices. After + decompression, they give the txn map index of the {oldest,youngest} + child of funk (i.e. an in-preparation transaction whose parent + transaction id is last_publish). FD_FUNKIER_TXN_IDX_NULL indicates + the funk is childless. Thus, if head/tail is FD_FUNKIER_TXN_IDX_NULL, + tail/head will be too. funk is "frozen" if it has children. + + last_publish is the ID of the last published transaction. It will + be the root transaction if no transactions have been published. + Will be the root transaction immediately after construction. */ + + ulong txn_max; /* In [0,FD_FUNKIER_TXN_IDX_NULL] */ + ulong txn_map_gaddr; /* Non-zero wksp gaddr with tag wksp_tag + seed ==fd_funkier_txn_map_seed (txn_map) + txn_max==fd_funkier_txn_map_key_max(txn_map) */ + ulong txn_pool_gaddr; + ulong txn_ele_gaddr; + + uint child_head_cidx; /* After decompression, in [0,txn_max) or FD_FUNKIER_TXN_IDX_NULL, FD_FUNKIER_TXN_IDX_NULL if txn_max 0 */ + uint child_tail_cidx; /* " */ + + /* Padding to FD_FUNKIER_TXN_XID_ALIGN here */ + + fd_funkier_txn_xid_t root[1]; /* Always equal to the root transaction */ + fd_funkier_txn_xid_t last_publish[1]; /* Root transaction immediately after construction, not root thereafter */ + + /* The funk record map stores the details about all the records in + the funk, including all those in the last published transaction and + all those getting updated in an in-preparation translation. This + is a fd_map_para/fd_pool_para and more details are given in fd_funkier_rec.h + + rec_max is the maximum number of records that can exist in this + funk. + + rec_map_gaddr is the wksp gaddr of the fd_funkier_rec_map_t used by + this funk. */ + + ulong rec_max; + ulong rec_map_gaddr; /* Non-zero wksp gaddr with tag wksp_tag + seed ==fd_funkier_rec_map_seed (rec_map) + rec_max==fd_funkier_rec_map_key_max(rec_map) */ + ulong rec_pool_gaddr; + ulong rec_ele_gaddr; + ulong rec_head_idx; /* Record map index of the first record, FD_FUNKIER_REC_IDX_NULL if none (from oldest to youngest) */ + ulong rec_tail_idx; /* " last " */ + + /* The funk alloc is used for allocating wksp resources for record + values. This is a fd_alloc and more details are given in + fd_funkier_val.h. Allocations from this allocator will be tagged with + wksp_tag and operations on this allocator will use concurrency + group 0. + + TODO: Consider letting user just pass a join of alloc (and maybe + the cgroup_idx to give the funk), inferring the wksp, cgroup from + that and allocating exclusively from that? */ + + ulong alloc_gaddr; /* Non-zero wksp gaddr with tag wksp tag */ + + /* Padding to FD_FUNKIER_ALIGN here */ +}; + +FD_PROTOTYPES_BEGIN + +/* Constructors */ + +/* fd_funkier_align return FD_FUNKIER_ALIGN. */ + +FD_FN_CONST ulong +fd_funkier_align( void ); + +/* fd_funkier_footprint returns the size need for funk and all + auxiliary data structures. Note that only record valus are + allocated dynamically. */ + +FD_FN_CONST ulong +fd_funkier_footprint( ulong txn_max, + ulong rec_max ); + +/* fd_wksp_new formats an unused wksp allocation with the appropriate + alignment and footprint as a funk. Caller is not joined on return. + Returns shmem on success and NULL on failure (shmem NULL, shmem + misaligned, zero wksp_tag, shmem is not backed by a wksp ... logs + details). A workspace can be used by multiple funks concurrently. + They will dynamically share the underlying workspace (along with any + other non-funk usage) but will otherwise act as completely separate + non-conflicting funks. To help with various diagnostics, garbage + collection and what not, all allocations to the underlying wksp are + tagged with the given tag (positive). Ideally, the tag used here + should be distinct from all other tags used by this workspace but + this is not required. */ + +void * +fd_funkier_new( void * shmem, + ulong wksp_tag, + ulong seed, + ulong txn_max, + ulong rec_max ); + +/* fd_funkier_join joins the caller to a funk instance. shfunk points to + the first byte of the memory region backing the funk in the caller's + address space. Returns an opaque handle of the join on success + (IMPORTANT! DO NOT ASSUME THIS IS A CAST OF SHFUNK) and NULL on + failure (NULL shfunk, misaligned shfunk, shfunk is not backed by a + wksp, bad magic, ... logs details). Every successful join should + have a matching leave. The lifetime of the join is until the + matching leave or the thread group is terminated (joins are local to + a thread group). */ + +fd_funkier_t * +fd_funkier_join( void * shfunk ); + +/* fd_funkier_leave leaves an existing join. Returns the underlying + shfunk (IMPORTANT! DO NOT ASSUME THIS IS A CAST OF FUNK) on success + and NULL on failure. Reasons for failure include funk is NULL (logs + details). */ + +void * +fd_funkier_leave( fd_funkier_t * funk ); + +/* fd_funkier_delete unformats a wksp allocation used as a funk + (additionally frees all wksp allocations used by that funk). Assumes + nobody is or will be joined to the funk. Returns shmem on success + and NULL on failure (logs details). Reasons for failure include + shfunk is NULL, misaligned shfunk, shfunk is not backed by a + workspace, etc. */ + +void * +fd_funkier_delete( void * shfunk ); + +/* Accessors */ + +/* fd_funkier_wksp returns the local join to the wksp backing the funk. + The lifetime of the returned pointer is at least as long as the + lifetime of the local join. Assumes funk is a current local join. */ + +FD_FN_PURE static inline fd_wksp_t * fd_funkier_wksp( fd_funkier_t * funk ) { return (fd_wksp_t *)(((ulong)funk) - funk->funk_gaddr); } + +/* fd_funkier_wksp_tag returns the workspace allocation tag used by the + funk for its wksp allocations. Will be positive. Assumes funk is a + current local join. */ + +FD_FN_PURE static inline ulong fd_funkier_wksp_tag( fd_funkier_t * funk ) { return funk->wksp_tag; } + +/* fd_funkier_seed returns the hash seed used by the funk for various hash + functions. Arbitrary value. Assumes funk is a current local join. + TODO: consider renaming hash_seed? */ + +FD_FN_PURE static inline ulong fd_funkier_seed( fd_funkier_t * funk ) { return funk->seed; } + +/* fd_funkier_txn_max returns maximum number of in-preparations the funk + can support. Assumes funk is a current local join. Return in + [0,FD_FUNKIER_TXN_IDX_NULL]. */ + +FD_FN_PURE static inline ulong fd_funkier_txn_max( fd_funkier_t * funk ) { return funk->txn_max; } + +/* fd_funkier_txn_map returns the funk's transaction map join. This + join can copied by value and is generally stored as a stack variable. */ + +FD_FN_PURE static inline fd_funkier_txn_map_t +fd_funkier_txn_map( fd_funkier_t * funk, /* Assumes current local join */ + fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */ + fd_funkier_txn_map_t join; + fd_funkier_txn_map_join( + &join, + fd_wksp_laddr_fast( wksp, funk->txn_map_gaddr ), + fd_wksp_laddr_fast( wksp, funk->txn_ele_gaddr ), + funk->txn_max ); + return join; +} + +/* fd_funkier_txn_pool returns the funk's transaction pool join. This + join can copied by value and is generally stored as a stack variable. */ + +FD_FN_PURE static inline fd_funkier_txn_pool_t +fd_funkier_txn_pool( fd_funkier_t * funk, /* Assumes current local join */ + fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */ + fd_funkier_txn_pool_t join; + fd_funkier_txn_pool_join( + &join, + fd_wksp_laddr_fast( wksp, funk->txn_pool_gaddr ), + fd_wksp_laddr_fast( wksp, funk->txn_ele_gaddr ), + funk->txn_max ); + return join; +} + +/* fd_funkier_last_publish_child_{head,tail} returns a pointer in the + caller's address space to {oldest,young} transaction child of root, NULL if + funk is childless. All pointers are in the caller's address space. + These are all a fast O(1) but not fortified against memory data + corruption. */ + +FD_FN_PURE static inline fd_funkier_txn_t * +fd_funkier_last_publish_child_head( fd_funkier_t * funk, + fd_funkier_txn_pool_t * pool ) { + ulong idx = fd_funkier_txn_idx( funk->child_head_cidx ); + if( fd_funkier_txn_idx_is_null( idx ) ) return NULL; /* TODO: Consider branchless? */ + return pool->ele + idx; +} + +FD_FN_PURE static inline fd_funkier_txn_t * +fd_funkier_last_publish_child_tail( fd_funkier_t * funk, + fd_funkier_txn_pool_t * pool ) { + ulong idx = fd_funkier_txn_idx( funk->child_tail_cidx ); + if( fd_funkier_txn_idx_is_null( idx ) ) return NULL; /* TODO: Consider branchless? */ + return pool->ele + idx; +} + +/* fd_funkier_root returns a pointer in the caller's address space to the + transaction id of the root transaction. Assumes funk is a current + local join. Lifetime of the returned pointer is the lifetime of the + current local join. The value at this pointer will always be the + root transaction id. */ + +FD_FN_CONST static inline fd_funkier_txn_xid_t const * fd_funkier_root( fd_funkier_t * funk ) { return funk->root; } + +/* fd_funkier_last_publish returns a pointer in the caller's address space + to transaction id of the last published transaction. Assumes funk is + a current local join. Lifetime of the returned pointer is the + lifetime of the current local join. The value at this pointer will + be constant until the next transaction is published. */ + +FD_FN_CONST static inline fd_funkier_txn_xid_t const * fd_funkier_last_publish( fd_funkier_t * funk ) { return funk->last_publish; } + +/* fd_funkier_is_frozen returns 1 if the records of the last published + transaction are frozen (i.e. the funk has children) and 0 otherwise + (i.e. the funk is childless). Assumes funk is a current local join. */ + +FD_FN_PURE static inline int +fd_funkier_last_publish_is_frozen( fd_funkier_t const * funk ) { + return fd_funkier_txn_idx( funk->child_head_cidx )!=FD_FUNKIER_TXN_IDX_NULL; +} + +/* fd_funkier_rec_max returns maximum number of records that can be held + in the funk. This includes both records of the last published + transaction and records for transactions that are in-flight. */ + +FD_FN_PURE static inline ulong fd_funkier_rec_max( fd_funkier_t * funk ) { return funk->rec_max; } + +/* fd_funkier_rec_map returns the funk's record map join. This + join can copied by value and is generally stored as a stack variable. */ + +FD_FN_PURE static inline fd_funkier_rec_map_t +fd_funkier_rec_map( fd_funkier_t * funk, /* Assumes current local join */ + fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */ + fd_funkier_rec_map_t join; + fd_funkier_rec_map_join( + &join, + fd_wksp_laddr_fast( wksp, funk->rec_map_gaddr ), + fd_wksp_laddr_fast( wksp, funk->rec_ele_gaddr ), + funk->rec_max ); + return join; +} + +/* fd_funkier_rec_pool returns the funk's record pool join. This + join can copied by value and is generally stored as a stack variable. */ + +FD_FN_PURE static inline fd_funkier_rec_pool_t +fd_funkier_rec_pool( fd_funkier_t * funk, /* Assumes current local join */ + fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */ + fd_funkier_rec_pool_t join; + fd_funkier_rec_pool_join( + &join, + fd_wksp_laddr_fast( wksp, funk->rec_pool_gaddr ), + fd_wksp_laddr_fast( wksp, funk->rec_ele_gaddr ), + funk->rec_max ); + return join; +} + +/* fd_funkier_alloc returns a pointer in the caller's address space to + the funk's allocator. */ + +FD_FN_PURE static inline fd_alloc_t * /* Lifetime is that of the local join */ +fd_funkier_alloc( fd_funkier_t * funk, /* Assumes current local join */ + fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */ + return fd_alloc_join_cgroup_hint_set( (fd_alloc_t *)fd_wksp_laddr_fast( wksp, funk->alloc_gaddr ), fd_tile_idx() ); +} + +/* Misc */ + +#ifdef FD_FUNKIER_HANDHOLDING +/* fd_funkier_verify verifies the integrity of funk. Returns + FD_FUNKIER_SUCCESS if funk appears to be intact and FD_FUNKIER_ERR_INVAL + otherwise (logs details). Assumes funk is a current local join (NULL + returns FD_FUNKIER_ERR_INVAL and logs details.) */ + +int +fd_funkier_verify( fd_funkier_t * funk ); +#endif + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_funk_fd_funkier_h */ diff --git a/src/funkier/fd_funkier_base.c b/src/funkier/fd_funkier_base.c new file mode 100644 index 0000000000..0392711c9a --- /dev/null +++ b/src/funkier/fd_funkier_base.c @@ -0,0 +1,18 @@ +#include "fd_funkier_base.h" + +char const * +fd_funkier_strerror( int err ) { + switch( err ) { + case FD_FUNKIER_SUCCESS: return "success"; + case FD_FUNKIER_ERR_INVAL: return "inval"; + case FD_FUNKIER_ERR_XID: return "xid"; + case FD_FUNKIER_ERR_KEY: return "key"; + case FD_FUNKIER_ERR_FROZEN: return "frozen"; + case FD_FUNKIER_ERR_TXN: return "txn"; + case FD_FUNKIER_ERR_REC: return "rec"; + case FD_FUNKIER_ERR_MEM: return "mem"; + case FD_FUNKIER_ERR_SYS: return "sys"; + default: break; + } + return "unknown"; +} diff --git a/src/funkier/fd_funkier_base.h b/src/funkier/fd_funkier_base.h new file mode 100644 index 0000000000..b0dc507a63 --- /dev/null +++ b/src/funkier/fd_funkier_base.h @@ -0,0 +1,307 @@ +#ifndef HEADER_fd_src_funk_fd_funkier_base_h +#define HEADER_fd_src_funk_fd_funkier_base_h + +/* Funk terminology / concepts: + + - A funk instance stores records. + + - A record is a key-value pair. + + - keys are a fixed length fd_funkier_rec_key_t. + + - values are variable size arbitrary binary data with an upper bound + to the size. + + - Records are indexed by key. + + - A funk transaction describes changes to the funk records. + + - A transactions has a globally unique identifier and a parent + transaction. + + - Transactions with children cannot be modified. + + - The chain of transactions through a transaction's ancestors + (its parent, grandparent, great-grandparent, ...) provides a + history of the funk all the way back the "root" transaction. + + - A transaction can be either in preparation or published. + + - The ancestors of a published transaction cannot be modified. + + - In preparation transactions can be cancelled. + + - Cancelling a transaction will discard all funk record updates for + that transaction and any descendant transactions. + + - Published transactions cannot be cancelled. + + - Critically, competing/parallel transaction histories are allowed. + + - A user can update all funk records for the most recently + published transactions (if it is not frozen) or all transactions + in preparation (if they are not frozen). */ + +#include "../util/fd_util.h" +#include "../util/valloc/fd_valloc.h" + +/* FD_FUNKIER_SUCCESS is used by various APIs to indicate the operation + successfully completed. This will be 0. FD_FUNKIER_ERR_* gives a + number of error codes used by fd_funk APIs. These will be negative + integers. */ + +#define FD_FUNKIER_SUCCESS (0) /* Success */ +#define FD_FUNKIER_ERR_INVAL (-1) /* Failed due to obviously invalid inputs */ +#define FD_FUNKIER_ERR_XID (-2) /* Failed due to transaction id issue (e.g. xid present/absent when it should be absent/present) */ +#define FD_FUNKIER_ERR_KEY (-3) /* Failed due to record key issue (e.g. key present/absent when it should be absent/present) */ +#define FD_FUNKIER_ERR_FROZEN (-4) /* Failed due to frozen issue (e.g. attempt to change records in a frozen transaction) */ +#define FD_FUNKIER_ERR_TXN (-5) /* Failed due to transaction map issue (e.g. funk txn_max too small) */ +#define FD_FUNKIER_ERR_REC (-6) /* Failed due to record map issue (e.g. funk rec_max too small) */ +#define FD_FUNKIER_ERR_MEM (-7) /* Failed due to wksp issue (e.g. wksp too small) */ +#define FD_FUNKIER_ERR_SYS (-8) /* Failed system call (e.g. a file write) */ + +/* FD_FUNKIER_REC_KEY_{ALIGN,FOOTPRINT} describe the alignment and + footprint of a fd_funkier_rec_key_t. ALIGN is a positive integer power + of 2. FOOTPRINT is a multiple of ALIGN. These are provided to + facilitate compile time declarations. */ + +#define FD_FUNKIER_REC_KEY_ALIGN (8UL) +#define FD_FUNKIER_REC_KEY_FOOTPRINT (40UL) /* 32 byte hash + 8 byte meta */ + +/* A fd_funkier_rec_key_t identifies a funk record. Compact binary keys + are encouraged but a cstr can be used so long as it has + strlen(cstr)ul[0] ) ^ fd_ulong_hash( seed ^ (1UL<<1) ^ k->ul[1] ) ) ^ + (fd_ulong_hash( seed ^ (1UL<<2) ^ k->ul[2] ) ^ fd_ulong_hash( seed ^ (1UL<<3) ^ k->ul[3] ) ) ^ + (fd_ulong_hash( seed ^ (1UL<<4) ^ k->ul[4] ) ); /* tons of ILP */ +} + +/* fd_funkier_rec_key_eq returns 1 if keys pointed to by ka and kb are + equal and 0 otherwise. Assumes ka and kb are in the caller's address + space and valid. */ + +FD_FN_UNUSED FD_FN_PURE static int /* Workaround -Winline */ +fd_funkier_rec_key_eq( fd_funkier_rec_key_t const * ka, + fd_funkier_rec_key_t const * kb ) { + ulong const * a = ka->ul; + ulong const * b = kb->ul; + return !( ((a[0]^b[0]) | (a[1]^b[1])) | ((a[2]^b[2]) | (a[3]^b[3])) | (a[4]^b[4]) ) ; +} + +/* fd_funkier_rec_key_copy copies the key pointed to by ks into the key + pointed to by kd and returns kd. Assumes kd and ks are in the + caller's address space and valid. */ + +static inline fd_funkier_rec_key_t * +fd_funkier_rec_key_copy( fd_funkier_rec_key_t * kd, + fd_funkier_rec_key_t const * ks ) { + ulong * d = kd->ul; + ulong const * s = ks->ul; + d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3]; d[4] = s[4]; + return kd; +} + +/* fd_funkier_txn_xid_hash provides a family of hashes that hash the xid + pointed to by x to a uniform quasi-random 64-bit integer. seed + selects the particular hash function to use and can be an arbitrary + 64-bit value. Returns the hash. The hash functions are high quality + but not cryptographically secure. Assumes x is in the caller's + address space and valid. */ + +FD_FN_UNUSED FD_FN_PURE static ulong /* Work around -Winline */ +fd_funkier_txn_xid_hash( fd_funkier_txn_xid_t const * x, + ulong seed ) { + return ( fd_ulong_hash( seed ^ (1UL<<0) ^ x->ul[0] ) ^ fd_ulong_hash( seed ^ (1UL<<1) ^ x->ul[1] ) ); /* tons of ILP */ +} + +/* fd_funkier_txn_xid_eq returns 1 if transaction id pointed to by xa and + xb are equal and 0 otherwise. Assumes xa and xb are in the caller's + address space and valid. */ + +FD_FN_PURE static inline int +fd_funkier_txn_xid_eq( fd_funkier_txn_xid_t const * xa, + fd_funkier_txn_xid_t const * xb ) { + ulong const * a = xa->ul; + ulong const * b = xb->ul; + return !( (a[0]^b[0]) | (a[1]^b[1]) ); +} + +/* fd_funkier_txn_xid_copy copies the transaction id pointed to by xs into + the transaction id pointed to by xd and returns xd. Assumes xd and + xs are in the caller's address space and valid. */ + +static inline fd_funkier_txn_xid_t * +fd_funkier_txn_xid_copy( fd_funkier_txn_xid_t * xd, + fd_funkier_txn_xid_t const * xs ) { + ulong * d = xd->ul; + ulong const * s = xs->ul; + d[0] = s[0]; d[1] = s[1]; + return xd; +} + +/* fd_funkier_txn_xid_eq_root returns 1 if transaction id pointed to by x + is the root transaction. Assumes x is in the caller's address space + and valid. */ + +FD_FN_PURE static inline int +fd_funkier_txn_xid_eq_root( fd_funkier_txn_xid_t const * x ) { + ulong const * a = x->ul; + return !(a[0] | a[1]); +} + +/* fd_funkier_txn_xid_set_root sets transaction id pointed to by x to the + root transaction and returns x. Assumes x is in the caller's address + space and valid. */ + +static inline fd_funkier_txn_xid_t * +fd_funkier_txn_xid_set_root( fd_funkier_txn_xid_t * x ) { + ulong * a = x->ul; + a[0] = 0UL; a[1] = 0UL; + return x; +} + +/* fd_funkier_xid_key_pair_hash produces a 64-bit hash case for a + xid_key_pair. Assumes p is in the caller's address space and valid. */ + +FD_FN_PURE static inline ulong +fd_funkier_xid_key_pair_hash( fd_funkier_xid_key_pair_t const * p, + ulong seed ) { + /* We ignore the xid part of the key because we need all the instances + of a given record key to appear in the same hash + chain. fd_funkier_rec_query_global depends on this. */ + return fd_funkier_rec_key_hash( p->key, seed ); +} + +/* fd_funkier_xid_key_pair_eq returns 1 if (xid,key) pair pointed to by pa + and pb are equal and 0 otherwise. Assumes pa and pb are in the + caller's address space and valid. */ + +FD_FN_UNUSED FD_FN_PURE static int /* Work around -Winline */ +fd_funkier_xid_key_pair_eq( fd_funkier_xid_key_pair_t const * pa, + fd_funkier_xid_key_pair_t const * pb ) { + return fd_funkier_txn_xid_eq( pa->xid, pb->xid ) & fd_funkier_rec_key_eq( pa->key, pb->key ); +} + +/* fd_funkier_xid_key_pair_copy copies the (xid,key) pair pointed to by ps + into the (xid,key) pair to by pd and returns pd. Assumes pd and ps + are in the caller's address space and valid. */ + +static inline fd_funkier_xid_key_pair_t * +fd_funkier_xid_key_pair_copy( fd_funkier_xid_key_pair_t * pd, + fd_funkier_xid_key_pair_t const * ps ) { + fd_funkier_txn_xid_copy( pd->xid, ps->xid ); + fd_funkier_rec_key_copy( pd->key, ps->key ); + return pd; +} + +/* fd_funkier_xid_key_pair_init set the (xid,key) pair p to pair formed + from the transaction id pointed to by x and the record key pointed to + by k. Assumes p, x and k are in the caller's address space and + valid. */ + +static inline fd_funkier_xid_key_pair_t * +fd_funkier_xid_key_pair_init( fd_funkier_xid_key_pair_t * p, + fd_funkier_txn_xid_t const * x, + fd_funkier_rec_key_t const * k ) { + fd_funkier_txn_xid_copy( p->xid, x ); + fd_funkier_rec_key_copy( p->key, k ); + return p; +} + +/* fd_funkier_strerror converts an FD_FUNKIER_SUCCESS / FD_FUNKIER_ERR_* code + into a human readable cstr. The lifetime of the returned pointer is + infinite. The returned pointer is always to a non-NULL cstr. */ + +FD_FN_CONST char const * +fd_funkier_strerror( int err ); + +/* TODO: Consider renaming transaction/txn to update (too much typing)? + upd (probably too similar to UDP) node? block? blk? state? commit? + ... to reduce naming collisions with terminology in use elsewhere? + + TODO: Consider fine tuning {REC,TXN}_{ALIGN,FOOTPRINT} to balance + application use cases with in memory packing with AVX / CPU cache + friendly accelerability. Likewise, virtually everything in here can + be AVX accelerated if desired. E.g. 8 uint hash in parallel then an + 8 wide xor lane reduction tree in hash? + + TODO: Consider letting the user provide alternatives for record and + transaction hashes at compile time (e.g. ids in blockchain apps are + often already crypto secure hashes in which case x->ul[0] ^ seed is + just as good theoretically and faster practically). */ + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_funk_fd_funkier_base_h */ diff --git a/src/funkier/fd_funkier_filemap.c b/src/funkier/fd_funkier_filemap.c new file mode 100644 index 0000000000..674406185b --- /dev/null +++ b/src/funkier/fd_funkier_filemap.c @@ -0,0 +1,447 @@ +#define _GNU_SOURCE +#define _FILE_OFFSET_BITS 64 +#include "fd_funkier_filemap.h" +#include +#include +#include +#include +#include +#include + +#define PAGESIZE (1UL<<12) /* 4 KiB */ + +fd_funkier_t * +fd_funkier_open_file( const char * filename, + ulong wksp_tag, + ulong seed, + ulong txn_max, + ulong rec_max, + ulong total_sz, + fd_funkier_file_mode_t mode, + fd_funkier_close_file_args_t * close_args_out ) { + + /* See if we already have the file open */ + + if( mode == FD_FUNKIER_READONLY || mode == FD_FUNKIER_READ_WRITE ) { + fd_shmem_join_info_t info; + if( !fd_shmem_join_query_by_name("funk", &info) ) { + void * shmem = info.join; + fd_wksp_t * wksp = fd_wksp_join( shmem ); + if( FD_UNLIKELY( !wksp ) ) { + FD_LOG_WARNING(( "fd_wksp_join(%p) failed", shmem )); + return NULL; + } + + fd_wksp_tag_query_info_t info2; + if( FD_UNLIKELY( !fd_wksp_tag_query( wksp, &wksp_tag, 1, &info2, 1 ) ) ) { + FD_LOG_WARNING(( "%s does not contain a funky", filename )); + return NULL; + } + + void * funk_shmem = fd_wksp_laddr_fast( wksp, info2.gaddr_lo ); + fd_funkier_t * funk = fd_funkier_join( funk_shmem ); + if( FD_UNLIKELY( funk == NULL ) ) { + FD_LOG_WARNING(( "failed to join a funky" )); + return NULL; + } + + if( FD_UNLIKELY( close_args_out != NULL ) ) { + close_args_out->shmem = shmem; + close_args_out->fd = -1; + close_args_out->total_sz = 0; + } + return funk; + } + } + + /* Open the file */ + + int open_flags, can_resize, can_create, do_new; + switch (mode) { + case FD_FUNKIER_READONLY: + if( filename == NULL || filename[0] == '\0' ) { + FD_LOG_WARNING(( "mode FD_FUNKIER_READONLY can not be used with an anonymous workspace, funk file required" )); + return NULL; + } + open_flags = O_RDWR; /* We mark the memory as read-only after we are done setting up */ + can_create = 0; + can_resize = 0; + do_new = 0; + break; + case FD_FUNKIER_READ_WRITE: + if( filename == NULL || filename[0] == '\0' ) { + FD_LOG_WARNING(( "mode FD_FUNKIER_READ_WRITE can not be used with an anonymous workspace, funk file required" )); + return NULL; + } + open_flags = O_RDWR; + can_create = 0; + can_resize = 0; + do_new = 0; + break; + case FD_FUNKIER_CREATE: + open_flags = O_CREAT|O_RDWR; + can_create = 1; + can_resize = 0; + do_new = 0; + break; + case FD_FUNKIER_OVERWRITE: + open_flags = O_CREAT|O_RDWR; + can_create = 1; + can_resize = 1; + do_new = 1; + break; + case FD_FUNKIER_CREATE_EXCL: + open_flags = O_CREAT|O_EXCL|O_RDWR; + can_create = 1; + can_resize = 1; + do_new = 1; + break; + default: + FD_LOG_WARNING(( "invalid mode when opening %s", filename )); + return NULL; + } + + int fd; + if( FD_UNLIKELY( filename == NULL || filename[0] == '\0' ) ) { + fd = -1; /* Anonymous */ + do_new = 1; + } else { + + /* Open the file */ + FD_LOG_DEBUG(( "opening %s", filename )); + fd = open( filename, open_flags, S_IRUSR|S_IWUSR ); + if( FD_UNLIKELY( fd < 0 ) ) { + FD_LOG_WARNING(( "error opening %s: %s", filename, strerror(errno) )); + return NULL; + } + + /* Resize the file */ + + struct stat statbuf; + int r = fstat( fd, &statbuf ); + if( FD_UNLIKELY( r < 0 ) ) { + FD_LOG_WARNING(( "error opening %s: %s", filename, strerror(errno) )); + close( fd ); + return NULL; + } + if( (can_create && statbuf.st_size == 0) || + (can_resize && statbuf.st_size != (off_t)total_sz) ) { + FD_LOG_DEBUG(( "resizing %s to %lu", filename, total_sz )); + if( FD_UNLIKELY( ftruncate( fd, (off_t)total_sz ) < 0 ) ) { + FD_LOG_WARNING(( "error resizing %s: %s", filename, strerror(errno) )); + close( fd ); + return NULL; + } + do_new = 1; + } else { + total_sz = (ulong)statbuf.st_size; + } + } + + if( FD_UNLIKELY( total_sz & (PAGESIZE-1) ) ) { + FD_LOG_WARNING(( "file size must be a multiple of a %lu", PAGESIZE )); + close( fd ); + return NULL; + } + + /* Force all the disk blocks to be physically allocated to avoid major faults in the future */ + + if( do_new & (fd != -1) ) { + FD_LOG_DEBUG(( "zeroing %s", (filename ? filename : "(NULL)") )); + uchar zeros[4<<20]; + memset( zeros, 0, sizeof(zeros) ); + for( ulong i = 0; i < total_sz; ) { + ulong sz = fd_ulong_min( sizeof(zeros), total_sz - i ); + if( FD_UNLIKELY( pwrite( fd, zeros, sz, (__off_t)i ) < (ssize_t)sz ) ) { + FD_LOG_WARNING(( "error zeroing %s: %s", (filename ? filename : "(NULL)"), strerror(errno) )); + close( fd ); + return NULL; + } + sync_file_range( fd, (__off64_t)i, (__off64_t)sz, SYNC_FILE_RANGE_WRITE ); + i += sz; + } + } + + /* Create the memory map */ + + FD_LOG_DEBUG(( "mapping %s", (filename ? filename : "(NULL)") )); + void * shmem = mmap( NULL, total_sz, (PROT_READ|PROT_WRITE), + (fd == -1 ? (MAP_ANONYMOUS|MAP_PRIVATE) : MAP_SHARED), fd, 0 ); + if( FD_UNLIKELY ( shmem == MAP_FAILED ) ) { + FD_LOG_WARNING(( "error mapping %s: %s", (filename ? filename : "(NULL)"), strerror(errno) )); + close( fd ); + return NULL; + } + + if( do_new ) { + + /* Create the data structures */ + + ulong part_max = fd_wksp_part_max_est( total_sz, 1U<<18U ); + if( FD_UNLIKELY( !part_max ) ) { + FD_LOG_WARNING(( "fd_wksp_part_max_est(%lu,64KiB) failed", total_sz )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + ulong data_max = fd_wksp_data_max_est( total_sz, part_max ); + if( FD_UNLIKELY( !data_max ) ) { + FD_LOG_WARNING(( "part_max (%lu) too large for footprint %lu", part_max, total_sz )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + FD_LOG_DEBUG(( "creating workspace in %s", (filename ? filename : "(NULL)") )); + void * shwksp = fd_wksp_new( shmem, "funk", (uint)seed, part_max, data_max ); + if( FD_UNLIKELY( !shwksp ) ) { + FD_LOG_WARNING(( "fd_wksp_new(%p,\"%s\",%lu,%lu,%lu) failed", shmem, "funk", seed, part_max, data_max )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + fd_wksp_t * wksp = fd_wksp_join( shwksp ); + if( FD_UNLIKELY( !wksp ) ) { + FD_LOG_WARNING(( "fd_wksp_join(%p) failed", shwksp )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + ulong page_sz = PAGESIZE; + ulong page_cnt = total_sz/PAGESIZE; + int join_err = fd_shmem_join_anonymous( "funk", FD_SHMEM_JOIN_MODE_READ_WRITE, wksp, shmem, page_sz, page_cnt ); + if( join_err ) { + FD_LOG_WARNING(( "fd_shmem_join_anonymous failed" )); + } + + FD_LOG_DEBUG(( "creating funk in %s", (filename ? filename : "(NULL)") )); + void * funk_shmem = fd_wksp_alloc_laddr( wksp, fd_funkier_align(), fd_funkier_footprint( txn_max, rec_max ), wksp_tag ); + if( FD_UNLIKELY(funk_shmem == NULL ) ) { + FD_LOG_WARNING(( "failed to allocate a funky" )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + fd_funkier_t * funk = fd_funkier_join( fd_funkier_new( funk_shmem, wksp_tag, seed, txn_max, rec_max ) ); + if( FD_UNLIKELY( funk == NULL ) ) { + FD_LOG_WARNING(( "failed to allocate a funky" )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + FD_LOG_NOTICE(( "opened funk size %f GB, backing file %s", ((double)total_sz)/((double)(1LU<<30)), (filename ? filename : "(NULL)") )); + + if( FD_UNLIKELY( close_args_out != NULL ) ) { + close_args_out->shmem = shmem; + close_args_out->fd = fd; + close_args_out->total_sz = total_sz; + } + return funk; + + } else { + + /* Join the data existing structures */ + + fd_wksp_t * wksp = fd_wksp_join( shmem ); + if( FD_UNLIKELY( !wksp ) ) { + FD_LOG_WARNING(( "fd_wksp_join(%p) failed", shmem )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + ulong page_sz = PAGESIZE; + ulong page_cnt = total_sz/PAGESIZE; + int join_err = fd_shmem_join_anonymous( "funk", FD_SHMEM_JOIN_MODE_READ_WRITE, wksp, shmem, page_sz, page_cnt ); + if( FD_UNLIKELY( join_err ) ) { + FD_LOG_WARNING(( "fd_shmem_join_anonymous failed" )); + } + + fd_wksp_tag_query_info_t info; + if( FD_UNLIKELY( !fd_wksp_tag_query( wksp, &wksp_tag, 1, &info, 1 ) ) ) { + FD_LOG_WARNING(( "%s does not contain a funky", filename )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + void * funk_shmem = fd_wksp_laddr_fast( wksp, info.gaddr_lo ); + fd_funkier_t * funk = fd_funkier_join( funk_shmem ); + if( FD_UNLIKELY( funk == NULL ) ) { + FD_LOG_WARNING(( "failed to join a funky" )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + if( mode == FD_FUNKIER_READONLY ) { + if( FD_UNLIKELY( mprotect( shmem, total_sz, PROT_READ ) ) ) { + FD_LOG_WARNING(( "mprotect failed (%i-%s)", errno, fd_io_strerror( errno ) )); + } + } + + FD_LOG_NOTICE(( "opened funk size %f GB, backing file %s", ((double)total_sz)/((double)(1LU<<30)), (filename ? filename : "(NULL)") )); + + if( FD_UNLIKELY( close_args_out != NULL ) ) { + close_args_out->shmem = shmem; + close_args_out->fd = fd; + close_args_out->total_sz = total_sz; + } + return funk; + } +} + +fd_funkier_t * +fd_funkier_recover_checkpoint( const char * funk_filename, + ulong wksp_tag, + const char * checkpt_filename, + fd_funkier_close_file_args_t * close_args_out ) { + /* Make the funk workspace match the parameters used to create the + checkpoint. */ + + fd_wksp_preview_t preview[1]; + int err = fd_wksp_preview( checkpt_filename, preview ); + if( FD_UNLIKELY( err ) ) { + FD_LOG_WARNING(( "unable to preview %s (%i-%s)", checkpt_filename, err, fd_wksp_strerror( err ) )); + return NULL; + } + uint seed = preview->seed; + ulong part_max = preview->part_max; + ulong data_max = preview->data_max; + + ulong total_sz = fd_wksp_footprint( part_max, data_max ); + + int fd; + if( funk_filename == NULL || funk_filename[0] == '\0' ) { + fd = -1; /* Anonymous */ + + } else { + + /* Open the file */ + fd = open( funk_filename, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR ); + if( FD_UNLIKELY( fd < 0 ) ) { + FD_LOG_WARNING(( "error opening %s: %s", funk_filename, strerror(errno) )); + return NULL; + } + + /* Resize the file */ + + struct stat statbuf; + int r = fstat( fd, &statbuf ); + if( FD_UNLIKELY( r < 0 ) ) { + FD_LOG_WARNING(( "error opening %s: %s", funk_filename, strerror(errno) )); + close( fd ); + return NULL; + } + if( statbuf.st_size != (off_t)total_sz ) { + if( FD_UNLIKELY( ftruncate( fd, (off_t)total_sz ) < 0 ) ) { + FD_LOG_WARNING(( "error resizing %s: %s", funk_filename, strerror(errno) )); + close( fd ); + return NULL; + } + } + + /* Force all the disk blocks to be physically allocated to avoid major faults in the future */ + + uchar zeros[4<<20]; + memset( zeros, 0, sizeof(zeros) ); + for( ulong i = 0; i < total_sz; ) { + ulong sz = fd_ulong_min( sizeof(zeros), total_sz - i ); + if( FD_UNLIKELY ( pwrite( fd, zeros, sz, (__off_t)i ) < (ssize_t)sz ) ) { + FD_LOG_WARNING(( "error zeroing %s: %s", (funk_filename ? funk_filename : "(NULL)"), strerror(errno) )); + close( fd ); + return NULL; + } + sync_file_range( fd, (__off64_t)i, (__off64_t)sz, SYNC_FILE_RANGE_WRITE ); + i += sz; + } + } + + /* Create the memory map */ + + void * shmem = mmap( NULL, total_sz, PROT_READ|PROT_WRITE, + (fd == -1 ? (MAP_ANONYMOUS|MAP_PRIVATE) : MAP_SHARED), fd, 0 ); + + if( FD_UNLIKELY( shmem == MAP_FAILED ) ) { + FD_LOG_WARNING(( "error mapping %s: %s", (funk_filename ? funk_filename : "(NULL)"), strerror(errno) )); + close( fd ); + return NULL; + } + + /* Create the workspace */ + + void * shwksp = fd_wksp_new( shmem, "funk", seed, part_max, data_max ); + if( FD_UNLIKELY( !shwksp ) ) { + FD_LOG_WARNING(( "fd_wksp_new(%p,\"%s\",%u,%lu,%lu) failed", shmem, "funk", seed, part_max, data_max )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + fd_wksp_t * wksp = fd_wksp_join( shwksp ); + if( FD_UNLIKELY( !wksp ) ) { + FD_LOG_WARNING(( "fd_wksp_join(%p) failed", shwksp )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + ulong page_sz = PAGESIZE; + ulong page_cnt = total_sz/PAGESIZE; + int join_err = fd_shmem_join_anonymous( "funk", FD_SHMEM_JOIN_MODE_READ_WRITE, wksp, shmem, page_sz, page_cnt ); + if( FD_UNLIKELY( join_err ) ) { + FD_LOG_WARNING(( "fd_shmem_join_anonymous failed" )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + /* Restore the checkpoint */ + + if( fd_wksp_restore( wksp, checkpt_filename, seed ) ) { + FD_LOG_WARNING(( "restoring %s failed", checkpt_filename )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + /* Let's play find the funk */ + + fd_wksp_tag_query_info_t info; + if( FD_UNLIKELY( !fd_wksp_tag_query( wksp, &wksp_tag, 1, &info, 1 ) ) ) { + FD_LOG_WARNING(( "%s does not contain a funky", checkpt_filename )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + void * funk_shmem = fd_wksp_laddr_fast( wksp, info.gaddr_lo ); + fd_funkier_t * funk = fd_funkier_join( funk_shmem ); + if( FD_UNLIKELY( funk == NULL ) ) { + FD_LOG_WARNING(( "failed to join a funky" )); + munmap( shmem, total_sz ); + close( fd ); + return NULL; + } + + FD_LOG_NOTICE(( "opened funk size %f GB, backing file %s", ((double)total_sz)/((double)(1LU<<30)), (funk_filename ? funk_filename : "(NULL)") )); + + if( FD_UNLIKELY( close_args_out != NULL ) ) { + close_args_out->shmem = shmem; + close_args_out->fd = fd; + close_args_out->total_sz = total_sz; + } + return funk; +} + +void +fd_funkier_close_file( fd_funkier_close_file_args_t * close_args ) { + fd_shmem_leave_anonymous( close_args->shmem, NULL ); + munmap( close_args->shmem, close_args->total_sz ); + close( close_args->fd ); +} diff --git a/src/funkier/fd_funkier_filemap.h b/src/funkier/fd_funkier_filemap.h new file mode 100644 index 0000000000..1f7451baec --- /dev/null +++ b/src/funkier/fd_funkier_filemap.h @@ -0,0 +1,66 @@ +#ifndef HEADER_fd_src_funk_fd_funkier_filemap_h +#define HEADER_fd_src_funk_fd_funkier_filemap_h + +#include "fd_funkier.h" + +enum fd_funkier_file_mode { + FD_FUNKIER_READONLY, /* Only open the file if it already exists, memory is marked readonly */ + FD_FUNKIER_READ_WRITE, /* Only open the file if it already exists, can be written to */ + FD_FUNKIER_CREATE, /* Use an existing file if available, otherwise create */ + FD_FUNKIER_OVERWRITE, /* Create new or overwrite existing with a fresh instance */ + FD_FUNKIER_CREATE_EXCL /* Fail if file exists, only create new */ +}; +typedef enum fd_funkier_file_mode fd_funkier_file_mode_t; + +/* fd_funkier_close_file_args_t contains the parameters needed by + * fd_funkier_close_file. It is initalized in fd_funkier_open_file. */ + +struct fd_funkier_close_file_args { + void * shmem; + int fd; + ulong total_sz; +}; +typedef struct fd_funkier_close_file_args fd_funkier_close_file_args_t; + +/* Open or create a funk instance with an optional mmap backing file. + filename is the backing file, or NULL for a local/anonymous + instance. wksp_tag is the workspace partition tag for funk (usually + just 1). seed is the randomized hash seed. txn_max is the maximum + number of funk transactions. rec_max is the maximum number of funk + records. total_sz is the total size of the funk workspace. mode is + the file mode (see above). close_args_opt is an optional pointer to a + structure which is filled in. This is needed for fd_funkier_close_file. + + Note that seed, txn_max, rec_max, and total_sz are ignored if + an existing file is opened without being overwritten. */ + +fd_funkier_t * +fd_funkier_open_file( const char * filename, + ulong wksp_tag, + ulong seed, + ulong txn_max, + ulong rec_max, + ulong total_sz, + fd_funkier_file_mode_t mode, + fd_funkier_close_file_args_t * close_args_out ); + +/* Load a workspace checkpoint containing a funk + instance. funk_filename is the backing file, or NULL for a + local/anonymous instance. wksp_tag is the workspace partition tag + for funk (usually just 1). checkpt_filename is the checkpoint + file. close_args_opt is an optional pointer to a structure which is + filled in. This is needed for fd_funkier_close_file. */ + +fd_funkier_t * +fd_funkier_recover_checkpoint( const char * funk_filename, + ulong wksp_tag, + const char * checkpt_filename, + fd_funkier_close_file_args_t * close_args_out ); + +/* Release the resources associated with a funk file map. The funk + pointer is invalid after this is called. */ + +void +fd_funkier_close_file( fd_funkier_close_file_args_t * close_args ); + +#endif /* HEADER_fd_src_funk_fd_funkier_filemap_h */ diff --git a/src/funkier/fd_funkier_rec.c b/src/funkier/fd_funkier_rec.c new file mode 100644 index 0000000000..b5128b2cc1 --- /dev/null +++ b/src/funkier/fd_funkier_rec.c @@ -0,0 +1,469 @@ +#include "fd_funkier.h" + +/* Provide the actual record map implementation */ + +#define POOL_NAME fd_funkier_rec_pool +#define POOL_ELE_T fd_funkier_rec_t +#define POOL_IDX_T uint +#define POOL_NEXT map_next +#define POOL_IMPL_STYLE 2 +#include "../util/tmpl/fd_pool_para.c" + +#define MAP_NAME fd_funkier_rec_map +#define MAP_ELE_T fd_funkier_rec_t +#define MAP_KEY_T fd_funkier_xid_key_pair_t +#define MAP_KEY pair +#define MAP_KEY_EQ(k0,k1) fd_funkier_xid_key_pair_eq((k0),(k1)) +#define MAP_KEY_HASH(k0,seed) fd_funkier_xid_key_pair_hash((k0),(seed)) +#define MAP_NEXT map_next +#define MAP_MEMO map_hash +#define MAP_MAGIC (0xf173da2ce77ecdb0UL) /* Firedancer rec db version 0 */ +#define MAP_MEMOIZE 1 +#define MAP_IMPL_STYLE 2 +#include "../util/tmpl/fd_map_para.c" + +fd_funkier_rec_t const * +fd_funkier_rec_query_try( fd_funkier_t * funk, + fd_funkier_txn_t const * txn, + fd_funkier_rec_key_t const * key, + fd_funkier_rec_query_t * query ) { +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( funk==NULL || key==NULL || query==NULL ) ) { + return NULL; + } + if( FD_UNLIKELY( txn && !fd_funkier_txn_valid( funk, txn ) ) ) { + return NULL; + } +#endif + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_rec_map_t rec_map = fd_funkier_rec_map( funk, wksp ); + fd_funkier_xid_key_pair_t pair[1]; + if( txn == NULL ) { + fd_funkier_txn_xid_set_root( pair->xid ); + } else { + fd_funkier_txn_xid_copy( pair->xid, &txn->xid ); + } + fd_funkier_rec_key_copy( pair->key, key ); + for(;;) { + int err = fd_funkier_rec_map_query_try( &rec_map, pair, NULL, query ); + if( err == FD_MAP_SUCCESS ) break; + if( err == FD_MAP_ERR_KEY ) return NULL; + if( err == FD_MAP_ERR_AGAIN ) continue; + FD_LOG_CRIT(( "query returned err %d", err )); + } + return fd_funkier_rec_map_query_ele_const( query ); +} + +fd_funkier_rec_t const * +fd_funkier_rec_query_try_global( fd_funkier_t * funk, + fd_funkier_txn_t const * txn, + fd_funkier_rec_key_t const * key, + fd_funkier_txn_t const ** txn_out, + fd_funkier_rec_query_t * query ) { +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( funk==NULL || key==NULL || query==NULL ) ) { + return NULL; + } + if( FD_UNLIKELY( txn && !fd_funkier_txn_valid( funk, txn ) ) ) { + return NULL; + } +#endif + + /* Look for the first element in the hash chain with the right + record key. This takes advantage of the fact that elements with + the same record key appear on the same hash chain in order of + newest to oldest. */ + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_rec_map_t rec_map = fd_funkier_rec_map( funk, wksp ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + + fd_funkier_xid_key_pair_t pair[1]; + if( txn == NULL ) { + fd_funkier_txn_xid_set_root( pair->xid ); + } else { + fd_funkier_txn_xid_copy( pair->xid, &txn->xid ); + } + fd_funkier_rec_key_copy( pair->key, key ); + ulong hash = fd_funkier_rec_map_key_hash( pair, rec_map.map->seed ); + ulong chain_idx = (hash & (rec_map.map->chain_cnt-1UL) ); + if( fd_funkier_rec_map_iter_lock( &rec_map, &chain_idx, 1, FD_MAP_FLAG_BLOCKING) ) { + FD_LOG_CRIT(( "failed to lock hash chain" )); + } + + fd_funkier_rec_map_shmem_private_chain_t * chain = (fd_funkier_rec_map_shmem_private_chain_t *)(rec_map.map+1) + chain_idx; + query->ele = NULL; + query->chain = chain; + query->ver_cnt = chain->ver_cnt + (1UL<<43U); /* After unlock */ + + for( fd_funkier_rec_map_iter_t iter = fd_funkier_rec_map_iter( &rec_map, chain_idx ); + !fd_funkier_rec_map_iter_done( iter ); + iter = fd_funkier_rec_map_iter_next( iter ) ) { + fd_funkier_rec_t const * ele = fd_funkier_rec_map_iter_ele_const( iter ); + if( FD_LIKELY( hash == ele->map_hash ) && FD_LIKELY( fd_funkier_rec_key_eq( key, ele->pair.key ) ) ) { + + /* For cur_txn in path from [txn] to [root] where root is NULL */ + + for( fd_funkier_txn_t const * cur_txn = txn; ; cur_txn = fd_funkier_txn_parent( cur_txn, &txn_pool ) ) { + /* If record ele is part of transaction cur_txn, we have a + match. According to the property above, this will be the + youngest descendent in the transaction stack. */ + + int match = FD_UNLIKELY( cur_txn ) ? /* opt for root find (FIXME: eliminate branch with cmov into txn_xid_eq?) */ + fd_funkier_txn_xid_eq( &cur_txn->xid, ele->pair.xid ) : + fd_funkier_txn_xid_eq_root( ele->pair.xid ); + + if( FD_LIKELY( match ) ) { + if( txn_out ) *txn_out = cur_txn; + query->ele = ( FD_UNLIKELY( ele->flags & FD_FUNKIER_REC_FLAG_ERASE ) ? NULL : + (fd_funkier_rec_t *)ele ); + fd_funkier_rec_map_iter_unlock( &rec_map, &chain_idx, 1 ); + return query->ele; + } + + if( cur_txn == NULL ) break; + } + } + } + fd_funkier_rec_map_iter_unlock( &rec_map, &chain_idx, 1 ); + return NULL; +} + +int +fd_funkier_rec_query_test( fd_funkier_rec_query_t * query ) { + return fd_funkier_rec_map_query_test( query ); +} + +fd_funkier_rec_t * +fd_funkier_rec_prepare( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + fd_funkier_rec_key_t const * key, + fd_funkier_rec_prepare_t * prepare, + int * opt_err ) { +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( funk==NULL || key==NULL || prepare==NULL ) ) { + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_ERR_INVAL ); + return NULL; + } + if( FD_UNLIKELY( txn && !fd_funkier_txn_valid( funk, txn ) ) ) { + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_ERR_INVAL ); + return NULL; + } +#endif + + if( !txn ) { /* Modifying last published */ + if( FD_UNLIKELY( fd_funkier_last_publish_is_frozen( funk ) ) ) { + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_ERR_FROZEN ); + return NULL; + } + } else { + if( FD_UNLIKELY( fd_funkier_txn_is_frozen( txn ) ) ) { + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_ERR_FROZEN ); + return NULL; + } + } + + prepare->funk = funk; + prepare->wksp = fd_funkier_wksp( funk ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( funk, prepare->wksp ); + fd_funkier_rec_t * rec = prepare->rec = fd_funkier_rec_pool_acquire( &rec_pool, NULL, 1, opt_err ); + if( rec != NULL ) { + if( txn == NULL ) { + fd_funkier_txn_xid_set_root( rec->pair.xid ); + rec->txn_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + prepare->rec_head_idx = &funk->rec_head_idx; + prepare->rec_tail_idx = &funk->rec_tail_idx; + } else { + fd_funkier_txn_xid_copy( rec->pair.xid, &txn->xid ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, prepare->wksp ); + rec->txn_cidx = fd_funkier_txn_cidx( (ulong)( txn - txn_pool.ele ) ); + prepare->rec_head_idx = &txn->rec_head_idx; + prepare->rec_tail_idx = &txn->rec_tail_idx; + } + fd_funkier_rec_key_copy( rec->pair.key, key ); + fd_funkier_val_init( rec ); + rec->tag = 0; + rec->flags = 0; + rec->prev_idx = FD_FUNKIER_REC_IDX_NULL; + rec->next_idx = FD_FUNKIER_REC_IDX_NULL; + } else { + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_ERR_REC ); + } + return rec; +} + +void +fd_funkier_rec_publish( fd_funkier_rec_prepare_t * prepare ) { + fd_funkier_rec_t * rec = prepare->rec; + ulong * rec_head_idx = prepare->rec_head_idx; + ulong * rec_tail_idx = prepare->rec_tail_idx; + fd_funkier_rec_map_t rec_map = fd_funkier_rec_map( prepare->funk, prepare->wksp ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( prepare->funk, prepare->wksp ); + + /* Use the tail idx to establish an order even if there is concurrency */ + ulong rec_prev_idx; + ulong rec_idx = (ulong)( rec - rec_pool.ele ); + for(;;) { + rec_prev_idx = *rec_tail_idx; + if( FD_ATOMIC_CAS( rec_tail_idx, rec_prev_idx, rec_idx ) == rec_prev_idx ) break; + } + rec->prev_idx = rec_prev_idx; + if( fd_funkier_rec_idx_is_null( rec_prev_idx ) ) { + *rec_head_idx = rec_idx; + } else { + rec_pool.ele[ rec_prev_idx ].next_idx = rec_idx; + } + + if( fd_funkier_rec_map_insert( &rec_map, rec, FD_MAP_FLAG_BLOCKING ) ) { + FD_LOG_CRIT(( "fd_funkier_rec_map_insert failed" )); + } +} + +void +fd_funkier_rec_cancel( fd_funkier_rec_prepare_t * prepare ) { + fd_funkier_val_flush( prepare->rec, fd_funkier_alloc( prepare->funk, prepare->wksp ), prepare->wksp ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( prepare->funk, prepare->wksp ); + fd_funkier_rec_pool_release( &rec_pool, prepare->rec, 1 ); +} + +int +fd_funkier_rec_is_full( fd_funkier_t * funk ) { + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( funk, wksp ); + return fd_funkier_rec_pool_is_empty( &rec_pool ); +} + +int +fd_funkier_rec_remove( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + fd_funkier_rec_key_t const * key, + fd_funkier_rec_t ** rec_out, + ulong erase_data ) { +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( funk==NULL || key==NULL ) ) { + return FD_FUNKIER_ERR_INVAL; + } + if( FD_UNLIKELY( txn && !fd_funkier_txn_valid( funk, txn ) ) ) { + return FD_FUNKIER_ERR_INVAL; + } +#endif + + if( !txn ) { /* Modifying last published */ + if( FD_UNLIKELY( fd_funkier_last_publish_is_frozen( funk ) ) ) { + return FD_FUNKIER_ERR_FROZEN; + } + } else { + if( FD_UNLIKELY( fd_funkier_txn_is_frozen( txn ) ) ) { + return FD_FUNKIER_ERR_FROZEN; + } + } + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_rec_map_t rec_map = fd_funkier_rec_map( funk, wksp ); + fd_funkier_xid_key_pair_t pair[1]; + if( txn == NULL ) { + fd_funkier_txn_xid_set_root( pair->xid ); + } else { + fd_funkier_txn_xid_copy( pair->xid, &txn->xid ); + } + fd_funkier_rec_key_copy( pair->key, key ); + fd_funkier_rec_query_t query[ 1 ]; + for(;;) { + int err = fd_funkier_rec_map_query_try( &rec_map, pair, NULL, query ); + if( err == FD_MAP_SUCCESS ) break; + if( err == FD_MAP_ERR_KEY ) return FD_FUNKIER_SUCCESS; + if( err == FD_MAP_ERR_AGAIN ) continue; + FD_LOG_CRIT(( "query returned err %d", err )); + } + + fd_funkier_rec_t * rec = fd_funkier_rec_map_query_ele( query ); + if( rec_out ) *rec_out = rec; + + /* Access the flags atomically */ + ulong old_flags; + for(;;) { + old_flags = rec->flags; + if( FD_UNLIKELY( old_flags & FD_FUNKIER_REC_FLAG_ERASE ) ) return FD_FUNKIER_SUCCESS; + if( FD_ATOMIC_CAS( &rec->flags, old_flags, old_flags | FD_FUNKIER_REC_FLAG_ERASE ) == old_flags ) break; + } + + /* Flush the value and leave a tombstone behind. In theory, this can + lead to an unbounded number of records, but for application + reasons, we need to remember what was deleted. */ + + fd_funkier_val_flush( rec, fd_funkier_alloc( funk, wksp ), wksp ); + + /* At this point, the 5 most significant bytes should store data about the + transaction that the record was updated in. */ + + fd_funkier_rec_set_erase_data( rec, erase_data ); + + return FD_FUNKIER_SUCCESS; +} + +void +fd_funkier_rec_set_erase_data( fd_funkier_rec_t * rec, ulong erase_data ) { + rec->flags |= ((erase_data & 0xFFFFFFFFFFUL) << (sizeof(unsigned long) * 8 - 40)); +} + +ulong +fd_funkier_rec_get_erase_data( fd_funkier_rec_t const * rec ) { + return (rec->flags >> (sizeof(unsigned long) * 8 - 40)) & 0xFFFFFFFFFFUL; +} + +static void +fd_funkier_all_iter_skip_nulls( fd_funkier_all_iter_t * iter ) { + if( iter->chain_idx == iter->chain_cnt ) return; + while( fd_funkier_rec_map_iter_done( iter->rec_map_iter ) ) { + if( ++(iter->chain_idx) == iter->chain_cnt ) break; + iter->rec_map_iter = fd_funkier_rec_map_iter( &iter->rec_map, iter->chain_idx ); + } +} + +void +fd_funkier_all_iter_new( fd_funkier_t * funk, fd_funkier_all_iter_t * iter ) { + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + iter->rec_map = fd_funkier_rec_map( funk, wksp ); + iter->chain_cnt = fd_funkier_rec_map_chain_cnt( &iter->rec_map ); + iter->chain_idx = 0; + iter->rec_map_iter = fd_funkier_rec_map_iter( &iter->rec_map, 0 ); + fd_funkier_all_iter_skip_nulls( iter ); +} + +int +fd_funkier_all_iter_done( fd_funkier_all_iter_t * iter ) { + return ( iter->chain_idx == iter->chain_cnt ); +} + +void +fd_funkier_all_iter_next( fd_funkier_all_iter_t * iter ) { + iter->rec_map_iter = fd_funkier_rec_map_iter_next( iter->rec_map_iter ); + fd_funkier_all_iter_skip_nulls( iter ); +} + +fd_funkier_rec_t const * +fd_funkier_all_iter_ele_const( fd_funkier_all_iter_t * iter ) { + return fd_funkier_rec_map_iter_ele_const( iter->rec_map_iter ); +} + +#ifdef FD_FUNKIER_HANDHOLDING +int +fd_funkier_rec_verify( fd_funkier_t * funk ) { + fd_wksp_t * wksp = fd_funkier_wksp( funk ); /* Previously verified */ + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( funk, wksp ); /* Previously verified */ + fd_funkier_rec_map_t rec_map = fd_funkier_rec_map( funk, wksp ); /* Previously verified */ + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); /* Previously verified */ + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( funk, wksp ); /* Previously verified */ + ulong txn_max = funk->txn_max; /* Previously verified */ + ulong rec_max = funk->rec_max; /* Previously verified */ + + /* At this point, txn_map has been extensively verified */ + +# define TEST(c) do { \ + if( FD_UNLIKELY( !(c) ) ) { FD_LOG_WARNING(( "FAIL: %s", #c )); return FD_FUNKIER_ERR_INVAL; } \ + } while(0) + + TEST( !fd_funkier_rec_map_verify( &rec_map ) ); + TEST( !fd_funkier_rec_pool_verify( &rec_pool ) ); + + /* Iterate over all records in use */ + + fd_funkier_all_iter_t iter[1]; + for( fd_funkier_all_iter_new( funk, iter ); !fd_funkier_all_iter_done( iter ); fd_funkier_all_iter_next( iter ) ) { + fd_funkier_rec_t const * rec = fd_funkier_all_iter_ele_const( iter ); + + /* Make sure every record either links up with the last published + transaction or an in-prep transaction and the flags are sane. */ + + fd_funkier_txn_xid_t const * txn_xid = fd_funkier_rec_xid( rec ); + ulong txn_idx = fd_funkier_txn_idx( rec->txn_cidx ); + + if( fd_funkier_txn_idx_is_null( txn_idx ) ) { /* This is a record from the last published transaction */ + + TEST( fd_funkier_txn_xid_eq_root( txn_xid ) ); + + } else { /* This is a record from an in-prep transaction */ + + TEST( txn_idxrec_head_idx; + while( !fd_funkier_rec_idx_is_null( rec_idx ) ) { + TEST( (rec_idxrec_head_idx; + while( !fd_funkier_rec_idx_is_null( rec_idx ) ) { + TEST( (rec_idxrec_tail_idx; + while( !fd_funkier_rec_idx_is_null( rec_idx ) ) { + TEST( (rec_idxrec_tail_idx; + while( !fd_funkier_rec_idx_is_null( rec_idx ) ) { + TEST( (rec_idxpair; } +FD_FN_CONST static inline fd_funkier_txn_xid_t const * fd_funkier_rec_xid ( fd_funkier_rec_t const * rec ) { return rec->pair.xid; } +FD_FN_CONST static inline fd_funkier_rec_key_t const * fd_funkier_rec_key ( fd_funkier_rec_t const * rec ) { return rec->pair.key; } + +/* fd_funkier_rec_prepare prepares to insert a new record. This call just + allocates a record from the pool and initializes it. + The application should then fill in the new + value. fd_funkier_rec_publish actually does the map insert and + should be called once the value is correct. */ + +fd_funkier_rec_t * +fd_funkier_rec_prepare( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + fd_funkier_rec_key_t const * key, + fd_funkier_rec_prepare_t * prepare, + int * opt_err ); + +/* fd_funkier_rec_publish inserts a prepared record into the record map. */ + +void +fd_funkier_rec_publish( fd_funkier_rec_prepare_t * prepare ); + +/* fd_funkier_rec_cancel returns a prepared record to the pool without + inserting it. */ + +void +fd_funkier_rec_cancel( fd_funkier_rec_prepare_t * prepare ); + +/* fd_funkier_rec_is_full returns true if no more records can be + allocated. */ + +int +fd_funkier_rec_is_full( fd_funkier_t * funk ); + +/* fd_funkier_rec_remove removes the live record with the + given (xid,key) from funk. Returns FD_FUNKIER_SUCCESS (0) on + success and a FD_FUNKIER_ERR_* (negative) on failure. Reasons for + failure include: + + FD_FUNKIER_ERR_INVAL - bad inputs (NULL funk, NULL xid) + + FD_FUNKIER_ERR_KEY - the record did not appear to be a live record. + Specifically, a record query of funk for rec's (xid,key) pair did + not return rec. + + The record will cease to exist in that transaction and any of + transaction's subsequently created descendants (again, assuming no + subsequent insert of key). This type of remove can be done on a + published record (assuming the last published transaction is + unfrozen). A tombstone is left in funk to track removals as they + are published or cancelled. + + Any information in an erased record is lost. + + This is a reasonably fast O(1) and fortified against memory + corruption. */ + +int +fd_funkier_rec_remove( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + fd_funkier_rec_key_t const * key, + fd_funkier_rec_t ** rec_out, + ulong erase_data ); + + +/* When a record is erased there is metadata stored in the five most + significant bytes of record flags. These are helpers to make setting + and getting these values simple. The caller is responsible for doing + a check on the flag of the record before using the value of the erase + data. The 5 least significant bytes of the erase data parameter will + be used and set into the erase flag. */ + +void +fd_funkier_rec_set_erase_data( fd_funkier_rec_t * rec, ulong erase_data ); + +ulong +fd_funkier_rec_get_erase_data( fd_funkier_rec_t const * rec ); + +/* Iterator which walks all records in all transactions. Usage is: + + fd_funkier_all_iter_t iter[1]; + for( fd_funkier_all_iter_new( funk, iter ); !fd_funkier_all_iter_done( iter ); fd_funkier_all_iter_next( iter ) ) { + fd_funkier_rec_t const * rec = fd_funkier_all_iter_ele_const( iter ); + ... + } +*/ + +struct fd_funkier_all_iter { + fd_funkier_rec_map_t rec_map; + ulong chain_cnt; + ulong chain_idx; + fd_funkier_rec_map_iter_t rec_map_iter; +}; + +typedef struct fd_funkier_all_iter fd_funkier_all_iter_t; + +void fd_funkier_all_iter_new( fd_funkier_t * funk, fd_funkier_all_iter_t * iter ); + +int fd_funkier_all_iter_done( fd_funkier_all_iter_t * iter ); + +void fd_funkier_all_iter_next( fd_funkier_all_iter_t * iter ); + +fd_funkier_rec_t const * fd_funkier_all_iter_ele_const( fd_funkier_all_iter_t * iter ); + +/* Misc */ + +#ifdef FD_FUNKIER_HANDHOLDING +/* fd_funkier_rec_verify verifies the record map. Returns FD_FUNKIER_SUCCESS + if the record map appears intact and FD_FUNKIER_ERR_INVAL if not (logs + details). Meant to be called as part of fd_funkier_verify. As such, it + assumes funk is non-NULL, fd_funkier_{wksp,txn_map,rec_map} have been + verified to work and the txn_map has been verified. */ + +int +fd_funkier_rec_verify( fd_funkier_t * funk ); +#endif + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_funk_fd_funkier_rec_h */ diff --git a/src/funkier/fd_funkier_txn.c b/src/funkier/fd_funkier_txn.c new file mode 100644 index 0000000000..bc81a9c33e --- /dev/null +++ b/src/funkier/fd_funkier_txn.c @@ -0,0 +1,971 @@ +#include "fd_funkier.h" + +/* Provide the actual transaction map implementation */ + +#define POOL_NAME fd_funkier_txn_pool +#define POOL_ELE_T fd_funkier_txn_t +#define POOL_IDX_T uint +#define POOL_NEXT map_next +#define POOL_IMPL_STYLE 2 +#include "../util/tmpl/fd_pool_para.c" + +#define MAP_NAME fd_funkier_txn_map +#define MAP_ELE_T fd_funkier_txn_t +#define MAP_KEY_T fd_funkier_txn_xid_t +#define MAP_KEY xid +#define MAP_KEY_EQ(k0,k1) fd_funkier_txn_xid_eq((k0),(k1)) +#define MAP_KEY_HASH(k0,seed) fd_funkier_txn_xid_hash((k0),(seed)) +#define MAP_NEXT map_next +#define MAP_HASH map_hash +#define MAP_MAGIC (0xf173da2ce7172db0UL) /* Firedancer txn db version 0 */ +#define MAP_IMPL_STYLE 2 +#include "../util/tmpl/fd_map_para.c" + +fd_funkier_txn_t * +fd_funkier_txn_prepare( fd_funkier_t * funk, + fd_funkier_txn_t * parent, + fd_funkier_txn_xid_t const * xid, + int verbose ) { + +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( !funk ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "NULL funk" )); + return NULL; + } + if( FD_UNLIKELY( !xid ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "NULL xid" )); + return NULL; + } + if( FD_UNLIKELY( parent && !fd_funkier_txn_valid( funk, parent ) ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "bad txn" )); + return NULL; + } + if( FD_UNLIKELY( fd_funkier_txn_xid_eq_root( xid ) ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "xid is the root" )); + return NULL; + } + if( FD_UNLIKELY( fd_funkier_txn_xid_eq( xid, funk->last_publish ) ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "xid is the last published" )); + return NULL; + } +#endif + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( funk, wksp ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + + fd_funkier_txn_map_query_t query[1]; + if( FD_UNLIKELY( fd_funkier_txn_map_query_try( &txn_map, xid, NULL, query ) != FD_MAP_ERR_KEY ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "xid in use" )); + return NULL; + } + + ulong parent_idx; + uint * _child_head_cidx; + uint * _child_tail_cidx; + + if( FD_LIKELY( !parent ) ) { /* opt for incr pub */ + + parent_idx = FD_FUNKIER_TXN_IDX_NULL; + + _child_head_cidx = &funk->child_head_cidx; + _child_tail_cidx = &funk->child_tail_cidx; + + } else { + + parent_idx = (ulong)(parent - txn_pool.ele); + + _child_head_cidx = &parent->child_head_cidx; + _child_tail_cidx = &parent->child_tail_cidx; + + } + + /* Get a new transaction from the map */ + + fd_funkier_txn_t * txn = fd_funkier_txn_pool_acquire( &txn_pool, NULL, 1, NULL ); + if( txn == NULL ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "transaction pool is exhuasted" )); + return NULL; + } + fd_funkier_txn_xid_copy( &txn->xid, xid ); + ulong txn_idx = (ulong)(txn - txn_pool.ele); + + /* Join the family */ + + ulong sibling_prev_idx = fd_funkier_txn_idx( *_child_tail_cidx ); + + int first_born = fd_funkier_txn_idx_is_null( sibling_prev_idx ); + + txn->parent_cidx = fd_funkier_txn_cidx( parent_idx ); + txn->child_head_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + txn->child_tail_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + txn->sibling_prev_cidx = fd_funkier_txn_cidx( sibling_prev_idx ); + txn->sibling_next_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + txn->stack_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + txn->tag = 0UL; + + txn->rec_head_idx = FD_FUNKIER_REC_IDX_NULL; + txn->rec_tail_idx = FD_FUNKIER_REC_IDX_NULL; + + /* TODO: consider branchless impl */ + if( FD_LIKELY( first_born ) ) *_child_head_cidx = fd_funkier_txn_cidx( txn_idx ); /* opt for non-compete */ + else txn_pool.ele[ sibling_prev_idx ].sibling_next_cidx = fd_funkier_txn_cidx( txn_idx ); + + *_child_tail_cidx = fd_funkier_txn_cidx( txn_idx ); + + fd_funkier_txn_map_insert( &txn_map, txn, FD_MAP_FLAG_BLOCKING ); + + return txn; +} + +int +fd_funkier_txn_is_full( fd_funkier_t * funk ) { + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + return fd_funkier_txn_pool_is_empty( &txn_pool ); +} + +/* fd_funkier_txn_cancel_childless cancels a transaction that is known + to be childless. Callers have already validated our input arguments. + Assumes that cancelling in the app can't fail but that could be + straightforward to support by giving this an error and plumbing + through to abort the overall cancel operation when it hits a error. */ + +static void +fd_funkier_txn_cancel_childless( fd_funkier_t * funk, + ulong txn_idx ) { + + /* Remove all records used by this transaction. Note that we don't + need to bother doing all the individual removal operations as we + are removing the whole list. We do reset the record transaction + idx with NULL though we can detect cycles as soon as possible + and abort. */ + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_alloc_t * alloc = fd_funkier_alloc( funk, wksp ); + fd_funkier_rec_map_t rec_map = fd_funkier_rec_map( funk, wksp ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( funk, wksp ); + ulong rec_max = funk->rec_max; + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( funk, wksp ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + + fd_funkier_txn_t * txn = &txn_pool.ele[ txn_idx ]; + ulong rec_idx = txn->rec_head_idx; + while( !fd_funkier_rec_idx_is_null( rec_idx ) ) { + + if( FD_UNLIKELY( rec_idx>=rec_max ) ) FD_LOG_CRIT(( "memory corruption detected (bad idx)" )); + if( FD_UNLIKELY( fd_funkier_txn_idx( rec_pool.ele[ rec_idx ].txn_cidx )!=txn_idx ) ) + FD_LOG_CRIT(( "memory corruption detected (cycle or bad idx)" )); + + fd_funkier_rec_t * rec = &rec_pool.ele[ rec_idx ]; + ulong next_idx = rec->next_idx; + rec->txn_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + + for(;;) { + fd_funkier_rec_map_query_t rec_query[1]; + int err = fd_funkier_rec_map_remove( &rec_map, fd_funkier_rec_pair( rec ), NULL, rec_query, FD_MAP_FLAG_BLOCKING ); + if( FD_UNLIKELY( err == FD_MAP_ERR_AGAIN ) ) continue; + if( err == FD_MAP_ERR_KEY ) break; + if( FD_UNLIKELY( err != FD_MAP_SUCCESS ) ) FD_LOG_CRIT(( "map corruption" )); + if( rec != fd_funkier_rec_map_query_ele( rec_query ) ) break; + fd_funkier_val_flush( rec, alloc, wksp ); + fd_funkier_rec_pool_release( &rec_pool, rec, 1 ); + break; + } + + rec_idx = next_idx; + } + + /* Leave the family */ + + ulong sibling_prev_idx = fd_funkier_txn_idx( txn->sibling_prev_cidx ); + ulong sibling_next_idx = fd_funkier_txn_idx( txn->sibling_next_cidx ); + + /* TODO: Consider branchless impl */ + + if( FD_LIKELY( fd_funkier_txn_idx_is_null( sibling_prev_idx ) ) ) { /* opt for non-compete */ + ulong parent_idx = fd_funkier_txn_idx( txn_pool.ele[ txn_idx ].parent_cidx ); + if( FD_LIKELY( fd_funkier_txn_idx_is_null( parent_idx ) ) ) { /* No older sib and is a funk child, opt for incr pub */ + funk->child_head_cidx = fd_funkier_txn_cidx( sibling_next_idx ); + } else { /* No older sib and has parent */ + txn_pool.ele[ parent_idx ].child_head_cidx = fd_funkier_txn_cidx( sibling_next_idx ); + } + } else { /* Has older sib */ + txn_pool.ele[ sibling_prev_idx ].sibling_next_cidx = fd_funkier_txn_cidx( sibling_next_idx ); + } + + if( FD_LIKELY( fd_funkier_txn_idx_is_null( sibling_next_idx ) ) ) { /* opt for non-compete */ + ulong parent_idx = fd_funkier_txn_idx( txn_pool.ele[ txn_idx ].parent_cidx ); + if( FD_LIKELY( fd_funkier_txn_idx_is_null( parent_idx ) ) ) { /* No younger sib and is a funk child, opt for incr pub */ + funk->child_tail_cidx = fd_funkier_txn_cidx( sibling_prev_idx ); + } else { /* No younger sib and has parent */ + txn_pool.ele[ parent_idx ].child_tail_cidx = fd_funkier_txn_cidx( sibling_prev_idx ); + } + } else { /* Has younger sib */ + txn_pool.ele[ sibling_next_idx ].sibling_prev_cidx = fd_funkier_txn_cidx( sibling_prev_idx ); + } + + fd_funkier_txn_map_query_t query[1]; + if( fd_funkier_txn_map_remove( &txn_map, fd_funkier_txn_xid( txn ), NULL, query, FD_MAP_FLAG_BLOCKING ) == FD_MAP_SUCCESS ) { + fd_funkier_txn_pool_release( &txn_pool, txn, 1 ); + } +} + +/* fd_funkier_txn_cancel_family cancels a transaction and all its + descendants in a tree-depth-first-ordered sense from youngest to + oldest. Callers have already validated our input arguments. Returns + the number of transactions canceled. */ + +static ulong +fd_funkier_txn_cancel_family( fd_funkier_t * funk, + ulong tag, + ulong txn_idx ) { + ulong cancel_cnt = 0UL; + + fd_wksp_t * wksp = fd_funkier_wksp ( funk ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + + ulong parent_stack_idx = FD_FUNKIER_TXN_IDX_NULL; + + for(;;) { + + fd_funkier_txn_t * txn = &txn_pool.ele[ txn_idx ]; + txn->tag = tag; + + ulong youngest_idx = fd_funkier_txn_idx( txn->child_tail_cidx ); + if( FD_LIKELY( fd_funkier_txn_idx_is_null( youngest_idx ) ) ) { /* txn is is childless, opt for incr pub */ + + fd_funkier_txn_cancel_childless( funk, txn_idx ); /* If this can fail, return cancel_cnt here on fail */ + cancel_cnt++; + + txn_idx = parent_stack_idx; /* Pop the parent stack */ + if( FD_LIKELY( fd_funkier_txn_idx_is_null( txn_idx ) ) ) break; /* If stack is empty, we are done, opt for incr pub */ + parent_stack_idx = fd_funkier_txn_idx( txn_pool.ele[ txn_idx ].stack_cidx ); + continue; + } + + /* txn has at least one child and the youngest is youngest_idx. Tag + the youngest child, push txn onto the parent stack and recurse + into the youngest child. */ + + txn->stack_cidx = fd_funkier_txn_cidx( parent_stack_idx ); + parent_stack_idx = txn_idx; + + txn_idx = youngest_idx; + } + + return cancel_cnt; +} + +ulong +fd_funkier_txn_cancel( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ) { + +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( !funk ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "NULL funk" )); + return 0UL; + } + if( FD_UNLIKELY( !fd_funkier_txn_valid( funk, txn ) ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "bad txn" )); + return 0UL; + } +#else + (void)verbose; +#endif + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + ulong txn_idx = (ulong)(txn - txn_pool.ele); + + return fd_funkier_txn_cancel_family( funk, funk->cycle_tag++, txn_idx ); +} + +/* fd_funkier_txn_oldest_sibling returns the index of the oldest sibling + in txn_idx's family. Callers have already validated our input + arguments. The caller should validate the return index. */ + +static inline ulong +fd_funkier_txn_oldest_sibling( fd_funkier_t * funk, + ulong txn_idx ) { + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + ulong parent_idx = fd_funkier_txn_idx( txn_pool.ele[ txn_idx ].parent_cidx ); + + if( FD_LIKELY( fd_funkier_txn_idx_is_null( parent_idx ) ) ) return fd_funkier_txn_idx( funk->child_head_cidx ); /* opt for incr pub */ + + return fd_funkier_txn_idx( txn_pool.ele[ parent_idx ].child_head_cidx ); +} + +/* fd_funkier_txn_cancel_sibling_list cancels siblings from sibling_idx down + to the youngest sibling inclusive in the order from youngest to + sibling_idx. Callers have already validated our input arguments + except sibling_idx. Returns the number of cancelled transactions + (should be at least 1). If any sibling is skip_idx, it will be not + be cancelled but still tagged as visited. Passing + FD_FUNKIER_TXN_IDX_NULL for skip_idx will cancel all siblings from + sibling_idx to the youngest inclusive. */ + +static ulong +fd_funkier_txn_cancel_sibling_list( fd_funkier_t * funk, + ulong tag, + ulong sibling_idx, + ulong skip_idx ) { + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + + ulong cancel_stack_idx = FD_FUNKIER_TXN_IDX_NULL; + + /* Push siblings_idx and its younger siblings inclusive (skipping + sibling skip_idx if encounter) onto the cancel stack from oldest to + youngest (such that we cancel youngest to oldest). */ + + for(;;) { + + /* At this point, sibling_idx is a sibling we might want to add to + the sibling stack. Validate and tag it. */ + + fd_funkier_txn_t * sibling = &txn_pool.ele[ sibling_idx ]; + sibling->tag = tag; + + if( FD_UNLIKELY( sibling_idx!=skip_idx ) ) { /* Not skip_idx so push onto the cancel stack, opt for non-compete */ + sibling->stack_cidx = fd_funkier_txn_cidx( cancel_stack_idx ); + cancel_stack_idx = sibling_idx; + } + + ulong younger_idx = fd_funkier_txn_idx( sibling->sibling_next_cidx ); + if( FD_LIKELY( fd_funkier_txn_idx_is_null( younger_idx ) ) ) break; /* opt for non-compete */ + sibling_idx = younger_idx; + + } + + /* Cancel all transactions and their descendants on the cancel stack */ + + ulong cancel_cnt = 0UL; + + while( !fd_funkier_txn_idx_is_null( cancel_stack_idx ) ) { /* TODO: peel first iter to make more predictable? */ + ulong sibling_idx = cancel_stack_idx; + cancel_stack_idx = fd_funkier_txn_idx( txn_pool.ele[ sibling_idx ].stack_cidx ); + cancel_cnt += fd_funkier_txn_cancel_family( funk, tag, sibling_idx ); + } + + return cancel_cnt; +} + +ulong +fd_funkier_txn_cancel_siblings( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ) { + +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( !funk ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "NULL funk" )); + return 0UL; + } + if( FD_UNLIKELY( !fd_funkier_txn_valid( funk, txn ) ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "bad txn" )); + return 0UL; + } +#else + (void)verbose; +#endif + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + ulong txn_idx = (ulong)(txn - txn_pool.ele); + + ulong oldest_idx = fd_funkier_txn_oldest_sibling( funk, txn_idx ); + + return fd_funkier_txn_cancel_sibling_list( funk, funk->cycle_tag++, oldest_idx, txn_idx ); +} + +ulong +fd_funkier_txn_cancel_children( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ) { + +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( !funk ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "NULL funk" )); + return 0UL; + } + if( FD_UNLIKELY( !fd_funkier_txn_valid( funk, txn ) ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "bad txn" )); + return 0UL; + } +#else + (void)verbose; +#endif + + ulong oldest_idx; + + if( FD_LIKELY( txn == NULL ) ) { + oldest_idx = fd_funkier_txn_idx( funk->child_head_cidx ); /* opt for non-compete */ + } else { + oldest_idx = fd_funkier_txn_idx( txn->child_head_cidx ); + } + + if( fd_funkier_txn_idx_is_null( oldest_idx ) ) { + return 0UL; + } + + return fd_funkier_txn_cancel_sibling_list( funk, funk->cycle_tag++, oldest_idx, FD_FUNKIER_TXN_IDX_NULL ); +} + +/* Cancel all outstanding transactions */ + +ulong +fd_funkier_txn_cancel_all( fd_funkier_t * funk, + int verbose ) { + return fd_funkier_txn_cancel_children( funk, NULL, verbose ); +} + +/* fd_funkier_txn_update applies the record updates in transaction txn_idx + to another transaction or the parent transaction. Callers have + already validated our input arguments. + + On entry, the head/tail of the destination records are at + *_dst_rec_head_idx / *_dst_rec_tail_idx. All transactions on this + list will have transaction id dst_xid and vice versa. That is, this + is the record list the last published transaction or txn_idx's + in-prep parent transaction. + + On exit, the head/tail of the updated records is at + *_dst_rec_head_idx / *_dst_rec_tail_idx. As before, all transactions + on this list will have transaction id dst_xid and vice versa. + Transaction txn_idx will have an _empty_ record list. + + Updates in the transaction txn_idx are processed from oldest to + youngest. If an update erases an existing record in dest, the record + to erase is removed from the destination records without perturbing + the order of remaining destination records. If an update is to + update an existing record, the destination record value is updated + and the order of the destination records is unchanged. If an update + is to create a new record, the record is appended to the list of + existing values as youngest without changing the order of existing + values. If an update erases a record in an in-prep parent, the + erasure will be moved into the parent as the youngest without + changing the order of existing values. */ + +static void +fd_funkier_txn_update( fd_funkier_t * funk, + ulong * _dst_rec_head_idx, /* Pointer to the dst list head */ + ulong * _dst_rec_tail_idx, /* Pointer to the dst list tail */ + ulong dst_txn_idx, /* Transaction index of the merge destination */ + fd_funkier_txn_xid_t const * dst_xid, /* dst xid */ + ulong txn_idx ) { /* Transaction index of the records to merge */ + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_alloc_t * alloc = fd_funkier_alloc( funk, wksp ); + fd_funkier_rec_map_t rec_map = fd_funkier_rec_map( funk, wksp ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( funk, wksp ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + + fd_funkier_txn_t * txn = &txn_pool.ele[ txn_idx ]; + ulong rec_idx = txn->rec_head_idx; + while( !fd_funkier_rec_idx_is_null( rec_idx ) ) { + fd_funkier_rec_t * rec = &rec_pool.ele[ rec_idx ]; + ulong next_rec_idx = rec->next_idx; + + /* See if (dst_xid,key) already exists. */ + fd_funkier_xid_key_pair_t pair[1]; + fd_funkier_xid_key_pair_init( pair, dst_xid, rec->pair.key ); + for(;;) { + fd_funkier_rec_map_query_t rec_query[1]; + int err = fd_funkier_rec_map_remove( &rec_map, pair, NULL, rec_query, FD_MAP_FLAG_BLOCKING ); + if( FD_UNLIKELY( err == FD_MAP_ERR_AGAIN ) ) continue; + if( err == FD_MAP_ERR_KEY ) break; + if( FD_UNLIKELY( err != FD_MAP_SUCCESS ) ) FD_LOG_CRIT(( "map corruption" )); + + /* Remove from the transaction */ + fd_funkier_rec_t * rec2 = fd_funkier_rec_map_query_ele( rec_query ); + ulong prev_idx = rec2->prev_idx; + ulong next_idx = rec2->next_idx; + if( fd_funkier_rec_idx_is_null( prev_idx ) ) { + *_dst_rec_head_idx = next_idx; + } else { + rec_pool.ele[ prev_idx ].next_idx = next_idx; + } + if( fd_funkier_rec_idx_is_null( next_idx ) ) { + *_dst_rec_tail_idx = prev_idx; + } else { + rec_pool.ele[ next_idx ].prev_idx = prev_idx; + } + /* Clean up value */ + fd_funkier_val_flush( rec2, alloc, wksp ); + rec2->txn_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + fd_funkier_rec_pool_release( &rec_pool, rec2, 1 ); + break; + } + + /* Add the new record to the transaction. We can update the xid in + place because it is not used for hashing the element. We have + to preserve the original element to preserve the + newest-to-oldest ordering in the hash + chain. fd_funkier_rec_query_global relies on this subtle + property. */ + + rec->pair.xid[0] = *dst_xid; + rec->txn_cidx = fd_funkier_txn_cidx( dst_txn_idx ); + + if( fd_funkier_rec_idx_is_null( *_dst_rec_head_idx ) ) { + *_dst_rec_head_idx = rec_idx; + rec->prev_idx = FD_FUNKIER_REC_IDX_NULL; + } else { + rec_pool.ele[ *_dst_rec_tail_idx ].next_idx = rec_idx; + rec->prev_idx = *_dst_rec_tail_idx; + } + *_dst_rec_tail_idx = rec_idx; + rec->next_idx = FD_FUNKIER_REC_IDX_NULL; + + rec_idx = next_rec_idx; + } + + txn_pool.ele[ txn_idx ].rec_head_idx = FD_FUNKIER_REC_IDX_NULL; + txn_pool.ele[ txn_idx ].rec_tail_idx = FD_FUNKIER_REC_IDX_NULL; +} + +/* fd_funkier_txn_publish_funk_child publishes a transaction that is known + to be a child of funk. Callers have already validated our input + arguments. Returns FD_FUNKIER_SUCCESS on success and an FD_FUNKIER_ERR_* + code on failure. (There are currently no failure cases but the + plumbing is there if value handling requires it at some point.) */ + +static int +fd_funkier_txn_publish_funk_child( fd_funkier_t * funk, + ulong tag, + ulong txn_idx ) { + + /* Apply the updates in txn to the last published transactions */ + + fd_funkier_txn_update( funk, &funk->rec_head_idx, &funk->rec_tail_idx, FD_FUNKIER_TXN_IDX_NULL, fd_funkier_root( funk ), txn_idx ); + + /* Cancel all competing transaction histories */ + + ulong oldest_idx = fd_funkier_txn_oldest_sibling( funk, txn_idx ); + fd_funkier_txn_cancel_sibling_list( funk, tag, oldest_idx, txn_idx ); + + /* Make all the children children of funk */ + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( funk, wksp ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + + ulong child_head_idx = fd_funkier_txn_idx( txn_pool.ele[ txn_idx ].child_head_cidx ); + ulong child_tail_idx = fd_funkier_txn_idx( txn_pool.ele[ txn_idx ].child_tail_cidx ); + + ulong child_idx = child_head_idx; + while( FD_UNLIKELY( !fd_funkier_txn_idx_is_null( child_idx ) ) ) { /* opt for incr pub */ + fd_funkier_txn_t * txn = &txn_pool.ele[ child_idx ]; + txn->tag = tag; + txn->parent_cidx = fd_funkier_txn_cidx( FD_FUNKIER_TXN_IDX_NULL ); + child_idx = fd_funkier_txn_idx( txn->sibling_next_cidx ); + } + + funk->child_head_cidx = fd_funkier_txn_cidx( child_head_idx ); + funk->child_tail_cidx = fd_funkier_txn_cidx( child_tail_idx ); + + fd_funkier_txn_xid_copy( funk->last_publish, fd_funkier_txn_xid( &txn_pool.ele[ txn_idx ] ) ); + + /* Remove the mapping */ + + fd_funkier_txn_map_query_t query[1]; + if( fd_funkier_txn_map_remove( &txn_map, funk->last_publish, NULL, query, FD_MAP_FLAG_BLOCKING ) == FD_MAP_SUCCESS ) { + fd_funkier_txn_pool_release( &txn_pool, &txn_pool.ele[ txn_idx ], 1 ); + } + + return FD_FUNKIER_SUCCESS; +} + +ulong +fd_funkier_txn_publish( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ) { + +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( !funk ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "NULL funk" )); + return 0UL; + } + if( FD_UNLIKELY( !fd_funkier_txn_valid( funk, txn ) ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "bad txn" )); + return 0UL; + } +#else + (void)verbose; +#endif + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + ulong txn_idx = (ulong)(txn - txn_pool.ele); + + ulong tag = funk->cycle_tag++; + + ulong publish_stack_idx = FD_FUNKIER_TXN_IDX_NULL; + + for(;;) { + fd_funkier_txn_t * txn2 = &txn_pool.ele[ txn_idx ]; + txn2->tag = tag; + + /* At this point, txn_idx is a transaction that needs to be + published and has been tagged. If txn is a child of funk, we are + ready to publish txn and everything on the publish stack. */ + + ulong parent_idx = fd_funkier_txn_idx( txn2->parent_cidx ); + if( FD_LIKELY( fd_funkier_txn_idx_is_null( parent_idx ) ) ) break; /* opt for incr pub */ + + /* txn_idx has a parent. Validate and tag it. Push txn to the + publish stack and recurse into the parent. */ + + txn2->stack_cidx = fd_funkier_txn_cidx( publish_stack_idx ); + publish_stack_idx = txn_idx; + + txn_idx = parent_idx; + } + + ulong publish_cnt = 0UL; + + for(;;) { + + /* At this point, all the transactions we need to publish are + tagged, txn is the next up publish funk and publish_stack has the + transactions to follow in order by pop. We use a new tag for + each publish as txn and its siblings we potentially visited in a + previous iteration of this loop. */ + + if( FD_UNLIKELY( fd_funkier_txn_publish_funk_child( funk, funk->cycle_tag++, txn_idx ) ) ) break; + publish_cnt++; + + txn_idx = publish_stack_idx; + if( FD_LIKELY( fd_funkier_txn_idx_is_null( txn_idx ) ) ) break; /* opt for incr pub */ + publish_stack_idx = fd_funkier_txn_idx( txn_pool.ele[ txn_idx ].stack_cidx ); + } + + return publish_cnt; +} + +int +fd_funkier_txn_publish_into_parent( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ) { +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( !funk ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "NULL funk" )); + return FD_FUNKIER_ERR_INVAL; + } + if( FD_UNLIKELY( !fd_funkier_txn_valid( funk, txn ) ) ) { + if( FD_UNLIKELY( verbose ) ) FD_LOG_WARNING(( "bad txn" )); + return 0UL; + } +#else + (void)verbose; +#endif + + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( funk, fd_funkier_wksp( funk ) ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + ulong txn_idx = (ulong)(txn - txn_pool.ele); + + ulong oldest_idx = fd_funkier_txn_oldest_sibling( funk, txn_idx ); + fd_funkier_txn_cancel_sibling_list( funk, funk->cycle_tag++, oldest_idx, txn_idx ); + + ulong parent_idx = fd_funkier_txn_idx( txn->parent_cidx ); + if( fd_funkier_txn_idx_is_null( parent_idx ) ) { + /* Publish to root */ + fd_funkier_txn_update( funk, &funk->rec_head_idx, &funk->rec_tail_idx, FD_FUNKIER_TXN_IDX_NULL, fd_funkier_root( funk ), txn_idx ); + /* Inherit the children */ + funk->child_head_cidx = txn->child_head_cidx; + funk->child_tail_cidx = txn->child_tail_cidx; + } else { + fd_funkier_txn_t * parent_txn = &txn_pool.ele[ parent_idx ]; + fd_funkier_txn_update( funk, &parent_txn->rec_head_idx, &parent_txn->rec_tail_idx, parent_idx, &parent_txn->xid, txn_idx ); + /* Inherit the children */ + parent_txn->child_head_cidx = txn->child_head_cidx; + parent_txn->child_tail_cidx = txn->child_tail_cidx; + } + + /* Adjust the parent pointers of the children to point to their grandparent */ + ulong child_idx = fd_funkier_txn_idx( txn->child_head_cidx ); + while( FD_UNLIKELY( !fd_funkier_txn_idx_is_null( child_idx ) ) ) { + txn_pool.ele[ child_idx ].parent_cidx = fd_funkier_txn_cidx( parent_idx ); + child_idx = fd_funkier_txn_idx( txn_pool.ele[ child_idx ].sibling_next_cidx ); + } + + fd_funkier_txn_map_query_t query[1]; + if( fd_funkier_txn_map_remove( &txn_map, fd_funkier_txn_xid( txn ), NULL, query, FD_MAP_FLAG_BLOCKING ) == FD_MAP_SUCCESS ) { + fd_funkier_txn_pool_release( &txn_pool, txn, 1 ); + } + + return FD_FUNKIER_SUCCESS; +} + +/* Return the first record in a transaction. Returns NULL if the + transaction has no records yet. */ + +FD_FN_PURE fd_funkier_rec_t const * +fd_funkier_txn_first_rec( fd_funkier_t * funk, + fd_funkier_txn_t const * txn ) { + ulong rec_idx; + if( FD_UNLIKELY( NULL == txn )) + rec_idx = funk->rec_head_idx; + else + rec_idx = txn->rec_head_idx; + if( fd_funkier_rec_idx_is_null( rec_idx ) ) return NULL; + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( funk, wksp ); + return rec_pool.ele + rec_idx; +} + +FD_FN_PURE fd_funkier_rec_t const * +fd_funkier_txn_last_rec( fd_funkier_t * funk, + fd_funkier_txn_t const * txn ) { + ulong rec_idx; + if( FD_UNLIKELY( NULL == txn )) + rec_idx = funk->rec_tail_idx; + else + rec_idx = txn->rec_tail_idx; + if( fd_funkier_rec_idx_is_null( rec_idx ) ) return NULL; + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( funk, wksp ); + return rec_pool.ele + rec_idx; +} + +/* Return the next record in a transaction. Returns NULL if the + transaction has no more records. */ + +FD_FN_PURE fd_funkier_rec_t const * +fd_funkier_txn_next_rec( fd_funkier_t * funk, + fd_funkier_rec_t const * rec ) { + ulong rec_idx = rec->next_idx; + if( fd_funkier_rec_idx_is_null( rec_idx ) ) return NULL; + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( funk, wksp ); + return rec_pool.ele + rec_idx; +} + +FD_FN_PURE fd_funkier_rec_t const * +fd_funkier_txn_prev_rec( fd_funkier_t * funk, + fd_funkier_rec_t const * rec ) { + ulong rec_idx = rec->prev_idx; + if( fd_funkier_rec_idx_is_null( rec_idx ) ) return NULL; + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_rec_pool_t rec_pool = fd_funkier_rec_pool( funk, wksp ); + return rec_pool.ele + rec_idx; +} + +fd_funkier_txn_xid_t +fd_funkier_generate_xid(void) { + fd_funkier_txn_xid_t xid; + static FD_TL ulong seq = 0; + xid.ul[0] = + (fd_log_cpu_id() + 1U)*3138831853UL + + (fd_log_thread_id() + 1U)*9180195821UL + + (++seq)*6208101967UL; + xid.ul[1] = ((ulong)fd_tickcount())*2810745731UL; + return xid; +} + +static void +fd_funkier_txn_all_iter_skip_nulls( fd_funkier_txn_all_iter_t * iter ) { + if( iter->chain_idx == iter->chain_cnt ) return; + while( fd_funkier_txn_map_iter_done( iter->txn_map_iter ) ) { + if( ++(iter->chain_idx) == iter->chain_cnt ) break; + iter->txn_map_iter = fd_funkier_txn_map_iter( &iter->txn_map, iter->chain_idx ); + } +} + +void +fd_funkier_txn_all_iter_new( fd_funkier_t * funk, fd_funkier_txn_all_iter_t * iter ) { + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + iter->txn_map = fd_funkier_txn_map( funk, wksp ); + iter->chain_cnt = fd_funkier_txn_map_chain_cnt( &iter->txn_map ); + iter->chain_idx = 0; + iter->txn_map_iter = fd_funkier_txn_map_iter( &iter->txn_map, 0 ); + fd_funkier_txn_all_iter_skip_nulls( iter ); +} + +int +fd_funkier_txn_all_iter_done( fd_funkier_txn_all_iter_t * iter ) { + return ( iter->chain_idx == iter->chain_cnt ); +} + +void +fd_funkier_txn_all_iter_next( fd_funkier_txn_all_iter_t * iter ) { + iter->txn_map_iter = fd_funkier_txn_map_iter_next( iter->txn_map_iter ); + fd_funkier_txn_all_iter_skip_nulls( iter ); +} + +fd_funkier_txn_t const * +fd_funkier_txn_all_iter_ele_const( fd_funkier_txn_all_iter_t * iter ) { + return fd_funkier_txn_map_iter_ele_const( iter->txn_map_iter ); +} + +fd_funkier_txn_t * +fd_funkier_txn_all_iter_ele( fd_funkier_txn_all_iter_t * iter ) { + return fd_funkier_txn_map_iter_ele( iter->txn_map_iter ); +} + +#ifdef FD_FUNKIER_HANDHOLDING +int +fd_funkier_txn_verify( fd_funkier_t * funk ) { + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( funk, wksp ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( funk, wksp ); + ulong txn_max = funk->txn_max; + + ulong funk_child_head_idx = fd_funkier_txn_idx( funk->child_head_cidx ); /* Previously verified */ + ulong funk_child_tail_idx = fd_funkier_txn_idx( funk->child_tail_cidx ); /* Previously verified */ + + fd_funkier_txn_xid_t const * last_publish = funk->last_publish; /* Previously verified */ + +# define TEST(c) do { \ + if( FD_UNLIKELY( !(c) ) ) { FD_LOG_WARNING(( "FAIL: %s", #c )); return FD_FUNKIER_ERR_INVAL; } \ + } while(0) + +# define IS_VALID( idx ) ( (idx==FD_FUNKIER_TXN_IDX_NULL) || \ + ((idx= funk->txn_max || txn != txn_idx + txn_pool.ele ) return 0; + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( funk, wksp ); + fd_funkier_txn_map_query_t query[1]; + if( FD_UNLIKELY( fd_funkier_txn_map_query_try( &txn_map, &txn->xid, NULL, query ) ) ) return 0; + if( fd_funkier_txn_map_query_ele( query ) != txn ) return 0; + return 1; +} + +#endif diff --git a/src/funkier/fd_funkier_txn.h b/src/funkier/fd_funkier_txn.h new file mode 100644 index 0000000000..4f425326af --- /dev/null +++ b/src/funkier/fd_funkier_txn.h @@ -0,0 +1,470 @@ +#ifndef HEADER_fd_src_funk_fd_funkier_txn_h +#define HEADER_fd_src_funk_fd_funkier_txn_h + +/* This provides APIs for managing forks (preparing, publishing and + cancelling funk transactions). It is generally not meant to be + included directly. Use fd_funkier.h instead. */ + +#include "fd_funkier_base.h" + +/* FD_FUNKIER_TXN_{ALIGN,FOOTPRINT} describe the alignment and footprint of + a fd_funkier_txn_t. ALIGN will be a power of 2, footprint will be a + multiple of align. These are provided to facilitate compile time + declarations. */ + +#define FD_FUNKIER_TXN_ALIGN (32UL) +#define FD_FUNKIER_TXN_FOOTPRINT (96UL) + +/* FD_FUNKIER_TXN_IDX_NULL gives the map transaction idx value used to + represent NULL. It also is the maximum value for txn_max in a funk + to support index compression. (E.g. could use ushort / USHORT_MAX + to use more aggressive compression or ulong / ULONG_MAX to disable + index compression.) */ + +#define FD_FUNKIER_TXN_IDX_NULL ((ulong)UINT_MAX) + +/* A fd_funkier_txn_t is an opaque handle of an in-preparation funk + transaction. The details are exposed here to facilitate inlining + various operations. */ + +struct __attribute__((aligned(FD_FUNKIER_TXN_ALIGN))) fd_funkier_txn_private { + + /* These fields are managed by the funk's txn_map */ + + fd_funkier_txn_xid_t xid; /* Transaction id, at a minimum, unique among all in-prepare and the last published transaction, + ideally globally unique */ + ulong map_next; /* Internal use by map */ + ulong map_hash; /* Internal use by map */ + + /* These fields are managed by the funk */ + + uint parent_cidx; /* compr map index of the in-prep parent txn, compr FD_FUNKIER_TXN_IDX_NULL if a funk child */ + uint child_head_cidx; /* " oldest child " childless */ + uint child_tail_cidx; /* " youngest child childless */ + uint sibling_prev_cidx; /* " older sibling oldest sibling */ + uint sibling_next_cidx; /* " younger sibling youngest sibling */ + uint stack_cidx; /* Internal use by funk */ + ulong tag; /* Internal use by funk */ + + ulong rec_head_idx; /* Record map index of the first record, FD_FUNKIER_REC_IDX_NULL if none (from oldest to youngest) */ + ulong rec_tail_idx; /* " last " */ +}; + +typedef struct fd_funkier_txn_private fd_funkier_txn_t; + +/* fd_funkier_txn_map allows for indexing transactions by their xid */ + +#define POOL_NAME fd_funkier_txn_pool +#define POOL_ELE_T fd_funkier_txn_t +#define POOL_IDX_T uint +#define POOL_NEXT map_next +#define POOL_IMPL_STYLE 1 +#include "../util/tmpl/fd_pool_para.c" + +#define MAP_NAME fd_funkier_txn_map +#define MAP_ELE_T fd_funkier_txn_t +#define MAP_KEY_T fd_funkier_txn_xid_t +#define MAP_KEY xid +#define MAP_KEY_EQ(k0,k1) fd_funkier_txn_xid_eq((k0),(k1)) +#define MAP_KEY_HASH(k0,seed) fd_funkier_txn_xid_hash((k0),(seed)) +#define MAP_NEXT map_next +#define MAP_HASH map_hash +#define MAP_MAGIC (0xf173da2ce7172db0UL) /* Firedancer txn db version 0 */ +#define MAP_IMPL_STYLE 1 +#include "../util/tmpl/fd_map_para.c" +#undef MAP_HASH + +FD_PROTOTYPES_BEGIN + +/* fd_funkier_txn_{cidx,idx} convert between an index and a compressed index. */ + +static inline uint fd_funkier_txn_cidx( ulong idx ) { return (uint) idx; } +static inline ulong fd_funkier_txn_idx ( uint idx ) { return (ulong)idx; } + +/* fd_funkier_txn_idx_is_null returns 1 if idx is FD_FUNKIER_TXN_IDX_NULL and + 0 otherwise. */ + +static inline int fd_funkier_txn_idx_is_null( ulong idx ) { return idx==FD_FUNKIER_TXN_IDX_NULL; } + +/* Generate a globally unique pseudo-random xid */ +fd_funkier_txn_xid_t fd_funkier_generate_xid(void); + +/* Accessors */ + +/* fd_funkier_txn_query returns a pointer to an in-preparation transaction + whose id is pointed to by xid. Returns NULL if xid is not an + in-preparation transaction. Assumes funk is a current local join, + map==fd_funkier_txn_map( funk, fd_funkier_wksp( funk ) ), xid points to a + transaction id in the caller's address space and there are no + concurrent operations on funk or xid. Retains no interest in xid. + + The returned pointer is in the caller's address space and, if the + return is non-NULL, the lifetime of the returned pointer is the + lesser of the funk join or the transaction is published or canceled + (either directly or indirectly via publish of a descendant, publish + of a competing transaction history or cancel of an ancestor). */ + +FD_FN_PURE static inline fd_funkier_txn_t * +fd_funkier_txn_query( fd_funkier_txn_xid_t const * xid, + fd_funkier_txn_map_t * map ) { + fd_funkier_txn_map_query_t query[1]; + do { + if( FD_UNLIKELY( fd_funkier_txn_map_query_try( map, xid, NULL, query ) ) ) return NULL; + fd_funkier_txn_t * ele = fd_funkier_txn_map_query_ele( query ); + if( FD_LIKELY( !fd_funkier_txn_map_query_test( query ) ) ) return ele; + } while( 1 ); +} + +/* fd_funkier_txn_xid returns a pointer in the local address space of the + ID of an in-preparation transaction. Assumes txn points to an + in-preparation transaction in the caller's address space. The + lifetime of the returned pointer is the same as the txn's pointer + lifetime. The value at the pointer will be stable for the lifetime + of the returned pointer. */ + +FD_FN_CONST static inline fd_funkier_txn_xid_t const * fd_funkier_txn_xid( fd_funkier_txn_t const * txn ) { return &txn->xid; } + +/* fd_funkier_txn_{parent,child_head,child_tail,sibling_prev,sibling_next} + return a pointer in the caller's address space to the corresponding + relative in-preparation transaction of in-preparation transaction + txn. + + Specifically: + + - parent is the parent transaction. NULL if txn is a child of funk. + - child_head is txn's oldest child. NULL if txn has no children. + - child_tail is txn's youngest child. NULL if txn has no children. + - sibling_prev is txn's closest older sibling. NULL if txn is the + oldest sibling. + - sibling_next is txn's closest younger sibling. NULL if txn is the + youngest sibling. + + E.g. child_head==NULL indicates txn has no children. + child_head!=NULL and child_head==child_tail indicates txn has one + child. child_head!=NULL and child_tail!=NULL and + child_head!=child_tail indicates txn has many children. + sibling_prev==sibling_next==NULL indicate no siblings / locally + competing transactions to txn. If the txn and all its ancestors have + no siblings, there are no transaction histories competing with txn + globally. */ + +#define FD_FUNKIER_ACCESSOR(field) \ +FD_FN_PURE static inline fd_funkier_txn_t * \ +fd_funkier_txn_##field( fd_funkier_txn_t const * txn, \ + fd_funkier_txn_pool_t * pool ) { \ + ulong idx = fd_funkier_txn_idx( txn->field##_cidx ); \ + if( idx==FD_FUNKIER_TXN_IDX_NULL ) return NULL; \ + return pool->ele + idx; \ +} + +FD_FUNKIER_ACCESSOR( parent ) +FD_FUNKIER_ACCESSOR( child_head ) +FD_FUNKIER_ACCESSOR( child_tail ) +FD_FUNKIER_ACCESSOR( sibling_prev ) +FD_FUNKIER_ACCESSOR( sibling_next ) + +#undef FD_FUNKIER_ACCESSOR + +/* fd_funkier_txn_frozen returns 1 if the in-preparation transaction is + frozen (i.e. has children) and 0 otherwise (i.e. has no children). + Assumes txn points to an in-preparation transaction in the caller's + address space. */ + +FD_FN_PURE static inline int +fd_funkier_txn_is_frozen( fd_funkier_txn_t const * txn ) { + return !fd_funkier_txn_idx_is_null( fd_funkier_txn_idx( txn->child_head_cidx ) ); +} + +/* fd_funkier_txn_is_only_child returns 1 if the in-preparation transaction + txn if is any only child and 0 if it has one or more siblings. + Assumes txn points to an in-preparation transaction in the caller's + address space. */ + +FD_FN_PURE static inline int +fd_funkier_txn_is_only_child( fd_funkier_txn_t const * txn ) { + return ( fd_funkier_txn_idx_is_null( fd_funkier_txn_idx( txn->sibling_prev_cidx ) ) ) & + ( fd_funkier_txn_idx_is_null( fd_funkier_txn_idx( txn->sibling_next_cidx ) ) ); +} + +typedef struct fd_funkier_rec fd_funkier_rec_t; + +/* Return the first (oldest) record in a transaction. Returns NULL if the + transaction has no records yet. */ + +FD_FN_PURE fd_funkier_rec_t const * +fd_funkier_txn_first_rec( fd_funkier_t * funk, + fd_funkier_txn_t const * txn ); + +/* Return the last (newest) record in a transaction. Returns NULL if the + transaction has no records yet. */ + +FD_FN_PURE fd_funkier_rec_t const * +fd_funkier_txn_last_rec( fd_funkier_t * funk, + fd_funkier_txn_t const * txn ); + +/* Return the next record in a transaction. Returns NULL if the + transaction has no more records. */ + +FD_FN_PURE fd_funkier_rec_t const * +fd_funkier_txn_next_rec( fd_funkier_t * funk, + fd_funkier_rec_t const * rec ); + +/* Return the previous record in a transaction. Returns NULL if the + transaction has no more records. */ + +FD_FN_PURE fd_funkier_rec_t const * +fd_funkier_txn_prev_rec( fd_funkier_t * funk, + fd_funkier_rec_t const * rec ); + +/* Operations */ + +/* fd_funkier_txn_ancestor returns a pointer in the caller's address space + to the youngest transaction among in-preparation transaction txn and + its ancestors that currently has siblings. Returns NULL if all + transactions back to the root transaction have no siblings (e.g. + there are no competing transaction histories and thus publishing + transaction txn will not require canceling any other competing + transactions). This is a reasonably fast O(length of ancestor + history) time (theoretical minimum) and a reasonably small O(1) space + (theoretical minimum). This is not fortified against transaction map + data corruption. + + fd_funkier_txn_descendant returns a pointer in the caller's address + space to the first transaction among txn and its youngest direct + descendant inclusive that currently either has no children or has + multiple children. Returns NULL if txn is not an only child. This + is a reasonably fast O(length of descendant history) time + (theoretical minimum) and a reasonably small O(1) space (theoretical + minimum). This is not fortified against transaction map data + corruption. + + That is, if txn's ancestor is NULL, all transactions up to and + including txn's descendant (which will be non-NULL) can be published + without cancelling any competing transactions. Further, if the + descendant has a child, it has multiple children. And if has no + children, all transactions in preparation are linear from the root to + the descendant. + + In code: + + if( !fd_funkier_txn_ancestor( txn, map ) ) + fd_funkier_publish( funk, fd_funkier_txn_descendant( funk, map ) ); + + would publish as much currently uncontested transaction history as + possible around txn. */ + +FD_FN_PURE static inline fd_funkier_txn_t * +fd_funkier_txn_ancestor( fd_funkier_txn_t * txn, + fd_funkier_txn_pool_t * pool ) { + for(;;) { + if( !fd_funkier_txn_is_only_child( txn ) ) break; + fd_funkier_txn_t * parent = fd_funkier_txn_parent( txn, pool ); + if( !parent ) return NULL; + txn = parent; + } + return txn; +} + +FD_FN_PURE static inline fd_funkier_txn_t * +fd_funkier_txn_descendant( fd_funkier_txn_t * txn, + fd_funkier_txn_pool_t * pool ) { + if( !fd_funkier_txn_is_only_child( txn ) ) return NULL; + for(;;) { /* txn is an only child at this point */ + fd_funkier_txn_t * child = fd_funkier_txn_child_head( txn, pool ); + if( !child || !fd_funkier_txn_is_only_child( child ) ) break; + txn = child; + } + return txn; +} + +/* IMPORTANT SAFETY TIP! **********************************************/ + +/* fd_funkier_txn_{prepare,publish,cancel,cancel_siblings,cancel_children} + are the pointy end of the stick practically. As such, these are + fortified against transaction map memory corruption. Since any such + corruption would be prima facie evidence of a hardware fault, + security compromise or software bug, these will FD_LOG_CRIT if + corruption is detected to contain the blast radius and get as much + ultra detailed diagnostic information to user (stack backtrace and + core) as possible. (This behavior is straightforward to disable in + fd_funkier_txn.c.) */ + +/**********************************************************************/ + +/* fd_funkier_txn_prepare starts preparation of a transaction. The + transaction will be a child of the in-preparation transaction pointed + to by parent. A NULL parent means the transaction should be a child + of funk. xid points to transaction id that should be used for the + transaction. This id must be unique over all in-preparation + transactions, the root transaction and the last published + transaction. It is strongly recommended to use globally unique ids + when possible. Returns a pointer in the caller's address space to + the in-preparation transaction on success and NULL on failure. The + lifetime of the returned pointer is as described in + fd_funkier_txn_query. + + At start of preparation, the records in the txn are a virtual clone of the + records in its parent transaction. The funk records can be modified + when the funk has no children. Similarly, the records of an + in-preparation transaction can be freely modified when it has + no children. + + Assumes funk is a current local join. Reasons for failure include + funk is NULL, the funk's transaction map is full, the parent is + neither NULL nor points to an in-preparation funk transaction, xid is + NULL, the requested xid is in use (i.e. the last published or matches + another in-preparation transaction). If verbose is non-zero, these + will FD_LOG_WARNING details about the reason for failure. + + This is a reasonably fast O(1) time (theoretical minimum), reasonably + small O(1) space (theoretical minimum), does no allocation, does no + system calls, and produces no garbage to collect (at this layer at + least). That is, we can scalably track forks until we run out of + resources allocated to the funk. */ + +fd_funkier_txn_t * +fd_funkier_txn_prepare( fd_funkier_t * funk, + fd_funkier_txn_t * parent, + fd_funkier_txn_xid_t const * xid, + int verbose ); + +/* fd_funkier_txn_is_full returns true if the transaction map is + full. No more in-preparation transactions are allowed. */ + +int +fd_funkier_txn_is_full( fd_funkier_t * funk ); + +/* fd_funkier_txn_cancel cancels in-preparation transaction txn and any of + its in-preparation descendants. On success, returns the number of + transactions cancelled and 0 on failure. The state of records in the + cancelled transactions will be lost and all resources used under the + hood are available for reuse. If this makes the txn's parent + childless, this will unfreeze the parent. + + fd_funkier_txn_cancel_siblings cancels txn's siblings and their + descendants. + + fd_funkier_txn_cancel_children cancels txn's children and their + descendants. If txn is NULL, all children of funk will be cancelled + (such that the number of transactions in preparation afterward will + be zero). + + Cancellations proceed from youngest to oldest in a tree depth first + sense. + + Assumes funk is a current local join. Reasons for failure include + NULL funk or txn does not point to an in-preparation funk + transaction. If verbose is non-zero, these will FD_LOG_WARNING level + details about the reason for failure. + + These are a reasonably fast O(number of cancelled transactions) time + (the theoretical minimum), reasonably small O(1) space (the + theoretical minimum), does no allocation, does no system calls, and + produces no garbage to collect (at this layer at least). That is, we + can scalably track forks until we run out of resources allocated to + the funk. */ + +ulong +fd_funkier_txn_cancel( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ); + +ulong +fd_funkier_txn_cancel_siblings( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ); + +ulong +fd_funkier_txn_cancel_children( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ); + +/* fd_funkier_txn_cancel_all cancels all in-preparation + transactions. Only the last published transaction remains. */ + +ulong +fd_funkier_txn_cancel_all( fd_funkier_t * funk, + int verbose ); + +/* fd_funkier_txn_publish publishes in-preparation transaction txn and any + of txn's in-preparation ancestors. Returns the number of + transactions published. Any competing histories to this chain will + be cancelled. + + Assumes funk is a current local join. Reasons for failure include + NULL funk, txn does not point to an in-preparation funk transaction. + If verbose is non-zero, these will FD_LOG_WARNING the details about + the reason for failure. + + This is a reasonably fast O(number of published transactions) + + O(number of cancelled transactions) time (theoretical minimum), + reasonably small O(1) space (theoretical minimum), does no allocation + does no system calls, and produces no garbage to collect (at this + layer at least). That is, we can scalably track forks until we run + out of resources allocated to the funk. */ + +ulong +fd_funkier_txn_publish( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ); + +/* This version of publish just combines the transaction with its + immediate parent. Ancestors will remain unpublished. Any competing + histories (siblings of the given transaction) are still cancelled. + + Returns FD_FUNKIER_SUCCESS on success or an error code on failure. */ +int +fd_funkier_txn_publish_into_parent( fd_funkier_t * funk, + fd_funkier_txn_t * txn, + int verbose ); + +/* Iterator which walks all in-preparation transactions. Usage is: + + fd_funkier_txn_all_iter_t txn_iter[1]; + for( fd_funkier_txn_all_iter_new( funk, txn_iter ); !fd_funkier_txn_all_iter_done( txn_iter ); fd_funkier_txn_all_iter_next( txn_iter ) ) { + fd_funkier_txn_t const * txn = fd_funkier_txn_all_iter_ele_const( txn_iter ); + ... + } +*/ + +struct fd_funkier_txn_all_iter { + fd_funkier_txn_map_t txn_map; + ulong chain_cnt; + ulong chain_idx; + fd_funkier_txn_map_iter_t txn_map_iter; +}; + +typedef struct fd_funkier_txn_all_iter fd_funkier_txn_all_iter_t; + +void fd_funkier_txn_all_iter_new( fd_funkier_t * funk, fd_funkier_txn_all_iter_t * iter ); + +int fd_funkier_txn_all_iter_done( fd_funkier_txn_all_iter_t * iter ); + +void fd_funkier_txn_all_iter_next( fd_funkier_txn_all_iter_t * iter ); + +fd_funkier_txn_t const * fd_funkier_txn_all_iter_ele_const( fd_funkier_txn_all_iter_t * iter ); +fd_funkier_txn_t * fd_funkier_txn_all_iter_ele( fd_funkier_txn_all_iter_t * iter ); + +/* Misc */ + +#ifdef FD_FUNKIER_HANDHOLDING + +/* fd_funkier_txn_verify verifies a transaction map. Returns + FD_FUNKIER_SUCCESS if the transaction map appears intact and + FD_FUNKIER_ERR_INVAL if not (logs details). Meant to be called as part + of fd_funkier_verify. */ + +int +fd_funkier_txn_verify( fd_funkier_t * funk ); + +/* fd_funkier_txn_valid returns true if txn appears to be a pointer to + a valid in-prep transaction. */ + +int +fd_funkier_txn_valid( fd_funkier_t * funk, fd_funkier_txn_t const * txn ); + +#endif + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_funk_fd_funkier_txn_h */ diff --git a/src/funkier/fd_funkier_val.c b/src/funkier/fd_funkier_val.c new file mode 100644 index 0000000000..fa89cdac39 --- /dev/null +++ b/src/funkier/fd_funkier_val.c @@ -0,0 +1,119 @@ +#include "fd_funkier.h" + +void * +fd_funkier_val_truncate( fd_funkier_rec_t * rec, + ulong new_val_sz, + fd_alloc_t * alloc, + fd_wksp_t * wksp, + int * opt_err ) { + + /* Check input args */ + +#ifdef FD_FUNKIER_HANDHOLDING + if( FD_UNLIKELY( (!rec) | (new_val_sz>FD_FUNKIER_REC_VAL_MAX) | (!alloc) | (!wksp) ) || /* NULL rec,too big,NULL alloc,NULL wksp */ + FD_UNLIKELY( rec->flags & FD_FUNKIER_REC_FLAG_ERASE ) ) { /* Marked erase */ + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_ERR_INVAL ); + return NULL; + } +#endif + + ulong val_sz = (ulong)rec->val_sz; + ulong val_max = (ulong)rec->val_max; + + if( FD_UNLIKELY( !new_val_sz ) ) { + + /* User asked to truncate to 0. Flush the any existing value. */ + + fd_funkier_val_flush( rec, alloc, wksp ); + + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_SUCCESS ); + return NULL; + + } else if( FD_LIKELY( new_val_sz > val_max ) ) { + + /* User requested to increase the value size. We presume they are + asking for a specific size (as opposed to bumping up the size ala + append) so we don't build in extra padding to amortize the cost + of future truncates. Note that new_val_sz is at least 1 at this + point but val_sz / val_gaddr could be zero / zero. */ + + ulong val_gaddr = rec->val_gaddr; + uchar * val = val_max ? fd_wksp_laddr_fast( wksp, val_gaddr ) : NULL; /* TODO: branchless */ + + ulong new_val_max; + uchar * new_val = (uchar *)fd_alloc_malloc_at_least( alloc, FD_FUNKIER_VAL_ALIGN, new_val_sz, &new_val_max ); + if( FD_UNLIKELY( !new_val ) ) { /* Allocation failure! */ + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_ERR_MEM ); + return NULL; + } + + if( val_sz ) fd_memcpy( new_val, val, val_sz ); /* Copy the existing value */ + fd_memset( new_val + val_sz, 0, new_val_max - val_sz ); /* Clear out trailing padding to be on the safe side */ + + rec->val_gaddr = fd_wksp_gaddr_fast( wksp, new_val ); + rec->val_sz = (uint)new_val_sz; + rec->val_max = (uint)fd_ulong_min( new_val_max, FD_FUNKIER_REC_VAL_MAX ); + + if( val ) fd_alloc_free( alloc, val ); /* Free the old value (if any) */ + + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_SUCCESS ); + return new_val; + + } else { + + /* Just set the new size */ + + rec->val_sz = (uint)new_val_sz; + + fd_int_store_if( !!opt_err, opt_err, FD_FUNKIER_SUCCESS ); + return (uchar *)fd_wksp_laddr_fast( wksp, rec->val_gaddr ); + + } +} + +#ifdef FD_FUNKIER_HANDHOLDING +int +fd_funkier_val_verify( fd_funkier_t * funk ) { + fd_wksp_t * wksp = fd_funkier_wksp( funk ); + ulong wksp_tag = funk->wksp_tag; + + /* At this point, rec_map has been extensively verified */ + +# define TEST(c) do { \ + if( FD_UNLIKELY( !(c) ) ) { FD_LOG_WARNING(( "FAIL: %s", #c )); return FD_FUNKIER_ERR_INVAL; } \ + } while(0) + + /* Iterate over all records in use */ + + fd_funkier_all_iter_t iter[1]; + for( fd_funkier_all_iter_new( funk, iter ); !fd_funkier_all_iter_done( iter ); fd_funkier_all_iter_next( iter ) ) { + fd_funkier_rec_t const * rec = fd_funkier_all_iter_ele_const( iter ); + + /* Make sure values look sane */ + /* TODO: consider doing an alias analysis on allocated values? + (tricky to do algo efficient in place) */ + + ulong val_sz = (ulong)rec->val_sz; + ulong val_max = (ulong)rec->val_max; + ulong val_gaddr = rec->val_gaddr; + + TEST( val_sz<=val_max ); + + if( rec->flags & FD_FUNKIER_REC_FLAG_ERASE ) { + TEST( !val_max ); + TEST( !val_gaddr ); + } else { + TEST( val_max<=FD_FUNKIER_REC_VAL_MAX ); + if( !val_gaddr ) TEST( !val_max ); + else { + TEST( (0ULval_sz; /* Covers the marked ERASE case too */ +} + +FD_FN_PURE static inline ulong /* Current size of the record's value allocation in bytes */ +fd_funkier_val_max( fd_funkier_rec_t const * rec ) { /* Assumes pointer in caller's address space to a live funk record */ + return (ulong)rec->val_max; /* Covers the marked ERASE case too */ +} + +/* fd_funkier_val returns a pointer in the caller's address space to the + current value associated with a record. fd_funkier_rec_val_const is a + const-correct version. There are sz bytes at the returned pointer. + IMPORTANT SAFETY TIP! There are _no_ alignment guarantees on the + returned value. Returns NULL if the record has a zero sz (which also + covers the case where rec has been marked ERASE). max 0 implies val + NULL and vice versa. Assumes no concurrent operations on rec. */ + +FD_FN_PURE static inline void * /* Lifetime is the lesser of rec or the value size is modified */ +fd_funkier_val( fd_funkier_rec_t const * rec, /* Assumes pointer in caller's address space to a live funk record */ + fd_wksp_t const * wksp ) { /* ==fd_funkier_wksp( funk ) where funk is a current local join */ + ulong val_gaddr = rec->val_gaddr; + if( !val_gaddr ) return NULL; /* Covers the marked ERASE case too */ /* TODO: consider branchless */ + return fd_wksp_laddr_fast( wksp, val_gaddr ); +} + +FD_FN_PURE static inline void const * /* Lifetime is the lesser of rec or the value size is modified */ +fd_funkier_val_const( fd_funkier_rec_t const * rec, /* Assumes pointer in caller's address space to a live funk record */ + fd_wksp_t const * wksp ) { /* ==fd_funkier_wksp( funk ) where funk is a current local join */ + ulong val_gaddr = rec->val_gaddr; + if( !val_gaddr ) return NULL; /* Covers the marked ERASE case too */ /* TODO: consider branchless */ + return fd_wksp_laddr_fast( wksp, val_gaddr ); +} + +/* fd_funkier_val_truncate resizes a record to be new_val_sz bytes in + size. + + This function is optimized for the user knowing the actual long term + record size when they call this. + + Regardless of the current and new value sizes, this will + always attempt to resize the record in order to minimize the amount + of excess allocation used by the record. So this function should be + assumed to kill any existing pointers into this record's value + storage. + + Returns a pointer to the value memory on success and NULL on + failure. If opt_err is non-NULL, on return, *opt_err will hold + FD_FUNKIER_SUCCESS if successful or a FD_FUNKIER_ERR_* code on + failure. Reasons for failure include FD_FUNKIER_ERR_INVAL (NULL + rec, too large new_val_sz, rec is marked ERASE) and + FD_FUNKIER_ERR_MEM (allocation failure, need a larger wksp). On + failure, the current value is unchanged. + + Assumes no concurrent operations on rec. */ + +void * /* Returns record value on success, NULL on failure */ +fd_funkier_val_truncate( fd_funkier_rec_t * rec, /* Assumed in caller's address space to a live funk record (NULL returns NULL) */ + ulong new_val_sz, /* Should be in [0,FD_FUNKIER_REC_VAL_MAX] (returns NULL otherwise) */ + fd_alloc_t * alloc, /* ==fd_funkier_alloc( funk, wksp ) */ + fd_wksp_t * wksp, /* ==fd_funkier_wksp( funk ) where funk is current local join */ + int * opt_err ); /* If non-NULL, *opt_err returns operation error code */ + +/* Misc */ + +/* fd_funkier_val_init sets a record with uninitialized value metadata to + the NULL value. Meant for internal use. */ + +static inline fd_funkier_rec_t * /* Returns rec */ +fd_funkier_val_init( fd_funkier_rec_t * rec ) { /* Assumed record in caller's address space with uninitialized value metadata */ + rec->val_sz = 0U; + rec->val_max = 0U; + rec->val_gaddr = 0UL; + return rec; +} + +/* fd_funkier_val_flush sets a record to the NULL value, discarding the + current value if any. Meant for internal use. */ + +static inline fd_funkier_rec_t * /* Returns rec */ +fd_funkier_val_flush( fd_funkier_rec_t * rec, /* Assumed live funk record in caller's address space */ + fd_alloc_t * alloc, /* ==fd_funkier_alloc( funk, wksp ) */ + fd_wksp_t * wksp ) { /* ==fd_funkier_wksp( funk ) where funk is a current local join */ + ulong val_gaddr = rec->val_gaddr; + fd_funkier_val_init( rec ); + if( val_gaddr ) fd_alloc_free( alloc, fd_wksp_laddr_fast( wksp, val_gaddr ) ); + return rec; +} + +#ifdef FD_FUNKIER_HANDHOLDING + +/* fd_funkier_val_verify verifies the record values. Returns + FD_FUNKIER_SUCCESS if the values appear intact and FD_FUNKIER_ERR_INVAL if + not (logs details). Meant to be called as part of fd_funkier_verify. + As such, it assumes funk is non-NULL, fd_funkier_{wksp,rec_map,wksp_tag} + have been verified to work and the rec_map has been verified. */ + +int +fd_funkier_val_verify( fd_funkier_t * funk ); + +#endif + +FD_PROTOTYPES_END + +/* TODO: Retune fd_alloc and fd_wksp for Solana record size optimized + size classes and transition point to fd_wksp backing. */ + +#endif /* HEADER_fd_src_funk_fd_funkier_val_h */ diff --git a/src/funkier/test_funkier.c b/src/funkier/test_funkier.c new file mode 100644 index 0000000000..6497e9ef83 --- /dev/null +++ b/src/funkier/test_funkier.c @@ -0,0 +1,140 @@ +#include "fd_funkier.h" + +#if FD_HAS_HOSTED + +FD_STATIC_ASSERT( FD_FUNKIER_ALIGN ==4096UL, unit-test ); + +FD_STATIC_ASSERT( FD_FUNKIER_ALIGN >=alignof(fd_funkier_t), unit-test ); + +FD_STATIC_ASSERT( FD_FUNKIER_MAGIC ==0xf17eda2ce7fc2c02UL, unit-test ); + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + + char const * name = fd_env_strip_cmdline_cstr ( &argc, &argv, "--wksp", NULL, NULL ); + char const * _page_sz = fd_env_strip_cmdline_cstr ( &argc, &argv, "--page-sz", NULL, "gigantic" ); + ulong page_cnt = fd_env_strip_cmdline_ulong( &argc, &argv, "--page-cnt", NULL, 1UL ); + ulong near_cpu = fd_env_strip_cmdline_ulong( &argc, &argv, "--near-cpu", NULL, fd_log_cpu_id() ); + ulong wksp_tag = fd_env_strip_cmdline_ulong( &argc, &argv, "--wksp-tag", NULL, 1234UL ); + ulong seed = fd_env_strip_cmdline_ulong( &argc, &argv, "--seed", NULL, 5678UL ); + ulong txn_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--txn-max", NULL, 262144UL ); + ulong rec_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--rec-max", NULL, 262144UL ); + + fd_wksp_t * wksp; + if( name ) { + FD_LOG_NOTICE(( "Attaching to --wksp %s", name )); + wksp = fd_wksp_attach( name ); + } else { + FD_LOG_NOTICE(( "--wksp not specified, using an anonymous local workspace, --page-sz %s, --page-cnt %lu, --near-cpu %lu", + _page_sz, page_cnt, near_cpu )); + wksp = fd_wksp_new_anonymous( fd_cstr_to_shmem_page_sz( _page_sz ), page_cnt, near_cpu, "wksp", 0UL ); + } + + if( FD_UNLIKELY( !wksp ) ) FD_LOG_ERR(( "Unable to attach to wksp" )); + + FD_LOG_NOTICE(( "Testing with --wksp-tag %lu --seed %lu --txn-max %lu --rec-max %lu", wksp_tag, seed, txn_max, rec_max )); + + ulong align = fd_funkier_align(); FD_TEST( align ==FD_FUNKIER_ALIGN ); + ulong footprint = fd_funkier_footprint(txn_max, rec_max); + FD_TEST( fd_ulong_is_pow2( align ) && footprint && fd_ulong_is_aligned( footprint, align ) ); + + void * shmem = fd_wksp_alloc_laddr( wksp, align, footprint, wksp_tag ); + if( FD_UNLIKELY( !shmem ) ) FD_LOG_ERR(( "Unable to allocate shmem" )); + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_new( NULL, wksp_tag, seed, txn_max, rec_max ) ); /* NULL shmem */ + FD_TEST( !fd_funkier_new( (void *)1UL, wksp_tag, seed, txn_max, rec_max ) ); /* misaligned shmem */ + FD_TEST( !fd_funkier_new( (void *)align, wksp_tag, seed, txn_max, rec_max ) ); /* not a wksp addr */ + FD_TEST( !fd_funkier_new( shmem, 0UL, seed, txn_max, rec_max ) ); /* bad tag */ + /* seed is arbitrary */ + FD_TEST( !fd_funkier_new( shmem, wksp_tag, seed, FD_FUNKIER_TXN_IDX_NULL+1UL, rec_max ) ); /* idx compr limited */ +#endif + void * shfunk = fd_funkier_new( shmem, wksp_tag, seed, txn_max, rec_max ); FD_TEST( shfunk==shmem ); + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_join( NULL ) ); /* NULL shmem */ + FD_TEST( !fd_funkier_join( (void *)1UL ) ); /* misaligned shmem */ + FD_TEST( !fd_funkier_join( (void *)align ) ); /* not a wksp addr */ +#endif + fd_funkier_t * funk = fd_funkier_join( shfunk ); FD_TEST( funk ); + + FD_TEST( fd_funkier_wksp ( funk )==wksp ); + FD_TEST( fd_funkier_wksp_tag( funk )==wksp_tag ); + FD_TEST( fd_funkier_seed ( funk )==seed ); + + FD_TEST( fd_funkier_txn_max( funk )==txn_max ); + + fd_funkier_txn_xid_t const * root = fd_funkier_root( funk ); + FD_TEST( root ); + FD_TEST( fd_funkier_txn_xid_eq_root( root ) ); + + fd_funkier_txn_xid_t const * last_publish = fd_funkier_last_publish( funk ); + FD_TEST( last_publish ); + FD_TEST( fd_funkier_txn_xid_eq_root( last_publish ) ); + + FD_TEST( !fd_funkier_last_publish_is_frozen ( funk ) ); + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_verify( funk ) ); +#endif + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_leave( NULL ) ); /* Not a join */ +#endif + FD_TEST( fd_funkier_leave( funk )==shfunk ); + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_delete( NULL ) ); /* NULL shmem */ + FD_TEST( !fd_funkier_delete( (void *)1UL ) ); /* misaligned shmem */ + FD_TEST( !fd_funkier_delete( (void *)align ) ); /* not wksp addr */ +#endif + FD_TEST( fd_funkier_delete( shfunk )==shmem ); /* NULL shmem */ + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_join ( shfunk ) ); /* Can't join deleted */ + FD_TEST( !fd_funkier_delete( shfunk ) ); /* Can't delete twice */ +#endif + + /* Test combinations of txn_max==0 and rec_max==0 */ + + funk = fd_funkier_join( fd_funkier_new( shmem, wksp_tag, seed, 0UL, rec_max ) ); FD_TEST( funk ); +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_verify( funk ) ); +#endif + FD_TEST( fd_funkier_delete( fd_funkier_leave( funk ) ) ); + + funk = fd_funkier_join( fd_funkier_new( shmem, wksp_tag, seed, txn_max, 0UL ) ); FD_TEST( funk ); +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_verify( funk ) ); +#endif + FD_TEST( fd_funkier_delete( fd_funkier_leave( funk ) ) ); + + funk = fd_funkier_join( fd_funkier_new( shmem, wksp_tag, seed, 0UL, 0UL ) ); FD_TEST( funk ); +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_verify( funk ) ); +#endif + FD_TEST( fd_funkier_delete( fd_funkier_leave( funk ) ) ); + + fd_wksp_free_laddr( shmem ); + if( name ) fd_wksp_detach( wksp ); + else fd_wksp_delete_anonymous( wksp ); + + FD_LOG_NOTICE(( "pass" )); + fd_halt(); + return 0; +} + +#else + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + FD_LOG_WARNING(( "skip: unit test requires FD_HAS_HOSTED capabilities" )); + fd_halt(); + return 0; +} + +#endif diff --git a/src/funkier/test_funkier_base.c b/src/funkier/test_funkier_base.c new file mode 100644 index 0000000000..cb1a058cfd --- /dev/null +++ b/src/funkier/test_funkier_base.c @@ -0,0 +1,150 @@ +#include "fd_funkier.h" + +FD_STATIC_ASSERT( FD_FUNKIER_SUCCESS == 0, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_ERR_INVAL ==-1, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_ERR_XID ==-2, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_ERR_KEY ==-3, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_ERR_FROZEN ==-4, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_ERR_TXN ==-5, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_ERR_REC ==-6, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_ERR_MEM ==-7, unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_REC_KEY_ALIGN ==8UL, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_REC_KEY_FOOTPRINT ==40UL, unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_REC_KEY_ALIGN ==alignof(fd_funkier_rec_key_t), unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_REC_KEY_FOOTPRINT ==sizeof (fd_funkier_rec_key_t), unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_TXN_XID_ALIGN ==8UL, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_TXN_XID_FOOTPRINT ==16UL, unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_TXN_XID_ALIGN ==alignof(fd_funkier_txn_xid_t), unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_TXN_XID_FOOTPRINT ==sizeof (fd_funkier_txn_xid_t), unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_XID_KEY_PAIR_ALIGN ==8UL, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_XID_KEY_PAIR_FOOTPRINT==56UL, unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_XID_KEY_PAIR_ALIGN ==alignof(fd_funkier_xid_key_pair_t), unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_XID_KEY_PAIR_FOOTPRINT==sizeof (fd_funkier_xid_key_pair_t), unit_test ); + +static FD_TL ulong unique_tag = 0UL; + +static fd_funkier_rec_key_t * +fd_funkier_rec_key_set_unique( fd_funkier_rec_key_t * key ) { + key->ul[0] = fd_log_app_id(); + key->ul[1] = fd_log_thread_id(); + key->ul[2] = ++unique_tag; +# if FD_HAS_X86 + key->ul[3] = (ulong)fd_tickcount(); +# else + key->ul[3] = 0UL; +# endif + key->ul[4] = ~key->ul[0]; + return key; +} + +static fd_funkier_xid_key_pair_t * +fd_funkier_xid_key_pair_set_unique( fd_funkier_xid_key_pair_t * pair ) { + pair->xid[0] = fd_funkier_generate_xid(); + fd_funkier_rec_key_set_unique( pair->key ); + return pair; +} + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + + FD_TEST( !strcmp( fd_funkier_strerror( FD_FUNKIER_SUCCESS ), "success" ) ); + FD_TEST( !strcmp( fd_funkier_strerror( FD_FUNKIER_ERR_INVAL ), "inval" ) ); + FD_TEST( !strcmp( fd_funkier_strerror( FD_FUNKIER_ERR_XID ), "xid" ) ); + FD_TEST( !strcmp( fd_funkier_strerror( FD_FUNKIER_ERR_KEY ), "key" ) ); + FD_TEST( !strcmp( fd_funkier_strerror( FD_FUNKIER_ERR_FROZEN ), "frozen" ) ); + FD_TEST( !strcmp( fd_funkier_strerror( FD_FUNKIER_ERR_TXN ), "txn" ) ); + FD_TEST( !strcmp( fd_funkier_strerror( FD_FUNKIER_ERR_REC ), "rec" ) ); + FD_TEST( !strcmp( fd_funkier_strerror( FD_FUNKIER_ERR_MEM ), "mem" ) ); + FD_TEST( !strcmp( fd_funkier_strerror( 1 ), "unknown" ) ); + + for( ulong rem=1000000UL; rem; rem-- ) { + fd_funkier_rec_key_t a[1]; fd_funkier_rec_key_set_unique( a ); + fd_funkier_rec_key_t b[1]; fd_funkier_rec_key_set_unique( b ); + + ulong hash = fd_funkier_rec_key_hash( a, 1234UL ); FD_COMPILER_FORGET( hash ); + /**/ hash = fd_funkier_rec_key_hash( b, 1234UL ); FD_COMPILER_FORGET( hash ); + + FD_TEST( fd_funkier_rec_key_eq( a, a )==1 ); FD_TEST( fd_funkier_rec_key_eq( a, b )==0 ); + FD_TEST( fd_funkier_rec_key_eq( b, a )==0 ); FD_TEST( fd_funkier_rec_key_eq( b, b )==1 ); + + FD_TEST( fd_funkier_rec_key_copy( b, a )==b ); + + FD_TEST( fd_funkier_rec_key_eq( a, a )==1 ); FD_TEST( fd_funkier_rec_key_eq( a, b )==1 ); + FD_TEST( fd_funkier_rec_key_eq( b, a )==1 ); FD_TEST( fd_funkier_rec_key_eq( b, b )==1 ); + } + + fd_funkier_txn_xid_t z[1]; + FD_TEST( fd_funkier_txn_xid_set_root( z )==z ); + FD_TEST( fd_funkier_txn_xid_eq_root ( z )==1 ); + FD_TEST( !(z->ul[0] | z->ul[1]) ); + + for( ulong rem=1000000UL; rem; rem-- ) { + fd_funkier_txn_xid_t a[1]; a[0] = fd_funkier_generate_xid(); + fd_funkier_txn_xid_t b[1]; b[0] = fd_funkier_generate_xid(); + + ulong hash = fd_funkier_txn_xid_hash( a, 1234UL ); FD_COMPILER_FORGET( hash ); + /**/ hash = fd_funkier_txn_xid_hash( b, 1234UL ); FD_COMPILER_FORGET( hash ); + + FD_TEST( fd_funkier_txn_xid_eq_root( a )==0 ); + FD_TEST( fd_funkier_txn_xid_eq_root( b )==0 ); + FD_TEST( fd_funkier_txn_xid_eq_root( z )==1 ); + FD_TEST( fd_funkier_txn_xid_eq( a, a )==1 ); FD_TEST( fd_funkier_txn_xid_eq( a, b )==0 ); FD_TEST( fd_funkier_txn_xid_eq( a, z )==0 ); + FD_TEST( fd_funkier_txn_xid_eq( b, a )==0 ); FD_TEST( fd_funkier_txn_xid_eq( b, b )==1 ); FD_TEST( fd_funkier_txn_xid_eq( b, z )==0 ); + FD_TEST( fd_funkier_txn_xid_eq( z, a )==0 ); FD_TEST( fd_funkier_txn_xid_eq( z, b )==0 ); FD_TEST( fd_funkier_txn_xid_eq( z, z )==1 ); + FD_TEST( !(z->ul[0] | z->ul[1] ) ); + + FD_TEST( fd_funkier_txn_xid_copy( b, a )==b ); + + FD_TEST( fd_funkier_txn_xid_eq_root( a )==0 ); + FD_TEST( fd_funkier_txn_xid_eq_root( b )==0 ); + FD_TEST( fd_funkier_txn_xid_eq_root( z )==1 ); + FD_TEST( fd_funkier_txn_xid_eq( a, a )==1 ); FD_TEST( fd_funkier_txn_xid_eq( a, b )==1 ); FD_TEST( fd_funkier_txn_xid_eq( a, z )==0 ); + FD_TEST( fd_funkier_txn_xid_eq( b, a )==1 ); FD_TEST( fd_funkier_txn_xid_eq( b, b )==1 ); FD_TEST( fd_funkier_txn_xid_eq( b, z )==0 ); + FD_TEST( fd_funkier_txn_xid_eq( z, a )==0 ); FD_TEST( fd_funkier_txn_xid_eq( z, b )==0 ); FD_TEST( fd_funkier_txn_xid_eq( z, z )==1 ); + FD_TEST( !(z->ul[0] | z->ul[1] ) ); + + FD_TEST( fd_funkier_txn_xid_copy( a, z )==a ); + + FD_TEST( fd_funkier_txn_xid_eq_root( a )==1 ); + FD_TEST( fd_funkier_txn_xid_eq_root( b )==0 ); + FD_TEST( fd_funkier_txn_xid_eq_root( z )==1 ); + FD_TEST( fd_funkier_txn_xid_eq( a, a )==1 ); FD_TEST( fd_funkier_txn_xid_eq( a, b )==0 ); FD_TEST( fd_funkier_txn_xid_eq( a, z )==1 ); + FD_TEST( fd_funkier_txn_xid_eq( b, a )==0 ); FD_TEST( fd_funkier_txn_xid_eq( b, b )==1 ); FD_TEST( fd_funkier_txn_xid_eq( b, z )==0 ); + FD_TEST( fd_funkier_txn_xid_eq( z, a )==1 ); FD_TEST( fd_funkier_txn_xid_eq( z, b )==0 ); FD_TEST( fd_funkier_txn_xid_eq( z, z )==1 ); + FD_TEST( !(z->ul[0] | z->ul[1]) ); + } + + for( ulong rem=1000000UL; rem; rem-- ) { + fd_funkier_xid_key_pair_t a[1]; fd_funkier_xid_key_pair_set_unique( a ); + fd_funkier_xid_key_pair_t b[1]; fd_funkier_xid_key_pair_set_unique( b ); + + ulong hash = fd_funkier_xid_key_pair_hash( a, 1234UL ); FD_COMPILER_FORGET( hash ); + /**/ hash = fd_funkier_xid_key_pair_hash( b, 1234UL ); FD_COMPILER_FORGET( hash ); + + FD_TEST( fd_funkier_xid_key_pair_eq( a, a )==1 ); FD_TEST( fd_funkier_xid_key_pair_eq( a, b )==0 ); + FD_TEST( fd_funkier_xid_key_pair_eq( b, a )==0 ); FD_TEST( fd_funkier_xid_key_pair_eq( b, b )==1 ); + + FD_TEST( fd_funkier_xid_key_pair_copy( b, a )==b ); + + FD_TEST( fd_funkier_xid_key_pair_eq( a, a )==1 ); FD_TEST( fd_funkier_xid_key_pair_eq( a, b )==1 ); + FD_TEST( fd_funkier_xid_key_pair_eq( b, a )==1 ); FD_TEST( fd_funkier_xid_key_pair_eq( b, b )==1 ); + + fd_funkier_xid_key_pair_set_unique( a ); + fd_funkier_xid_key_pair_set_unique( b ); + + FD_TEST( fd_funkier_xid_key_pair_init( b, a->xid, a->key )==b ); + FD_TEST( fd_funkier_xid_key_pair_eq( a, b ) ); + } + + FD_LOG_NOTICE(( "pass" )); + fd_halt(); + return 0; +} diff --git a/src/funkier/test_funkier_common.c b/src/funkier/test_funkier_common.c new file mode 100644 index 0000000000..704dbff928 --- /dev/null +++ b/src/funkier/test_funkier_common.c @@ -0,0 +1,360 @@ +#include "test_funkier_common.h" +#include + +/* Mini internals *****************************************************/ + +static void +txn_unmap( funk_t * funk, + txn_t * txn ) { + + txn_t * prev = txn->map_prev; + txn_t * next = txn->map_next; + + if( prev ) prev->map_next = next; + else funk->txn_map_head = next; + + if( next ) next->map_prev = prev; + else funk->txn_map_tail = prev; + + funk->txn_cnt--; + free( txn ); +} + +static txn_t * +txn_leave( funk_t * funk, + txn_t * txn ) { + txn_t ** _head = txn->parent ? &txn->parent->child_head : &funk->child_head; + txn_t ** _tail = txn->parent ? &txn->parent->child_tail : &funk->child_tail; + + txn_t * prev = txn->sibling_prev; + txn_t * next = txn->sibling_next; + + if( prev ) prev->sibling_next = next; + else *_head = next; + + if( next ) next->sibling_prev = prev; + else *_tail = prev; + + return txn; +} + +static rec_t * +rec_leave( funk_t * funk, + rec_t * rec ) { + rec_t ** _head = rec->txn ? &rec->txn->rec_head : &funk->rec_head; + rec_t ** _tail = rec->txn ? &rec->txn->rec_tail : &funk->rec_tail; + + rec_t * prev = rec->prev; + rec_t * next = rec->next; + + if( prev ) prev->next = next; + else *_head = next; + + if( next ) next->prev = prev; + else *_tail = prev; + + return rec; +} + +static void +rec_unmap( funk_t * funk, + rec_t * rec ) { + + rec_t * map_prev = rec->map_prev; + rec_t * map_next = rec->map_next; + + if( map_prev ) map_prev->map_next = map_next; + else funk->rec_map_head = map_next; + + if( map_next ) map_next->map_prev = map_prev; + else funk->rec_map_tail = map_prev; + + funk->rec_cnt--; + free( rec ); +} + +/* Mini txn implementation ********************************************/ + +txn_t * +txn_prepare( funk_t * funk, + txn_t * parent, + ulong xid ) { + +//FD_LOG_NOTICE(( "prepare %lu (parent %lu)", xid, parent ? parent->xid : 0UL )); + + txn_t * txn = (txn_t *)malloc( sizeof(txn_t) ); + if( !txn ) FD_LOG_ERR(( "insufficient memory for unit test" )); + + txn->xid = xid; + txn->rec_head = NULL; + txn->rec_tail = NULL; + + /* Map the txn */ + + txn_t * prev = funk->txn_map_tail; + + txn->map_prev = prev; + txn->map_next = NULL; + + if( prev ) prev->map_next = txn; + else funk->txn_map_head = txn; + funk->txn_map_tail = txn; + + funk->txn_cnt++; + + /* Join the family */ + + txn_t ** _head = parent ? &parent->child_head : &funk->child_head; + txn_t ** _tail = parent ? &parent->child_tail : &funk->child_tail; + + prev = *_tail; + + txn->parent = parent; + txn->child_head = NULL; + txn->child_tail = NULL; + txn->sibling_prev = prev; + txn->sibling_next = NULL; + + if( prev ) prev->sibling_next = txn; + else *_head = txn; + *_tail = txn; + + return txn; +} + +void +txn_cancel( funk_t * funk, + txn_t * txn ) { + +//FD_LOG_NOTICE(( "cancel %lu", txn->xid )); + + rec_t * rec = txn->rec_head; + while( rec ) { + rec_t * next = rec->next; + rec_unmap( funk, rec ); + rec = next; + } + + txn_unmap( funk, txn_leave( funk, txn_cancel_children( funk, txn ) ) ); +} + +ulong +txn_publish( funk_t * funk, + txn_t * txn, + ulong cnt ) { + + if( txn->parent ) cnt = txn_publish( funk, txn->parent, cnt ); + +//FD_LOG_NOTICE(( "publish %lu", txn->xid )); + + rec_t * rec = txn->rec_head; + while( rec ) { + rec_t * next = rec->next; + + rec_t * root_rec = rec_query( funk, NULL, rec->key ); + + if( root_rec ) { + // Remove old version of record + rec_unmap( funk, rec_leave( funk, root_rec ) ); + } + + rec_t * prev = funk->rec_tail; + + rec->txn = NULL; + rec->prev = prev; + rec->next = NULL; + + if( prev ) prev->next = rec; + else funk->rec_head = rec; + funk->rec_tail = rec; + + rec = next; + } + + txn_cancel_siblings( funk, txn ); + + for( txn_t * child=txn->child_head; child; child=child->sibling_next ) child->parent = NULL; + funk->child_head = txn->child_head; + funk->child_tail = txn->child_tail; + + funk->last_publish = txn->xid; + + txn_unmap( funk, txn ); + + return cnt + 1UL; +} + +#if 0 +void +txn_merge( funk_t * funk, + txn_t * txn ) { /* Note: txn is a childless only child of an unpublished transaction */ + + txn_t * dst_txn = txn->parent; + +//FD_LOG_NOTICE(( "merge %lu into %lu", txn->xid, dst_txn->xid )); + + rec_t * rec = txn->rec_head; + while( rec ) { + rec_t * next = rec->next; + + rec_t * dst_rec = rec_query( funk, dst_txn, rec->key ); + + if( dst_rec ) { + // Remove old version of record + rec_unmap( funk, rec_leave( funk, dst_rec ) ); + } + + rec_t * prev = dst_txn->rec_tail; + + rec->txn = dst_txn; + rec->prev = prev; + rec->next = NULL; + + if( prev ) prev->next = rec; + else dst_txn->rec_head = rec; + dst_txn->rec_tail = rec; + + rec = next; + } + + txn_unmap( funk, txn_leave( funk, txn ) ); +} +#endif + +/* Mini rec implementation ********************************************/ + +rec_t * +rec_query( funk_t * funk, + txn_t * txn, + ulong key ) { + rec_t * rec = txn ? txn->rec_head : funk->rec_head; + for( ; rec; rec=rec->next ) if( rec->key==key ) break; + return rec; +} + +rec_t * +rec_query_global( funk_t * funk, + txn_t * txn, + ulong key ) { + while( txn ) { + rec_t * rec = rec_query( funk, txn, key ); + if( rec ) return rec; + txn = txn->parent; + } + return rec_query( funk, txn, key ); +} + +rec_t * +rec_insert( funk_t * funk, + txn_t * txn, + ulong key ) { + +//FD_LOG_NOTICE(( "insert (%lu,%lu)", txn ? txn->xid : 0UL, key )); + + rec_t * rec = rec_query( funk, txn, key ); + if( rec ) { + if( rec->erase ) { /* Undo any previous erase */ + rec->erase = 0; + return rec; + } + FD_LOG_ERR(( "never get here unless user error" )); + } + + rec = (rec_t *)malloc( sizeof(rec_t) ); + if( !rec ) FD_LOG_ERR(( "insufficient memory for unit test" )); + + /* Push into the map */ + + rec->key = key; + rec->erase = 0; + rec->val = 0U; + + rec_t * prev = funk->rec_map_tail; + + rec->map_prev = prev; + rec->map_next = NULL; + + if( prev ) prev->map_next = rec; + else funk->rec_map_head = rec; + funk->rec_map_tail = rec; + + funk->rec_cnt++; + + /* Join the txn */ + + rec->txn = txn; + + rec_t ** _head = txn ? &txn->rec_head : &funk->rec_head; + rec_t ** _tail = txn ? &txn->rec_tail : &funk->rec_tail; + + prev = *_tail; + + rec->prev = prev; + rec->next = NULL; + + if( prev ) prev->next = rec; + else *_head = rec; + *_tail = rec; + + return rec; +} + +void +rec_remove( funk_t * funk, + rec_t * rec ) { + (void)funk; + +//FD_LOG_NOTICE(( "remove (%lu,%lu) erase=%i", rec->txn ? rec->txn->xid : 0UL, rec->key, erase )); + + rec->erase = 1; +} + +/* Mini funk implementation *******************************************/ + +funk_t * +funk_new( void ) { + funk_t * funk = (funk_t *)malloc( sizeof(funk_t) ); + if( !funk ) FD_LOG_ERR(( "insufficient memory for unit test" )); + + funk->last_publish = 0UL; + funk->child_head = NULL; + funk->child_tail = NULL; + funk->txn_map_head = NULL; + funk->txn_map_tail = NULL; + funk->txn_cnt = 0UL; + + funk->rec_head = NULL; + funk->rec_tail = NULL; + funk->rec_map_head = NULL; + funk->rec_map_tail = NULL; + funk->rec_cnt = 0UL; + + return funk; +} + +void +funk_delete( funk_t * funk ) { + txn_cancel_children( funk, NULL ); + rec_t * rec = funk->rec_map_head; + while( rec ) { + rec_t * next = rec->map_next; + free( rec ); + rec = next; + } + free( funk ); +} + +/* Testing utility implementations ************************************/ + +ulong +xid_unique( void ) { + static ulong xid = 0UL; + return ++xid; +} + +int +key_eq( fd_funkier_rec_key_t const * key, + ulong _key ) { + fd_funkier_rec_key_t tmp[1]; + return fd_funkier_rec_key_eq( key, key_set( tmp, _key ) ); +} diff --git a/src/funkier/test_funkier_common.h b/src/funkier/test_funkier_common.h new file mode 100644 index 0000000000..3e32217438 --- /dev/null +++ b/src/funkier/test_funkier_common.h @@ -0,0 +1,195 @@ +#ifndef HEADER_fd_src_funkier_test_funkier_common_h +#define HEADER_fd_src_funkier_test_funkier_common_h + +/* "Mini-funk" implementation for reference and testing purposes */ + +#include "fd_funkier_base.h" + +struct txn; +typedef struct txn txn_t; + +struct rec; +typedef struct rec rec_t; + +struct rec { + txn_t * txn; + ulong key; + rec_t * prev; + rec_t * next; + rec_t * map_prev; + rec_t * map_next; + int erase; + uint val; +}; + +struct txn { + ulong xid; + txn_t * parent; + txn_t * child_head; + txn_t * child_tail; + txn_t * sibling_prev; + txn_t * sibling_next; + txn_t * map_prev; + txn_t * map_next; + rec_t * rec_head; + rec_t * rec_tail; +}; + +struct funk { + ulong last_publish; + txn_t * child_head; + txn_t * child_tail; + txn_t * txn_map_head; + txn_t * txn_map_tail; + ulong txn_cnt; + rec_t * rec_head; + rec_t * rec_tail; + rec_t * rec_map_head; + rec_t * rec_map_tail; + ulong rec_cnt; +}; + +typedef struct funk funk_t; + +FD_PROTOTYPES_BEGIN + +/* Mini txn API */ + +FD_FN_PURE static inline int txn_is_frozen( txn_t * txn ) { return !!txn->child_head; } + +FD_FN_PURE static inline int txn_is_only_child( txn_t * txn ) { return !txn->sibling_prev && !txn->sibling_next; } + +FD_FN_PURE static inline txn_t * +txn_ancestor( txn_t * txn ) { + for(;;) { + if( !txn_is_only_child( txn ) ) break; + if( !txn->parent ) return NULL; + txn = txn->parent; + } + return txn; +} + +FD_FN_PURE static inline txn_t * +txn_descendant( txn_t * txn ) { + if( !txn_is_only_child( txn ) ) return NULL; + for(;;) { + if( !txn->child_head || !txn_is_only_child( txn->child_head ) ) break; + txn = txn->child_head; + } + return txn; +} + +txn_t * +txn_prepare( funk_t * funk, + txn_t * parent, + ulong xid ); + +void +txn_cancel( funk_t * funk, + txn_t * txn ); + +ulong +txn_publish( funk_t * funk, + txn_t * txn, + ulong cnt ); + +/* +void +txn_merge( funk_t * funk, + txn_t * txn ); +*/ + +static inline txn_t * +txn_cancel_children( funk_t * funk, + txn_t * txn ) { + txn_t * child = txn ? txn->child_head : funk->child_head; + while( child ) { + txn_t * next = child->sibling_next; + txn_cancel( funk, child ); + child = next; + } + return txn; +} + +static inline txn_t * +txn_cancel_siblings( funk_t * funk, + txn_t * txn ) { + txn_t * child = txn->parent ? txn->parent->child_head : funk->child_head; + while( child ) { + txn_t * next = child->sibling_next; + if( child!=txn ) txn_cancel( funk, child ); + child = next; + } + return txn; +} + +/* Mini rec API */ + +FD_FN_PURE rec_t * +rec_query( funk_t * funk, + txn_t * txn, + ulong key ); + +FD_FN_PURE rec_t * +rec_query_global( funk_t * funk, + txn_t * txn, + ulong key ); + +rec_t * +rec_insert( funk_t * funk, + txn_t * txn, + ulong key ); + +void +rec_remove( funk_t * funk, + rec_t * rec ); + +/* Mini funk API */ + +funk_t * +funk_new( void ); + +void +funk_delete( funk_t * funk ); + +static inline int funk_is_frozen( funk_t * funk ) { return !!funk->child_head; } + +FD_FN_PURE static inline txn_t * +funk_descendant( funk_t * funk ) { + return funk->child_head ? txn_descendant( funk->child_head ) : NULL; +} + +/* Testing utilities */ + +ulong +xid_unique( void ); + +static inline fd_funkier_txn_xid_t * +xid_set( fd_funkier_txn_xid_t * xid, + ulong _xid ) { + xid->ul[0] = _xid; xid->ul[1] = _xid+_xid; + return xid; +} + +FD_FN_PURE static inline int +xid_eq( fd_funkier_txn_xid_t const * xid, + ulong _xid ) { + fd_funkier_txn_xid_t tmp[1]; + return fd_funkier_txn_xid_eq( xid, xid_set( tmp, _xid ) ); +} + +static inline fd_funkier_rec_key_t * +key_set( fd_funkier_rec_key_t * key, + ulong _key ) { + key->ul[0] = _key; key->ul[1] = _key+_key; key->ul[2] = _key*_key; key->ul[3] = -_key; + key->ul[4] = _key*3U; + return key; +} + +FD_FN_PURE int +key_eq( fd_funkier_rec_key_t const * key, + ulong _key ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_funkier_test_funkier_common_h */ diff --git a/src/funkier/test_funkier_common.hpp b/src/funkier/test_funkier_common.hpp new file mode 100644 index 0000000000..ebf99356a9 --- /dev/null +++ b/src/funkier/test_funkier_common.hpp @@ -0,0 +1,438 @@ +extern "C" { + #include "fd_funkier.h" +} +#include +#include +#include +#include +#include +#include +#include + +static const long ROOT_KEY = 0; +static const ulong MAX_TXNS = 100; +static const ulong MAX_CHILDREN = 100; +static const uint MAX_PARTS = 8; + +struct fake_rec { + ulong _key; + std::vector _data; + bool _erased = false; + bool _touched = false; + static std::set _all; + + fake_rec() = delete; + fake_rec(ulong key) : _key(key) { + assert(_all.count(this) == 0); + _all.insert(this); + } + ~fake_rec() { + assert(_all.count(this) == 1); + _all.erase(this); + } + + static fake_rec * make_random() { + auto * rec = new fake_rec(((ulong)lrand48())%MAX_CHILDREN); + auto len = ((ulong)lrand48())%8UL; + rec->_data.resize(len); + for (ulong i = 0; i < len; ++i) + rec->_data[i] = lrand48(); + return rec; + } + + fd_funkier_rec_key_t real_id() const { + fd_funkier_rec_key_t i; + memset(&i, 0, sizeof(i)); + i.ul[0] = _key; + return i; + } + + ulong size() const { + return _data.size()*sizeof(long); + } + + const uchar* data() const { + return (const uchar*)_data.data(); + } +}; + +std::set fake_rec::_all; + +struct fake_txn { + ulong _key; + std::vector _recs; + std::map _children; + fake_txn * _parent = NULL; + bool _touched = false; + + fake_txn(ulong key) : _key(key) { } + ~fake_txn() { + for (auto i : _recs) { + delete i; + } + } + + fd_funkier_txn_xid_t real_id() const { + fd_funkier_txn_xid_t i; + memset(&i, 0, sizeof(i)); + i.ul[0] = _key; + return i; + } + + bool insert(fake_rec* rec) { + for (auto i : _recs) + if( i->_key == rec->_key ) { + delete rec; + return false; /* Error */ + } + auto sz = _recs.size(); + _recs.resize(sz+1); + _recs[sz] = rec; + return true; + } +}; + +struct fake_funk { + fd_wksp_t * _wksp; + fd_funkier_t * _real; + std::map _txns; + ulong _lastxid = 0; +#ifdef TEST_FUNKIER_FILE + fd_funkier_close_file_args_t close_args; +#endif + + fake_funk(int * argc, char *** argv) { + fd_boot( argc, argv ); + ulong txn_max = 128; + ulong rec_max = 1<<16; + +#ifdef TEST_FUNKIER_FILE + _real = fd_funkier_open_file( "funk_test_file", 1, 1234U, txn_max, rec_max, FD_SHMEM_GIGANTIC_PAGE_SZ, FD_FUNKIER_OVERWRITE, &close_args ); + _wksp = fd_funkier_wksp( _real ); + +#else + ulong numa_idx = fd_shmem_numa_idx( 0 ); + _wksp = fd_wksp_new_anonymous( FD_SHMEM_GIGANTIC_PAGE_SZ, 1U, fd_shmem_cpu_idx( numa_idx ), "wksp", 0UL ); + void * mem = fd_wksp_alloc_laddr( _wksp, fd_funkier_align(), fd_funkier_footprint( txn_max, rec_max ), FD_FUNKIER_MAGIC ); + _real = fd_funkier_join( fd_funkier_new( mem, 1, 1234U, txn_max, rec_max ) ); +#endif + + _txns[ROOT_KEY] = new fake_txn(ROOT_KEY); + } + ~fake_funk() { + for (auto i : _txns) + delete i.second; +#ifdef TEST_FUNKIER_FILE + fd_funkier_close_file( &close_args ); + unlink( "funk_test_file" ); +#endif + for( auto i : fake_rec::_all ) + FD_LOG_NOTICE(( "leaked record 0x%lx!", (ulong)i )); + + } + +#ifdef TEST_FUNKIER_FILE + void reopen_file() { + fd_funkier_close_file( &close_args ); + _real = fd_funkier_open_file( "funk_test_file", 1, 0, 0, 0, 0, FD_FUNKIER_READ_WRITE, &close_args ); + _wksp = fd_funkier_wksp( _real ); + } +#endif + + fake_txn * pick_unfrozen_txn() { + fake_txn* list[MAX_TXNS]; + uint listlen = 0; + for (auto i : _txns) + if (i.second->_children.size() == 0) + list[listlen++] = i.second; + return list[((uint)lrand48())%listlen]; + } + + fd_funkier_txn_t * get_real_txn(fake_txn * txn) { + if (txn->_key == ROOT_KEY) + return NULL; + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( _real, _wksp ); + auto xid = txn->real_id(); + return fd_funkier_txn_query(&xid, &txn_map); + } + + void random_insert() { + fake_txn * txn = pick_unfrozen_txn(); + if( txn->_recs.size() == MAX_CHILDREN ) return; + fake_rec * rec = NULL; + do { + rec = fake_rec::make_random(); + /* Prevent duplicate keys */ + } while (!txn->insert(rec)); + + fd_funkier_txn_t * txn2 = get_real_txn(txn); + auto key = rec->real_id(); + fd_funkier_rec_prepare_t prepare[1]; + fd_funkier_rec_t * rec2 = fd_funkier_rec_prepare(_real, txn2, &key, prepare, NULL); + void * val = fd_funkier_val_truncate(rec2, rec->size(), fd_funkier_alloc(_real, _wksp), _wksp, NULL); + memcpy(val, rec->data(), rec->size()); + fd_funkier_rec_publish( prepare ); + assert(fd_funkier_val_sz(rec2) == rec->size()); + } + + void random_remove() { + fake_txn * txn = pick_unfrozen_txn(); + auto& recs = txn->_recs; + fake_rec* list[MAX_CHILDREN]; + uint listlen = 0; + for (auto i : recs) + if (!i->_erased) + list[listlen++] = i; + if (!listlen) return; + auto* rec = list[((uint)lrand48())%listlen]; + + fd_funkier_txn_t * txn2 = get_real_txn(txn); + auto key = rec->real_id(); + assert(fd_funkier_rec_remove(_real, txn2, &key, NULL, 0UL) == FD_FUNKIER_SUCCESS); + + rec->_erased = true; + rec->_data.clear(); + } + + void random_new_txn() { + if (_txns.size() == MAX_TXNS) + return; + + fake_txn* list[MAX_TXNS]; + uint listlen = 0; + for (auto i : _txns) + list[listlen++] = i.second; + auto * parent = list[((uint)lrand48())%listlen]; + + ulong key = ++_lastxid; + auto * txn = _txns[key] = new fake_txn(key); + + txn->_parent = parent; + parent->_children[key] = txn; + + fd_funkier_txn_t * parent2 = get_real_txn(parent); + auto xid = txn->real_id(); + assert(fd_funkier_txn_prepare(_real, parent2, &xid, 1) != NULL); + } + + void fake_cancel_family(fake_txn* txn) { + assert(txn->_key != ROOT_KEY); + while (!txn->_children.empty()) + fake_cancel_family(txn->_children.begin()->second); + txn->_parent->_children.erase(txn->_key); + _txns.erase(txn->_key); + delete txn; + } + + void fake_publish_to_parent(fake_txn* txn) { + // Move records into parent + auto* parent = txn->_parent; + for (auto i : txn->_recs) { + uint p = 0; + for (auto j : parent->_recs) { + if( i->_key == j->_key ) { + delete j; + parent->_recs.erase(parent->_recs.begin()+p); + break; + } + p++; + } + parent->insert(i); + } + txn->_recs.clear(); + + // Cancel siblings + for (;;) { + bool repeat = false; + for (auto i : parent->_children) + if (txn != i.second) { + fake_cancel_family(i.second); + repeat = true; + break; + } + if (!repeat) break; + } + assert(parent->_children.size() == 1 && parent->_children[txn->_key] == txn); + + // Move children up + parent->_children.clear(); + for (auto i : txn->_children) { + auto* child = i.second; + child->_parent = parent; + parent->_children[child->_key] = child; + } + + _txns.erase(txn->_key); + delete txn; + } + + void fake_publish(fake_txn* txn) { + assert(txn->_key != ROOT_KEY); + if (txn->_parent->_key != ROOT_KEY) + fake_publish(txn->_parent); + assert(txn->_parent->_key == ROOT_KEY); + fake_publish_to_parent(txn); + } + + void random_publish() { + fake_txn* list[MAX_TXNS]; + uint listlen = 0; + for (auto i : _txns) + if (i.second->_key != ROOT_KEY) + list[listlen++] = i.second; + if (!listlen) return; + auto * txn = list[((uint)lrand48())%listlen]; + + fd_funkier_txn_t * txn2 = get_real_txn(txn); + assert(fd_funkier_txn_publish(_real, txn2, 1) > 0); + + // Simulate publication + fake_publish(txn); + } + + void random_publish_into_parent() { + fake_txn* list[MAX_TXNS]; + uint listlen = 0; + for (auto i : _txns) + if (i.second->_key != ROOT_KEY) + list[listlen++] = i.second; + if (!listlen) { + return; + } + auto * txn = list[((uint)lrand48())%listlen]; + + fd_funkier_txn_t * txn2 = get_real_txn(txn); + assert(fd_funkier_txn_publish_into_parent(_real, txn2, 1) == FD_FUNKIER_SUCCESS); + + // Simulate publication + fake_publish_to_parent(txn); + } + + void random_cancel() { + fake_txn* list[MAX_TXNS]; + uint listlen = 0; + for (auto i : _txns) + if (i.second->_key != ROOT_KEY) + list[listlen++] = i.second; + if (!listlen) return; + auto * txn = list[((uint)lrand48())%listlen]; + + fd_funkier_txn_t * txn2 = get_real_txn(txn); + assert(fd_funkier_txn_cancel(_real, txn2, 1) > 0); + + // Simulate cancel + fake_cancel_family(txn); + } + + void verify() { +#ifdef FD_FUNKIER_HANDHOLDING + assert(fd_funkier_verify(_real) == FD_FUNKIER_SUCCESS); +#endif + + for (auto i : _txns) { + assert(i.first == i.second->_key); + for (auto j : i.second->_recs) { + j->_touched = false; + } + } + + fd_funkier_all_iter_t iter[1]; + for( fd_funkier_all_iter_new( _real, iter ); !fd_funkier_all_iter_done( iter ); fd_funkier_all_iter_next( iter ) ) { + fd_funkier_rec_t const * rec = fd_funkier_all_iter_ele_const( iter ); + auto const * xid = fd_funkier_rec_xid( rec ); + auto i = _txns.find(xid->ul[0]); + assert(i != _txns.end()); + auto const * key = fd_funkier_rec_key( rec ); + auto& recs = i->second->_recs; + auto j = std::find_if(recs.begin(), recs.end(), [key](auto * rec2) {return rec2->_key == key->ul[0];}); + assert(j != recs.end()); + auto * rec2 = *j; + if (rec2->_erased) { + assert(rec->flags & FD_FUNKIER_REC_FLAG_ERASE); + assert(fd_funkier_val_sz(rec) == 0); + } else { + assert(!(rec->flags & FD_FUNKIER_REC_FLAG_ERASE)); + assert(fd_funkier_val_sz(rec) == rec2->size()); + assert(memcmp(fd_funkier_val(rec, _wksp), rec2->data(), rec2->size()) == 0); + } + + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( _real, fd_funkier_wksp( _real ) ); + fd_funkier_txn_t * txn = fd_funkier_txn_query( xid, &txn_map ); + fd_funkier_rec_query_t query[1]; + auto* rec3 = fd_funkier_rec_query_try_global(_real, txn, rec->pair.key, NULL, query); + if( ( rec->flags & FD_FUNKIER_REC_FLAG_ERASE ) ) + assert(rec3 == NULL); + else + assert(rec == rec3); + assert(!fd_funkier_rec_query_test( query )); + + assert(!rec2->_touched); + rec2->_touched = true; + } + + for (auto i : _txns) { + for (auto j : i.second->_recs) { + assert(j->_touched || j->_erased); + } + } + + for (auto i : _txns) { + auto * txn = i.second; + assert(i.first == txn->_key); + if (txn->_key == ROOT_KEY) { + assert(txn->_parent == NULL); + } else { + assert(txn->_parent->_children.find(txn->_key)->second == txn); + } + txn->_touched = false; + } + + { + // Root transaction + auto * txn2 = _txns[ROOT_KEY]; + assert(!txn2->_touched); + txn2->_touched = true; + + auto& recs = txn2->_recs; + for( auto const * rec = fd_funkier_txn_first_rec(_real, NULL); + rec; + rec = fd_funkier_txn_next_rec(_real, rec) ) { + auto const * key = fd_funkier_rec_key( rec ); + auto j = std::find_if(recs.begin(), recs.end(), [key](auto * rec2) {return rec2->_key == key->ul[0];}); + assert(j != recs.end()); + } + } + + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( _real, _wksp ); + + fd_funkier_txn_all_iter_t txn_iter[1]; + for( fd_funkier_txn_all_iter_new( _real, txn_iter ); !fd_funkier_txn_all_iter_done( txn_iter ); fd_funkier_txn_all_iter_next( txn_iter ) ) { + fd_funkier_txn_t const * txn = fd_funkier_txn_all_iter_ele_const( txn_iter ); + + auto i = _txns.find(txn->xid.ul[0]); + assert(i != _txns.end()); + auto * txn2 = i->second; + assert(!txn2->_touched); + txn2->_touched = true; + + auto * parent = fd_funkier_txn_parent(txn, &txn_pool); + if (parent == NULL) + assert(ROOT_KEY == txn2->_parent->_key); + else + assert(parent->xid.ul[0] == txn2->_parent->_key); + + auto& recs = txn2->_recs; + for( auto const * rec = fd_funkier_txn_first_rec(_real, txn); + rec; + rec = fd_funkier_txn_next_rec(_real, rec) ) { + auto const * key = fd_funkier_rec_key( rec ); + auto j = std::find_if(recs.begin(), recs.end(), [key](auto * rec2) {return rec2->_key == key->ul[0];}); + assert(j != recs.end()); + } + } + + for (auto i : _txns) { + assert(i.second->_touched); + } + } +}; diff --git a/src/funkier/test_funkier_concur.cxx b/src/funkier/test_funkier_concur.cxx new file mode 100644 index 0000000000..5e28be9071 --- /dev/null +++ b/src/funkier/test_funkier_concur.cxx @@ -0,0 +1,154 @@ +#include "test_funkier_common.hpp" +#include +#include "pthread.h" + +#define NUM_THREADS 10 +#define MAX_TXN_CNT 64 + +class TestState { + public: + fd_funkier_t * _funk = NULL; + fd_wksp_t * _wksp = NULL; + struct ThreadPair { + TestState * _state; + ulong _rec_offset; + } _pairs[NUM_THREADS]; + + TestState( fd_funkier_t * funk ) : _funk(funk), _wksp(fd_funkier_wksp(funk)) { + for( uint i = 0; i < NUM_THREADS; ++i ) { + _pairs[i] = ThreadPair{ this, (ulong)i }; + } + } + + fd_funkier_txn_t * pick_txn(bool unfrozen) { + fd_funkier_txn_t * txns[MAX_TXN_CNT+1]; + uint txns_cnt = 0; + if( !unfrozen || !fd_funkier_last_publish_is_frozen( _funk )) txns[txns_cnt++] = NULL; + fd_funkier_txn_all_iter_t txn_iter[1]; + for( fd_funkier_txn_all_iter_new( _funk, txn_iter ); !fd_funkier_txn_all_iter_done( txn_iter ); fd_funkier_txn_all_iter_next( txn_iter ) ) { + fd_funkier_txn_t * txn = fd_funkier_txn_all_iter_ele( txn_iter ); + if( !unfrozen || !fd_funkier_txn_is_frozen( txn )) { + assert(txns_cnt < MAX_TXN_CNT); + txns[txns_cnt++] = txn; + } + } + return txns[lrand48()%txns_cnt]; + } + + uint count_txns() { + fd_funkier_txn_all_iter_t txn_iter[1]; + uint cnt = 0; + for( fd_funkier_txn_all_iter_new( _funk, txn_iter ); !fd_funkier_txn_all_iter_done( txn_iter ); fd_funkier_txn_all_iter_next( txn_iter ) ) ++cnt; + return cnt; + } +}; + +enum { STARTUP, PAUSE, RUN, DONE }; +static volatile int runstate = (int)STARTUP; +static volatile uint runcnt = 0; +static volatile uint insertcnt = 0; + +static void * work_thread(void * arg) { + auto p = *(TestState::ThreadPair*)arg; + fd_funkier_rec_key_t key; + memset(&key, 0, sizeof(key)); + key.ul[0] = p._rec_offset; + auto * state = p._state; + auto * funk = state->_funk; + auto * wksp = state->_wksp; + + while( runstate == (int)STARTUP ) continue; + while( runstate != (int)DONE ) { + while( runstate == (int)PAUSE ) continue; + + FD_ATOMIC_FETCH_AND_ADD( &runcnt, 1 ); + + while( runstate == (int)RUN) { + fd_funkier_txn_t * txn = state->pick_txn(true); + fd_funkier_rec_prepare_t prepare[1]; + fd_funkier_rec_t * rec = fd_funkier_rec_prepare(funk, txn, &key, prepare, NULL); + if( rec == NULL ) continue; + void * val = fd_funkier_val_truncate(rec, sizeof(ulong), fd_funkier_alloc(funk, wksp), wksp, NULL); + memcpy(val, &key.ul[0], sizeof(ulong)); + fd_funkier_rec_publish( prepare ); + + FD_ATOMIC_FETCH_AND_ADD( &insertcnt, 1 ); + + for(;;) { + fd_funkier_rec_query_t query[1]; + fd_funkier_rec_t const * rec2 = fd_funkier_rec_query_try_global(funk, txn, &key, NULL, query); + assert(rec2 && fd_funkier_val_sz(rec2) == sizeof(ulong)); + ulong val2; + memcpy(&val2, fd_funkier_val(rec2, wksp), sizeof(ulong)); + if( fd_funkier_rec_query_test( query ) ) continue; + assert(val2 == key.ul[0]); + break; + } + + key.ul[0] += NUM_THREADS; + } + + FD_ATOMIC_FETCH_AND_SUB( &runcnt, 1 ); + } + + return NULL; +} + +int main(int argc, char** argv) { + srand(1234); + + fd_boot( &argc, &argv ); + + ulong txn_max = MAX_TXN_CNT; + ulong rec_max = 1<<20; + ulong numa_idx = fd_shmem_numa_idx( 0 ); + fd_wksp_t * wksp = fd_wksp_new_anonymous( FD_SHMEM_GIGANTIC_PAGE_SZ, 1U, fd_shmem_cpu_idx( numa_idx ), "wksp", 0UL ); + void * mem = fd_wksp_alloc_laddr( wksp, fd_funkier_align(), fd_funkier_footprint( txn_max, rec_max ), FD_FUNKIER_MAGIC ); + fd_funkier_t * funk = fd_funkier_join( fd_funkier_new( mem, 1, 1234U, txn_max, rec_max ) ); + TestState state(funk); + + pthread_t thr[NUM_THREADS]; + for( uint i = 0; i < NUM_THREADS; ++i ) { + FD_TEST( pthread_create(&thr[i], NULL, work_thread, &state._pairs[i]) == 0 ); + } + + runstate = (int)PAUSE; + + fd_funkier_txn_xid_t xid; + memset(&xid, 0, sizeof(xid)); + + for (uint loop = 0; loop < 60U; ++loop) { + for( uint i = 0; i < 2; ++i ) { + auto * txn = state.pick_txn(false); + if( txn == NULL ) continue; + fd_funkier_txn_publish(funk, txn, 1); + } + for( uint i = 0; i < 20; ++i ) { + auto * parent = state.pick_txn(false); + xid.ul[0]++; + fd_funkier_txn_prepare(funk, parent, &xid, 1); + } + + runstate = (int)RUN; + FD_LOG_NOTICE(( "running (%u transactions)", state.count_txns() )); + sleep(2); + runstate = (int)PAUSE; + while( runcnt ) continue; + FD_LOG_NOTICE(( "paused (%u inserts)", insertcnt )); + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_verify( funk ) ); +#endif + + } + + runstate = (int)DONE; + for( uint i = 0; i < NUM_THREADS; ++i ) { + pthread_join( thr[i], NULL ); + } + + fd_funkier_delete( fd_funkier_leave( funk ) ); + + printf("test passed!\n"); + return 0; +} diff --git a/src/funkier/test_funkier_file.cxx b/src/funkier/test_funkier_file.cxx new file mode 100644 index 0000000000..031420de1f --- /dev/null +++ b/src/funkier/test_funkier_file.cxx @@ -0,0 +1,66 @@ +#define TEST_FUNKIER_FILE 1 +extern "C" { +#include "fd_funkier_filemap.h" +} +#include "test_funkier_common.hpp" +#include + +int main(int argc, char** argv) { + (void)argc; + (void)argv; + srand(1234); + + fake_funk ff(&argc, &argv); + for (uint loop = 0; loop < 50U; ++loop) { + for (uint i = 0; i < 10; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_new_txn(); + ff.verify(); + for (uint i = 0; i < 50; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 20; ++i) + ff.random_remove(); + ff.verify(); + ff.random_publish(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_new_txn(); + ff.verify(); + for (uint i = 0; i < 50; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_remove(); + ff.verify(); + ff.random_publish_into_parent(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_new_txn(); + ff.verify(); + for (uint i = 0; i < 50; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_remove(); + ff.verify(); + ff.random_cancel(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_new_txn(); + ff.verify(); + for (uint i = 0; i < 50; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_remove(); + ff.verify(); + ff.reopen_file(); + ff.verify(); + } + + printf("test passed!\n"); + return 0; +} diff --git a/src/funkier/test_funkier_rec.c b/src/funkier/test_funkier_rec.c new file mode 100644 index 0000000000..5b46092f40 --- /dev/null +++ b/src/funkier/test_funkier_rec.c @@ -0,0 +1,419 @@ +#include "fd_funkier.h" + +#if FD_HAS_HOSTED + +FD_STATIC_ASSERT( FD_FUNKIER_REC_ALIGN == 32UL, unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_REC_FLAG_ERASE==1UL, unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_REC_IDX_NULL==ULONG_MAX, unit_test ); + +#include "test_funkier_common.h" + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + + char const * name = fd_env_strip_cmdline_cstr ( &argc, &argv, "--wksp", NULL, NULL ); + char const * _page_sz = fd_env_strip_cmdline_cstr ( &argc, &argv, "--page-sz", NULL, "gigantic" ); + ulong page_cnt = fd_env_strip_cmdline_ulong( &argc, &argv, "--page-cnt", NULL, 1UL ); + ulong near_cpu = fd_env_strip_cmdline_ulong( &argc, &argv, "--near-cpu", NULL, fd_log_cpu_id() ); + ulong wksp_tag = fd_env_strip_cmdline_ulong( &argc, &argv, "--wksp-tag", NULL, 1234UL ); + ulong seed = fd_env_strip_cmdline_ulong( &argc, &argv, "--seed", NULL, 5678UL ); + ulong txn_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--txn-max", NULL, 32UL ); + ulong rec_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--rec-max", NULL, 128UL ); + ulong iter_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--iter-max", NULL, 1048576UL ); + int verbose = fd_env_strip_cmdline_int ( &argc, &argv, "--verbose", NULL, 0 ); + + fd_rng_t _rng[1]; fd_rng_t * rng = fd_rng_join( fd_rng_new( _rng, 0U, 0UL ) ); + + fd_wksp_t * wksp; + if( name ) { + FD_LOG_NOTICE(( "Attaching to --wksp %s", name )); + wksp = fd_wksp_attach( name ); + } else { + FD_LOG_NOTICE(( "--wksp not specified, using an anonymous local workspace, --page-sz %s, --page-cnt %lu, --near-cpu %lu", + _page_sz, page_cnt, near_cpu )); + wksp = fd_wksp_new_anonymous( fd_cstr_to_shmem_page_sz( _page_sz ), page_cnt, near_cpu, "wksp", 0UL ); + } + + if( FD_UNLIKELY( !wksp ) ) FD_LOG_ERR(( "Unable to attach to wksp" )); + + FD_LOG_NOTICE(( "Testing with --wksp-tag %lu --seed %lu --txn-max %lu --rxn-max %lu --iter-max %lu --verbose %i", + wksp_tag, seed, txn_max, rec_max, iter_max, verbose )); + + fd_funkier_t * tst = fd_funkier_join( fd_funkier_new( fd_wksp_alloc_laddr( wksp, fd_funkier_align(), fd_funkier_footprint(txn_max, rec_max), wksp_tag ), + wksp_tag, seed, txn_max, rec_max ) ); + if( FD_UNLIKELY( !tst ) ) FD_LOG_ERR(( "Unable to create tst" )); + + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( tst, wksp ); + fd_funkier_txn_pool_t txn_pool = fd_funkier_txn_pool( tst, wksp ); + + funk_t * ref = funk_new(); + + for( ulong iter=0UL; itertxn_cnt, ref->rec_cnt )); + + //if( !ref->txn_cnt ) { + // FD_LOG_NOTICE(( "***************************************************************" )); + // for( rec_t * rrec=ref->rec_head; rrec; rrec=rrec->next ) FD_LOG_NOTICE(( "has %lu", rrec->key )); + //} + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_verify( tst ) ); +#endif + + fd_funkier_txn_xid_t txid[1]; + fd_funkier_rec_key_t tkey[1]; + + do { + + int is_frozen = fd_funkier_last_publish_is_frozen( tst ); + + FD_TEST( is_frozen==funk_is_frozen( ref ) ); + FD_TEST( xid_eq( fd_funkier_last_publish( tst ), ref->last_publish ) ); + + ulong rpmap = 0UL; + for( rec_t * rrec=ref->rec_head; rrec; rrec=rrec->next ) { + FD_TEST( !fd_ulong_extract_bit( rpmap, (int)rrec->key ) ); + rpmap = fd_ulong_set_bit( rpmap, (int)rrec->key ); + } + + ulong tpmap = 0UL; + for( fd_funkier_rec_t const * trec=fd_funkier_txn_first_rec( tst, NULL ); + trec; + trec=fd_funkier_txn_next_rec( tst, trec ) ) { + ulong _tkey = fd_funkier_rec_key( trec )->ul[0]; FD_TEST( _tkey<64UL ); + FD_TEST( !fd_ulong_extract_bit( tpmap, (int)_tkey ) ); + tpmap = fd_ulong_set_bit( tpmap, (int)_tkey ); + } + + ulong rkey = (ulong)(fd_rng_uint( rng ) & 63U); + key_set( tkey, rkey ); + + fd_funkier_rec_query_t rec_query[1]; +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_rec_query_try ( NULL, NULL, NULL, NULL ) ); + FD_TEST( !fd_funkier_rec_query_try ( NULL, NULL, tkey, rec_query ) ); + FD_TEST( !fd_funkier_rec_query_try ( tst, NULL, NULL, rec_query ) ); + FD_TEST( !fd_funkier_rec_query_try ( tst, NULL, tkey, NULL ) ); + + FD_TEST( !fd_funkier_rec_query_try_global ( NULL, NULL, NULL, NULL, NULL ) ); + FD_TEST( !fd_funkier_rec_query_try_global ( NULL, NULL, tkey, NULL, rec_query ) ); + FD_TEST( !fd_funkier_rec_query_try_global ( tst, NULL, NULL, NULL, rec_query ) ); + FD_TEST( !fd_funkier_rec_query_try_global ( tst, NULL, tkey, NULL, NULL ) ); +#endif + + rec_t * rrec = rec_query_global( ref, NULL, rkey ); + fd_funkier_rec_t const * trec = fd_funkier_rec_query_try_global( tst, NULL, tkey, NULL, rec_query ); + if( !rrec || rrec->erase ) FD_TEST( !trec ); + else FD_TEST( trec && xid_eq( fd_funkier_rec_xid( trec ), rrec->txn ? rrec->txn->xid : 0UL ) ); + FD_TEST( !fd_funkier_rec_query_test( rec_query ) ); + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( fd_funkier_rec_remove( NULL, NULL, NULL, NULL, 0UL )==FD_FUNKIER_ERR_INVAL ); + FD_TEST( fd_funkier_rec_remove( NULL, NULL, tkey, NULL, 0UL )==FD_FUNKIER_ERR_INVAL ); + FD_TEST( fd_funkier_rec_remove( tst, NULL, NULL, NULL, 0UL )==FD_FUNKIER_ERR_INVAL ); +#endif + + if( trec ) { + if( is_frozen ) { + FD_TEST( fd_funkier_rec_remove( tst, NULL, tkey, NULL, 0UL )==FD_FUNKIER_ERR_FROZEN ); + } + } + + fd_funkier_rec_prepare_t rec_prepare[1]; + int err; +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_rec_prepare( NULL, NULL, NULL, NULL, NULL ) ); + FD_TEST( !fd_funkier_rec_prepare( NULL, NULL, NULL, NULL, &err ) ); FD_TEST( err==FD_FUNKIER_ERR_INVAL ); +#endif + + if( is_frozen ) { + FD_TEST( !fd_funkier_rec_prepare( tst, NULL, tkey, rec_prepare, NULL ) ); + FD_TEST( !fd_funkier_rec_prepare( tst, NULL, tkey, rec_prepare, &err ) ); FD_TEST( err==FD_FUNKIER_ERR_FROZEN ); + } else if( fd_funkier_rec_is_full( tst ) ) { + FD_TEST( !fd_funkier_rec_prepare( tst, NULL, tkey, rec_prepare, NULL ) ); + FD_TEST( !fd_funkier_rec_prepare( tst, NULL, tkey, rec_prepare, &err ) ); FD_TEST( err==FD_FUNKIER_ERR_REC ); + } + + } while(0); + + ulong cnt = 0UL; + + txn_t * rtxn = ref->txn_map_head; + while( rtxn ) { + + fd_funkier_txn_t * ttxn = fd_funkier_txn_query( xid_set( txid, rtxn->xid ), &txn_map ); + FD_TEST( ttxn && xid_eq( fd_funkier_txn_xid( ttxn ), rtxn->xid ) ); + +# define TEST_RELATIVE(rel) do { \ + txn_t * r##rel = rtxn->rel; \ + fd_funkier_txn_t * t##rel = fd_funkier_txn_##rel( ttxn, &txn_pool ); \ + if( !r##rel ) FD_TEST( !t##rel ); \ + else FD_TEST( t##rel && xid_eq( fd_funkier_txn_xid( t##rel ), r##rel->xid ) ); \ + } while(0) + TEST_RELATIVE( parent ); + TEST_RELATIVE( child_head ); + TEST_RELATIVE( child_tail ); + TEST_RELATIVE( sibling_prev ); + TEST_RELATIVE( sibling_next ); +# undef TEST_RELATIVE + + int ttxn_is_frozen = fd_funkier_txn_is_frozen( ttxn ); + + FD_TEST( txn_is_frozen ( rtxn )==ttxn_is_frozen ); + FD_TEST( txn_is_only_child( rtxn )==fd_funkier_txn_is_only_child( ttxn ) ); + + txn_t * rancestor = txn_ancestor( rtxn ); + fd_funkier_txn_t * tancestor = fd_funkier_txn_ancestor( ttxn, &txn_pool ); + if( rancestor ) FD_TEST( tancestor && xid_eq( fd_funkier_txn_xid( tancestor ), rancestor->xid ) ); + else FD_TEST( !tancestor ); + + txn_t * rdescendant = txn_descendant( rtxn ); + fd_funkier_txn_t * tdescendant = fd_funkier_txn_descendant( ttxn, &txn_pool ); + if( rdescendant ) FD_TEST( tdescendant && xid_eq( fd_funkier_txn_xid( tdescendant ), rdescendant->xid ) ); + else FD_TEST( !tdescendant ); + + ulong rkey = (ulong)(fd_rng_uint( rng ) & 63U); + key_set( tkey, rkey ); + + fd_funkier_rec_query_t rec_query[1]; +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_rec_query_try ( NULL, ttxn, NULL, NULL ) ); + FD_TEST( !fd_funkier_rec_query_try ( NULL, ttxn, tkey, rec_query ) ); + FD_TEST( !fd_funkier_rec_query_try ( tst, ttxn, NULL, rec_query ) ); + FD_TEST( !fd_funkier_rec_query_try ( tst, ttxn, tkey, NULL ) ); + + FD_TEST( !fd_funkier_rec_query_try_global ( NULL, ttxn, NULL, NULL, NULL ) ); + FD_TEST( !fd_funkier_rec_query_try_global ( NULL, ttxn, tkey, NULL, rec_query ) ); + FD_TEST( !fd_funkier_rec_query_try_global ( tst, ttxn, NULL, NULL, rec_query ) ); + FD_TEST( !fd_funkier_rec_query_try_global ( tst, ttxn, tkey, NULL, NULL ) ); +#endif + + rec_t * rrec = rec_query_global( ref, rtxn, rkey ); + fd_funkier_rec_t const * trec = fd_funkier_rec_query_try_global( tst, ttxn, tkey, NULL, rec_query ); + if( !rrec || rrec->erase ) FD_TEST( !trec ); + else { + FD_TEST( trec && xid_eq( fd_funkier_rec_xid( trec ), rrec->txn ? rrec->txn->xid : 0UL ) ); + } + FD_TEST( !fd_funkier_rec_query_test( rec_query ) ); + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( fd_funkier_rec_remove( NULL, ttxn, NULL, NULL, 0UL )==FD_FUNKIER_ERR_INVAL ); + FD_TEST( fd_funkier_rec_remove( NULL, ttxn, tkey, NULL, 0UL )==FD_FUNKIER_ERR_INVAL ); + FD_TEST( fd_funkier_rec_remove( tst, ttxn, NULL, NULL, 0UL )==FD_FUNKIER_ERR_INVAL ); +#endif + + if( trec && ttxn_is_frozen ) { + FD_TEST( fd_funkier_rec_remove( tst, ttxn, tkey, NULL, 0UL )==FD_FUNKIER_ERR_FROZEN ); + } + + fd_funkier_rec_prepare_t rec_prepare[1]; + int err; +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_rec_prepare( NULL, ttxn, NULL, NULL, NULL ) ); + FD_TEST( !fd_funkier_rec_prepare( NULL, ttxn, NULL, NULL, &err ) ); FD_TEST( err==FD_FUNKIER_ERR_INVAL ); +#endif + + if( ttxn_is_frozen ) { + FD_TEST( !fd_funkier_rec_prepare( tst, ttxn, tkey, rec_prepare, NULL ) ); + FD_TEST( !fd_funkier_rec_prepare( tst, ttxn, tkey, rec_prepare, &err ) ); FD_TEST( err==FD_FUNKIER_ERR_FROZEN ); + } else if( fd_funkier_rec_is_full( tst ) ) { + FD_TEST( !fd_funkier_rec_prepare( tst, ttxn, tkey, rec_prepare, NULL ) ); + FD_TEST( !fd_funkier_rec_prepare( tst, ttxn, tkey, rec_prepare, &err ) ); FD_TEST( err==FD_FUNKIER_ERR_REC ); + } + + ulong rpmap = 0UL; + for( rec_t * rrec=rtxn->rec_head; rrec; rrec=rrec->next ) { + FD_TEST( !fd_ulong_extract_bit( rpmap, (int)rrec->key ) ); + rpmap = fd_ulong_set_bit( rpmap, (int)rrec->key ); + } + + ulong tpmap = 0UL; + for( fd_funkier_rec_t const * trec=fd_funkier_txn_first_rec( tst, ttxn ); + trec; + trec=fd_funkier_txn_next_rec( tst, trec ) ) { + ulong _tkey = fd_funkier_rec_key( trec )->ul[0]; FD_TEST( _tkey<64UL ); + FD_TEST( !fd_ulong_extract_bit( tpmap, (int)_tkey ) ); + tpmap = fd_ulong_set_bit( tpmap, (int)_tkey ); + } + + FD_TEST( rpmap==tpmap ); + + cnt++; + rtxn = rtxn->map_next; + } + + FD_TEST( cnt==ref->txn_cnt ); + + cnt = 0UL; + + rec_t * rrec = ref->rec_map_head; + while( rrec ) { + + ulong rxid = rrec->txn ? rrec->txn->xid : 0UL; + ulong rkey = rrec->key; + + xid_set( txid, rxid ); + key_set( tkey, rkey ); + + fd_funkier_txn_t const * ttxn = rxid ? fd_funkier_txn_query( txid, &txn_map ) : NULL; + fd_funkier_rec_query_t rec_query[1]; + fd_funkier_rec_t const * trec = fd_funkier_rec_query_try( tst, ttxn, tkey, rec_query ); + FD_TEST( trec && xid_eq( fd_funkier_rec_xid( trec ), rxid ) && key_eq( fd_funkier_rec_key( trec ), rkey ) ); + FD_TEST( !fd_funkier_rec_query_test( rec_query ) ); + +# define TEST_RELATIVE(rel) do { \ + rec_t * r##rel = rrec->rel; \ + fd_funkier_rec_t const * t##rel = fd_funkier_txn_##rel##_rec( tst, trec ); \ + if( !r##rel ) FD_TEST( !t##rel ); \ + else { \ + ulong r##rel##xid = r##rel->txn ? r##rel->txn->xid : 0UL; \ + FD_TEST( t##rel && xid_eq( fd_funkier_rec_xid( t##rel ), r##rel##xid ) && \ + key_eq( fd_funkier_rec_key( t##rel ), r##rel->key ) ); \ + } \ + } while(0) + TEST_RELATIVE( prev ); + TEST_RELATIVE( next ); +# undef TEST_RELATIVE + + cnt++; + rrec = rrec->map_next; + } + + FD_TEST( cnt==ref->rec_cnt ); + + uint r = fd_rng_uint( rng ); + + uint op = fd_rng_uint_roll( rng, 1U+1U+16U+128U+128U ); + if( op>=146U ) { /* Insert 8x prepare rate */ + + if( FD_UNLIKELY( fd_funkier_rec_is_full( tst ) ) ) continue; + + ulong idx = fd_rng_ulong_roll( rng, ref->txn_cnt+1UL ); + txn_t * rtxn; + ulong rxid; + if( idxtxn_cnt ) { /* insert into in-prep */ + rtxn = ref->txn_map_head; for( ulong rem=idx; rem; rem-- ) rtxn = rtxn->map_next; + if( txn_is_frozen( rtxn ) ) continue; + rxid = rtxn->xid; + } else { /* insert into last published */ + if( funk_is_frozen( ref ) ) continue; + rtxn = NULL; + rxid = 0UL; + } + + ulong rkey = (ulong)(r & 63U); r >>= 6; + if( rec_query( ref, rtxn, rkey ) ) continue; + rec_insert( ref, rtxn, rkey ); + + int err; + fd_funkier_txn_t * ttxn = fd_funkier_txn_query( xid_set( txid, rxid ), &txn_map ); + fd_funkier_rec_prepare_t prepare[1]; + fd_funkier_rec_t const * trec = fd_funkier_rec_prepare( tst, ttxn, key_set( tkey, rkey ), prepare, &err ); + FD_TEST( trec ); + FD_TEST( !err ); + fd_funkier_rec_publish( prepare ); + + } else if( op>=18UL ) { /* Remove and insert at same rate */ + + if( FD_UNLIKELY( !ref->rec_cnt ) ) continue; + + ulong idx = fd_rng_ulong_roll( rng, ref->rec_cnt ); + rec_t * rrec = ref->rec_map_head; for( ulong rem=idx; rem; rem-- ) rrec = rrec->map_next; + + ulong rxid; + if( rrec->txn ) { + if( txn_is_frozen( rrec->txn ) ) continue; + rxid = rrec->txn->xid; + } else { + if( funk_is_frozen( ref ) ) continue; + rxid = 0UL; + } + ulong rkey = rrec->key; + + rec_remove( ref, rrec ); + + fd_funkier_txn_t * ttxn = rxid ? fd_funkier_txn_query( xid_set( txid, rxid ), &txn_map ) : NULL; + fd_funkier_rec_query_t query[1]; + fd_funkier_rec_t const * trec = fd_funkier_rec_query_try( tst, ttxn, key_set( tkey, rkey ), query ); + FD_TEST( trec ); + FD_TEST( !fd_funkier_rec_query_test( query ) ); + + fd_funkier_rec_t * trec2; + FD_TEST( !fd_funkier_rec_remove( tst, ttxn, key_set( tkey, rkey ), &trec2, 0UL ) ); + FD_TEST( trec == trec2 ); + + } else if( op>=2 ) { /* Prepare 8x as publish and cancel combined */ + + if( FD_UNLIKELY( fd_funkier_txn_is_full( tst ) ) ) continue; + + txn_t * rparent; + fd_funkier_txn_t * tparent; + + ulong idx = fd_rng_ulong_roll( rng, ref->txn_cnt+1UL ); + if( idxtxn_cnt ) { /* Branch off in-prep */ + rparent = ref->txn_map_head; for( ulong rem=idx; rem; rem-- ) rparent = rparent->map_next; + tparent = fd_funkier_txn_query( xid_set( txid, rparent->xid ), &txn_map ); + } else { /* Branch off last published */ + rparent = NULL; + tparent = NULL; + } + + ulong rxid = xid_unique(); + txn_prepare( ref, rparent, rxid ); + FD_TEST( fd_funkier_txn_prepare( tst, tparent, xid_set( txid, rxid ), verbose ) ); + + } else if( op>=1UL ) { + + if( FD_UNLIKELY( !ref->txn_cnt ) ) continue; + + ulong idx = fd_rng_ulong_roll( rng, ref->txn_cnt ); + + txn_t * rtxn = ref->txn_map_head; for( ulong rem=idx; rem; rem-- ) rtxn = rtxn->map_next; + fd_funkier_txn_t * ttxn = fd_funkier_txn_query( xid_set( txid, rtxn->xid ), &txn_map ); + + ulong cnt = ref->txn_cnt; txn_cancel( ref, rtxn ); cnt -= ref->txn_cnt; + FD_TEST( fd_funkier_txn_cancel( tst, ttxn, verbose )==cnt ); + + } else { + + if( FD_UNLIKELY( !ref->txn_cnt ) ) continue; + + ulong idx = fd_rng_ulong_roll( rng, ref->txn_cnt ); + txn_t * rtxn = ref->txn_map_head; for( ulong rem=idx; rem; rem-- ) rtxn = rtxn->map_next; + fd_funkier_txn_t * ttxn = fd_funkier_txn_query( xid_set( txid, rtxn->xid ), &txn_map ); + + ulong cnt = txn_publish( ref, rtxn, 0UL ); + FD_TEST( fd_funkier_txn_publish( tst, ttxn, verbose )==cnt ); + } + + } + + funk_delete( ref ); + + fd_wksp_free_laddr( fd_funkier_delete( fd_funkier_leave( tst ) ) ); + if( name ) fd_wksp_detach( wksp ); + else fd_wksp_delete_anonymous( wksp ); + + fd_rng_delete( fd_rng_leave( rng ) ); + + FD_LOG_NOTICE(( "pass" )); + fd_halt(); + return 0; +} + +#else + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + FD_LOG_WARNING(( "skip: unit test requires FD_HAS_HOSTED capabilities" )); + fd_halt(); + return 0; +} + +#endif diff --git a/src/funkier/test_funkier_txn.c b/src/funkier/test_funkier_txn.c new file mode 100644 index 0000000000..14a5985827 --- /dev/null +++ b/src/funkier/test_funkier_txn.c @@ -0,0 +1,343 @@ +#include "fd_funkier.h" + +/* TODO: more extensive testing of fd_funkier_txn_cancel_siblings, + fd_funkier_txn_cancel_children (implicitly tested under the hood but the + user wrappers aren't), coverage of fd_funkier_txn_merge. */ + +#if FD_HAS_HOSTED + +FD_STATIC_ASSERT( FD_FUNKIER_TXN_ALIGN ==32UL, unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_TXN_FOOTPRINT==96UL, unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_TXN_ALIGN ==alignof(fd_funkier_txn_t), unit_test ); +FD_STATIC_ASSERT( FD_FUNKIER_TXN_FOOTPRINT==sizeof (fd_funkier_txn_t), unit_test ); + +FD_STATIC_ASSERT( FD_FUNKIER_TXN_IDX_NULL==(ulong)UINT_MAX, unit_test ); + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + + char const * name = fd_env_strip_cmdline_cstr ( &argc, &argv, "--wksp", NULL, NULL ); + char const * _page_sz = fd_env_strip_cmdline_cstr ( &argc, &argv, "--page-sz", NULL, "gigantic" ); + ulong page_cnt = fd_env_strip_cmdline_ulong( &argc, &argv, "--page-cnt", NULL, 1UL ); + ulong near_cpu = fd_env_strip_cmdline_ulong( &argc, &argv, "--near-cpu", NULL, fd_log_cpu_id() ); + ulong wksp_tag = fd_env_strip_cmdline_ulong( &argc, &argv, "--wksp-tag", NULL, 1234UL ); + ulong seed = fd_env_strip_cmdline_ulong( &argc, &argv, "--seed", NULL, 5678UL ); + ulong txn_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--txn-max", NULL, 32UL ); + ulong rec_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--rec-max", NULL, 32UL ); + ulong iter_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--iter-max", NULL, 1048576UL ); + int verbose = fd_env_strip_cmdline_int ( &argc, &argv, "--verbose", NULL, 0 ); + + fd_rng_t _rng[1]; fd_rng_t * rng = fd_rng_join( fd_rng_new( _rng, 0U, 0UL ) ); + + fd_wksp_t * wksp; + if( name ) { + FD_LOG_NOTICE(( "Attaching to --wksp %s", name )); + wksp = fd_wksp_attach( name ); + } else { + FD_LOG_NOTICE(( "--wksp not specified, using an anonymous local workspace, --page-sz %s, --page-cnt %lu, --near-cpu %lu", + _page_sz, page_cnt, near_cpu )); + wksp = fd_wksp_new_anonymous( fd_cstr_to_shmem_page_sz( _page_sz ), page_cnt, near_cpu, "wksp", 0UL ); + } + + if( FD_UNLIKELY( !wksp ) ) FD_LOG_ERR(( "Unable to attach to wksp" )); + + FD_LOG_NOTICE(( "Testing with --wksp-tag %lu --seed %lu --txn-max %lu --rxn-max %lu --iter-max %lu --verbose %i", + wksp_tag, seed, txn_max, rec_max, iter_max, verbose )); + + fd_funkier_t * funk = fd_funkier_join( fd_funkier_new( fd_wksp_alloc_laddr( wksp, fd_funkier_align(), fd_funkier_footprint( txn_max, rec_max ), wksp_tag ), + wksp_tag, seed, txn_max, rec_max ) ); + if( FD_UNLIKELY( !funk ) ) FD_LOG_ERR(( "Unable to create funk" )); + + fd_funkier_txn_map_t map = fd_funkier_txn_map( funk, wksp ); + fd_funkier_txn_pool_t pool = fd_funkier_txn_pool( funk, wksp ); + + for( ulong rem=1000000UL; rem; rem-- ) { + ulong idx = (ulong)fd_rng_uint( rng ); + FD_TEST( fd_funkier_txn_idx( fd_funkier_txn_cidx( idx ) )==idx ); + } + + FD_TEST( fd_funkier_txn_idx_is_null( FD_FUNKIER_TXN_IDX_NULL ) ); + FD_TEST( !fd_funkier_txn_idx_is_null( 0UL ) ); + + fd_funkier_txn_xid_t const * last_publish = fd_funkier_last_publish( funk ); + + FD_TEST( !fd_funkier_txn_is_full( funk ) ); + + fd_funkier_txn_xid_t recent_xid[ 64 ]; for( ulong idx=0UL; idx<64UL; idx++ ) recent_xid[ idx ] = fd_funkier_generate_xid(); + ulong recent_cursor = 0UL; + + for( ulong iter=0UL; iter>= 6; \ + } while(0) + + uint r = fd_rng_uint( rng ); + int op = (int)(r & 15U); r >>= 4; + switch( op ) { + + case 0: { /* look up a live xid (always succeed) */ + if( FD_UNLIKELY( !live_pmap ) ) break; + uint idx; RANDOM_SET_BIT_IDX( live_pmap ); + fd_funkier_txn_t * txn = fd_funkier_txn_query( &recent_xid[idx], &map ); + FD_TEST( txn && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( txn ), &recent_xid[idx] ) ); + break; + } + + case 1: { /* look up a dead xid (always fail) */ + if( FD_UNLIKELY( !~live_pmap ) ) break; + uint idx; RANDOM_SET_BIT_IDX( ~live_pmap ); + fd_funkier_txn_t * txn = fd_funkier_txn_query( &recent_xid[idx], &map ); + FD_TEST( !txn ); + break; + } + + case 2: { /* look up never seen xid (always fail) */ + fd_funkier_txn_xid_t xid[1]; + xid[0] = fd_funkier_generate_xid(); + fd_funkier_txn_t * txn = fd_funkier_txn_query( xid, &map ); + FD_TEST( !txn ); + break; + } + + case 3: { /* prepare from most recent published with an live xid (always fail) */ + if( FD_UNLIKELY( !live_pmap ) ) break; + uint idx; RANDOM_SET_BIT_IDX( live_pmap ); + FD_TEST( !fd_funkier_txn_prepare( funk, NULL, &recent_xid[idx], verbose ) ); + break; + } + + case 4: { /* prepare from most recent published with a dead xid (succeed if not full) */ + if( FD_UNLIKELY( !~live_pmap ) ) break; + uint idx; RANDOM_SET_BIT_IDX( ~live_pmap ); + int is_full = fd_funkier_txn_is_full( funk ); + if( FD_UNLIKELY( fd_funkier_txn_xid_eq( &recent_xid[idx], last_publish ) ) ) break; + fd_funkier_txn_t * txn = fd_funkier_txn_prepare( funk, NULL, &recent_xid[idx], verbose ); + if( is_full ) FD_TEST( !txn ); + else FD_TEST( txn && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( txn ), &recent_xid[idx] ) ); + break; + } + + case 5: { /* prepare from most recent published never seen xid (succeed if not full) */ + fd_funkier_txn_xid_t * xid = &recent_xid[ recent_cursor ]; + *xid = fd_funkier_generate_xid(); + recent_cursor = (recent_cursor+1UL) & 63UL; + int is_full = fd_funkier_txn_is_full( funk ); + fd_funkier_txn_t * txn = fd_funkier_txn_prepare( funk, NULL, xid, verbose ); + if( is_full ) FD_TEST( !txn ); + else FD_TEST( txn && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( txn ), xid ) ); + break; + } + + case 6: { /* prepare from live xid with a live xid (always fail) */ + if( FD_UNLIKELY( !live_pmap ) ) break; + uint idx; uint idx1; RANDOM_SET_BIT_IDX( live_pmap ); idx1 = idx; RANDOM_SET_BIT_IDX( live_pmap ); + fd_funkier_txn_t * parent = fd_funkier_txn_query( &recent_xid[idx], &map ); + FD_TEST( parent && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( parent ), &recent_xid[idx] ) ); + FD_TEST( !fd_funkier_txn_prepare( funk, parent, &recent_xid[idx1], verbose ) ); + break; + } + + case 7: { /* prepare from live xid with a dead xid (succeed if not full) */ + if( FD_UNLIKELY( !live_pmap || !~live_pmap ) ) break; + uint idx; uint idx1; RANDOM_SET_BIT_IDX( ~live_pmap ); idx1 = idx; RANDOM_SET_BIT_IDX( live_pmap ); + if( FD_UNLIKELY( fd_funkier_txn_xid_eq( &recent_xid[idx1], last_publish ) ) ) break; + fd_funkier_txn_t * parent = fd_funkier_txn_query( &recent_xid[idx], &map ); + FD_TEST( parent && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( parent ), &recent_xid[idx] ) ); + int is_full = fd_funkier_txn_is_full( funk ); + fd_funkier_txn_t * txn = fd_funkier_txn_prepare( funk, parent, &recent_xid[idx1], verbose ); + if( is_full ) FD_TEST( !txn ); + else FD_TEST( txn && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( txn ), &recent_xid[idx1] ) ); + break; + } + + case 8: { /* prepare from live xid with a never seen xid (succeed if not full) */ + if( FD_UNLIKELY( !live_pmap ) ) break; + uint idx; RANDOM_SET_BIT_IDX( live_pmap ); + fd_funkier_txn_t * parent = fd_funkier_txn_query( &recent_xid[idx], &map ); + FD_TEST( parent && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( parent ), &recent_xid[idx] ) ); + fd_funkier_txn_xid_t * xid = &recent_xid[ recent_cursor ]; + *xid = fd_funkier_generate_xid(); + recent_cursor = (recent_cursor+1UL) & 63UL; + int is_full = fd_funkier_txn_is_full( funk ); + fd_funkier_txn_t * txn = fd_funkier_txn_prepare( funk, parent, xid, verbose ); + if( is_full ) FD_TEST( !txn ); + else FD_TEST( txn && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( txn ), xid ) ); + break; + } + + case 9: { /* cancel a live xid (should always be at least 1) */ + if( FD_UNLIKELY( !live_pmap ) ) break; + uint idx; RANDOM_SET_BIT_IDX( live_pmap ); + fd_funkier_txn_t * txn = fd_funkier_txn_query( &recent_xid[idx], &map ); + FD_TEST( txn && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( txn ), &recent_xid[idx] ) ); + FD_TEST( fd_funkier_txn_cancel( funk, txn, verbose )>0UL ); + break; + } + + case 10: { /* cancel a dead xid (should always be 0) */ + if( FD_UNLIKELY( !~live_pmap ) ) break; + uint idx; RANDOM_SET_BIT_IDX( ~live_pmap ); + fd_funkier_txn_t * txn = fd_funkier_txn_query( &recent_xid[idx], &map ); + FD_TEST( !txn ); +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( fd_funkier_txn_cancel( funk, txn, verbose )==0UL ); +#endif + break; + } + + case 11: { /* cancel a never seen xid (should always be 0) */ + fd_funkier_txn_xid_t xid[1]; + xid[0] = fd_funkier_generate_xid(); + fd_funkier_txn_t * txn = fd_funkier_txn_query( xid, &map ); + FD_TEST( !txn ); +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( fd_funkier_txn_cancel( funk, txn, verbose )==0UL ); +#endif + break; + } + + case 12: { /* publish a live xid (should always be at least 1) */ + if( FD_UNLIKELY( !live_pmap ) ) break; + uint idx; RANDOM_SET_BIT_IDX( live_pmap ); + fd_funkier_txn_t * txn = fd_funkier_txn_query( &recent_xid[idx], &map ); + FD_TEST( txn && fd_funkier_txn_xid_eq( fd_funkier_txn_xid( txn ), &recent_xid[idx] ) ); + FD_TEST( fd_funkier_txn_publish( funk, txn, verbose )>0UL ); + FD_TEST( fd_funkier_txn_xid_eq( last_publish, &recent_xid[idx] ) ); + break; + } + + case 13: { /* publish a dead xid (should always be 0) */ + if( FD_UNLIKELY( !~live_pmap ) ) break; + uint idx; RANDOM_SET_BIT_IDX( ~live_pmap ); + fd_funkier_txn_t * txn = fd_funkier_txn_query( &recent_xid[idx], &map ); + FD_TEST( !txn ); +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( fd_funkier_txn_publish( funk, txn, verbose )==0UL ); +#endif + break; + } + + case 14: { /* publish a never seen xid (should always be 0) */ + fd_funkier_txn_xid_t xid[1]; + xid[0] = fd_funkier_generate_xid(); + fd_funkier_txn_t * txn = fd_funkier_txn_query( xid, &map ); + FD_TEST( !txn ); +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( fd_funkier_txn_publish( funk, txn, verbose )==0UL ); +#endif + break; + } + + default: { /* various sanity checks */ + uint idx = r & 63U; r >>= 6; + fd_funkier_txn_t * txn = fd_funkier_txn_query( &recent_xid[idx], &map ); +#ifdef FD_FUNKIER_HANDHOLDING + fd_funkier_txn_xid_t xid[1]; + xid[0] = fd_funkier_generate_xid(); + + fd_funkier_txn_t * dead = NULL; + if( txn_max && !fd_funkier_txn_query( fd_funkier_txn_xid( &pool.ele[0] ), &map ) ) dead = &pool.ele[0]; + + fd_funkier_txn_t bad[1]; fd_funkier_txn_xid_copy( &bad->xid, xid ); + + /* Too many in-prep already tested */ + /* Live xid cases already tested */ + + FD_TEST( !fd_funkier_txn_prepare( NULL, txn, xid, verbose ) ); /* NULL funk */ + FD_TEST( !fd_funkier_txn_prepare( funk, txn, NULL, verbose ) ); /* NULL xid */ + FD_TEST( !fd_funkier_txn_prepare( funk, txn, last_publish, verbose ) ); /* last published xid */ + FD_TEST( !fd_funkier_txn_prepare( funk, bad, xid, verbose ) ); /* Parent not in map */ + if( dead ) FD_TEST( !fd_funkier_txn_prepare( funk, dead, xid, verbose ) ); /* Parent not in prep */ + + FD_TEST( !fd_funkier_txn_cancel( NULL, txn, verbose ) ); /* NULL funk (and maybe NULL txn) */ + FD_TEST( !fd_funkier_txn_cancel( funk, NULL, verbose ) ); /* NULL txn */ + FD_TEST( !fd_funkier_txn_cancel( funk, bad, verbose ) ); /* tx not in map */ + if( dead ) FD_TEST( !fd_funkier_txn_cancel( funk, dead, verbose ) ); /* tx not in prep */ + + FD_TEST( !fd_funkier_txn_publish( NULL, txn, verbose ) ); /* NULL funk (and maybe NULL txn) */ + FD_TEST( !fd_funkier_txn_publish( funk, NULL, verbose ) ); /* NULL txn */ + FD_TEST( !fd_funkier_txn_publish( funk, bad, verbose ) ); /* tx not in map */ + if( dead ) FD_TEST( !fd_funkier_txn_publish( funk, dead, verbose ) ); /* tx not in prep */ +#endif + + if( txn ) { + FD_TEST( fd_funkier_txn_xid_eq( fd_funkier_txn_xid( txn ), &recent_xid[idx] ) ); + + fd_funkier_txn_t * parent = fd_funkier_txn_parent ( txn, &pool ); + fd_funkier_txn_t * first_born = fd_funkier_txn_child_head ( txn, &pool ); + fd_funkier_txn_t * last_born = fd_funkier_txn_child_tail ( txn, &pool ); + fd_funkier_txn_t * older_sib = fd_funkier_txn_sibling_prev( txn, &pool ); + fd_funkier_txn_t * younger_sib = fd_funkier_txn_sibling_next( txn, &pool ); + + /* Make sure transaction suitable marked as frozen */ + + if( !first_born ) FD_TEST( !last_born ); + if( !last_born ) FD_TEST( !first_born ); + + FD_TEST( fd_funkier_txn_is_frozen( txn )==!!first_born ); + + FD_TEST( fd_funkier_txn_is_only_child( txn )==((!older_sib) & (!younger_sib)) ); + + fd_funkier_txn_t * cur; + + /* Make sure txn's children know that txn is the parent (in both + directions) */ + + for( cur = first_born; cur; cur = fd_funkier_txn_sibling_next( cur, &pool ) ) FD_TEST( fd_funkier_txn_parent( cur, &pool )==txn ); + for( cur = last_born; cur; cur = fd_funkier_txn_sibling_prev( cur, &pool ) ) FD_TEST( fd_funkier_txn_parent( cur, &pool )==txn ); + + /* Make sure txn's parent knows this txn is a child (in both + directions) */ + + if( !parent ) cur = fd_funkier_last_publish_child_head( funk, &pool ); + else cur = fd_funkier_txn_child_head( parent, &pool ); + for( ; cur; cur = fd_funkier_txn_sibling_next( cur, &pool ) ) if( cur==txn ) break; + FD_TEST( cur ); + + if( !parent ) cur = fd_funkier_last_publish_child_tail( funk, &pool ); + else cur = fd_funkier_txn_child_tail( parent, &pool ); + for( ; cur; cur = fd_funkier_txn_sibling_prev( cur, &pool ) ) if( cur==txn ) break; + FD_TEST( cur ); + } + + break; + } + } + +#ifdef FD_FUNKIER_HANDHOLDING + FD_TEST( !fd_funkier_verify( funk ) ); +#endif + } + + fd_wksp_free_laddr( fd_funkier_delete( fd_funkier_leave( funk ) ) ); + if( name ) fd_wksp_detach( wksp ); + else fd_wksp_delete_anonymous( wksp ); + + fd_rng_delete( fd_rng_leave( rng ) ); + + FD_LOG_NOTICE(( "pass" )); + fd_halt(); + return 0; +} + +#else + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + FD_LOG_WARNING(( "skip: unit test requires FD_HAS_HOSTED capabilities" )); + fd_halt(); + return 0; +} + +#endif diff --git a/src/funkier/test_funkier_txn2.cxx b/src/funkier/test_funkier_txn2.cxx new file mode 100644 index 0000000000..24badcc65b --- /dev/null +++ b/src/funkier/test_funkier_txn2.cxx @@ -0,0 +1,66 @@ +#include "test_funkier_common.hpp" +#include + +int main(int argc, char** argv) { + (void)argc; + (void)argv; + srand(1234); + + fake_funk ff(&argc, &argv); + ff.verify(); + for (uint loop = 0; loop < 5000U; ++loop) { + for (uint i = 0; i < 10; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_new_txn(); + ff.verify(); + for (uint i = 0; i < 50; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 20; ++i) + ff.random_remove(); + ff.verify(); + ff.random_publish(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_new_txn(); + ff.verify(); + for (uint i = 0; i < 50; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_remove(); + ff.verify(); + ff.random_publish_into_parent(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_new_txn(); + ff.verify(); + for (uint i = 0; i < 50; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_remove(); + ff.verify(); + ff.random_cancel(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_new_txn(); + ff.verify(); + for (uint i = 0; i < 50; ++i) + ff.random_insert(); + ff.verify(); + for (uint i = 0; i < 10; ++i) + ff.random_remove(); + ff.verify(); + ff.random_publish_into_parent(); + ff.verify(); + // ff.random_merge(); + // ff.verify(); + if( loop % 100 == 0 ) FD_LOG_NOTICE(( "iter %u", loop )); + } + + printf("test passed!\n"); + return 0; +} diff --git a/src/funkier/test_funkier_val.c b/src/funkier/test_funkier_val.c new file mode 100644 index 0000000000..e06ad0b9e8 --- /dev/null +++ b/src/funkier/test_funkier_val.c @@ -0,0 +1,246 @@ +#include "fd_funkier.h" + +#if FD_HAS_HOSTED + +#include "test_funkier_common.h" + +FD_STATIC_ASSERT( FD_FUNKIER_REC_VAL_MAX==UINT_MAX, unit_test ); + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + + char const * name = fd_env_strip_cmdline_cstr ( &argc, &argv, "--wksp", NULL, NULL ); + char const * _page_sz = fd_env_strip_cmdline_cstr ( &argc, &argv, "--page-sz", NULL, "gigantic" ); + ulong page_cnt = fd_env_strip_cmdline_ulong( &argc, &argv, "--page-cnt", NULL, 1UL ); + ulong near_cpu = fd_env_strip_cmdline_ulong( &argc, &argv, "--near-cpu", NULL, fd_log_cpu_id() ); + ulong wksp_tag = fd_env_strip_cmdline_ulong( &argc, &argv, "--wksp-tag", NULL, 1234UL ); + ulong seed = fd_env_strip_cmdline_ulong( &argc, &argv, "--seed", NULL, 5678UL ); + ulong txn_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--txn-max", NULL, 32UL ); + ulong rec_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--rec-max", NULL, 128UL ); + ulong iter_max = fd_env_strip_cmdline_ulong( &argc, &argv, "--iter-max", NULL, 1048576UL ); + int verbose = fd_env_strip_cmdline_int ( &argc, &argv, "--verbose", NULL, 0 ); + + fd_rng_t _rng[1]; fd_rng_t * rng = fd_rng_join( fd_rng_new( _rng, 0U, 0UL ) ); + + fd_wksp_t * wksp; + if( name ) { + FD_LOG_NOTICE(( "Attaching to --wksp %s", name )); + wksp = fd_wksp_attach( name ); + } else { + FD_LOG_NOTICE(( "--wksp not specified, using an anonymous local workspace, --page-sz %s, --page-cnt %lu, --near-cpu %lu", + _page_sz, page_cnt, near_cpu )); + wksp = fd_wksp_new_anonymous( fd_cstr_to_shmem_page_sz( _page_sz ), page_cnt, near_cpu, "wksp", 0UL ); + } + + if( FD_UNLIKELY( !wksp ) ) FD_LOG_ERR(( "Unable to attach to wksp" )); + + FD_LOG_NOTICE(( "Testing with --wksp-tag %lu --seed %lu --txn-max %lu --rxn-max %lu --iter-max %lu --verbose %i", + wksp_tag, seed, txn_max, rec_max, iter_max, verbose )); + + fd_funkier_t * tst = fd_funkier_join( fd_funkier_new( fd_wksp_alloc_laddr( wksp, fd_funkier_align(), fd_funkier_footprint( txn_max, rec_max ), wksp_tag ), + wksp_tag, seed, txn_max, rec_max ) ); + if( FD_UNLIKELY( !tst ) ) FD_LOG_ERR(( "Unable to create tst" )); + + fd_funkier_txn_map_t txn_map = fd_funkier_txn_map( tst, wksp ); + fd_alloc_t * alloc = fd_funkier_alloc ( tst, wksp ); + + funk_t * ref = funk_new(); + + for( ulong iter=0UL; itertxn_cnt, ref->rec_cnt )); + + //if( !ref->txn_cnt ) { + // FD_LOG_NOTICE(( "***************************************************************" )); + // for( rec_t * rrec=ref->rec_head; rrec; rrec=rrec->next ) FD_LOG_NOTICE(( "has %lu", rrec->key )); + //} + +# define TEST_TAIL_PADDING(start) do { \ + uchar const * buf = (uchar const *)fd_funkier_val_const( trec, wksp ); \ + ulong end = fd_funkier_val_max( trec ); \ + uchar tmp = (uchar)0; \ + for( ulong off=start; offrec_map_head; + while( rrec ) { + + ulong rxid = rrec->txn ? rrec->txn->xid : 0UL; + ulong rkey = rrec->key; + + xid_set( txid, rxid ); + key_set( tkey, rkey ); + + fd_funkier_rec_query_t rec_query[1]; + fd_funkier_txn_t const * ttxn = rxid ? fd_funkier_txn_query( txid, &txn_map ) : NULL; + fd_funkier_rec_t const * trec = fd_funkier_rec_query_try( tst, ttxn, tkey, rec_query ); + + void const * _val = (void const *)fd_funkier_val( trec, wksp ); + + FD_TEST( !fd_funkier_rec_query_test( rec_query ) ); + + if( rrec->erase ) { + + FD_TEST( !_val ); + + FD_TEST( !fd_funkier_val_sz ( trec ) ); + FD_TEST( !fd_funkier_val_max ( trec ) ); + FD_TEST( !fd_funkier_val_const( trec, wksp ) ); + + } else { + + FD_TEST( _val && FD_LOAD( uint, _val )==rrec->val ); + + FD_TEST( fd_funkier_val_sz ( trec )==sizeof(uint) ); + FD_TEST( fd_funkier_val_max ( trec )>=sizeof(uint) ); + FD_TEST( fd_funkier_val_const( trec, wksp )==(void const *)_val ); + } + + rrec = rrec->map_next; + } + + uint r = fd_rng_uint( rng ); + + uint op = fd_rng_uint_roll( rng, 1U+1U+16U+128U+128U ); + + if( op>=146U ) { /* Insert 8x prepare rate */ + + if( FD_UNLIKELY( fd_funkier_rec_is_full( tst ) ) ) continue; + + ulong idx = fd_rng_ulong_roll( rng, ref->txn_cnt+1UL ); + txn_t * rtxn; + ulong rxid; + if( idxtxn_cnt ) { /* insert into in-prep */ + rtxn = ref->txn_map_head; for( ulong rem=idx; rem; rem-- ) rtxn = rtxn->map_next; + if( txn_is_frozen( rtxn ) ) continue; + rxid = rtxn->xid; + } else { /* insert into last published */ + if( funk_is_frozen( ref ) ) continue; + rtxn = NULL; + rxid = 0UL; + } + + ulong rkey = (ulong)(r & 63U); r >>= 6; + if( rec_query( ref, rtxn, rkey ) ) continue; + + rec_t * rrec = rec_insert( ref, rtxn, rkey ); + + int err = 1; + fd_funkier_rec_prepare_t prepare[1]; + fd_funkier_rec_t * trec = + fd_funkier_rec_prepare( tst, fd_funkier_txn_query( xid_set( txid, rxid ), &txn_map ), key_set( tkey, rkey ), prepare, &err ); + FD_TEST( trec && !err ); + + uint val = (fd_rng_uint( rng )<<2) | 1U; + rrec->val = val; + + memcpy( fd_funkier_val_truncate( trec, sizeof(val), alloc, wksp, NULL ), &val, sizeof(val) ); + + fd_funkier_rec_publish( prepare ); + + FD_TEST( FD_LOAD( uint, fd_funkier_val( trec, wksp ) )==val ); + TEST_TAIL_PADDING( 4UL ); + + } else if( op>=18UL ) { /* Remove and insert at same rate */ + + if( FD_UNLIKELY( !ref->rec_cnt ) ) continue; + + ulong idx = fd_rng_ulong_roll( rng, ref->rec_cnt ); + rec_t * rrec = ref->rec_map_head; for( ulong rem=idx; rem; rem-- ) rrec = rrec->map_next; + + ulong rxid; + if( rrec->txn ) { + if( txn_is_frozen( rrec->txn ) ) continue; + rxid = rrec->txn->xid; + } else { + if( funk_is_frozen( ref ) ) continue; + rxid = 0UL; + } + ulong rkey = rrec->key; + + rec_remove( ref, rrec ); + + fd_funkier_txn_t * ttxn = rxid ? fd_funkier_txn_query( xid_set( txid, rxid ), &txn_map ) : NULL; + FD_TEST( !fd_funkier_rec_remove( tst, ttxn, key_set( tkey, rkey ), NULL, 0UL ) ); + + } else if( op>=2 ) { /* Prepare 8x as publish and cancel combined */ + + if( FD_UNLIKELY( fd_funkier_txn_is_full( tst ) ) ) continue; + + txn_t * rparent; + fd_funkier_txn_t * tparent; + + ulong idx = fd_rng_ulong_roll( rng, ref->txn_cnt+1UL ); + if( idxtxn_cnt ) { /* Branch off in-prep */ + rparent = ref->txn_map_head; for( ulong rem=idx; rem; rem-- ) rparent = rparent->map_next; + tparent = fd_funkier_txn_query( xid_set( txid, rparent->xid ), &txn_map ); + } else { /* Branch off last published */ + rparent = NULL; + tparent = NULL; + } + + ulong rxid = xid_unique(); + txn_prepare( ref, rparent, rxid ); + FD_TEST( fd_funkier_txn_prepare( tst, tparent, xid_set( txid, rxid ), verbose ) ); + + } else if( op>=1UL ) { /* Cancel (same rate as publish) */ + + if( FD_UNLIKELY( !ref->txn_cnt ) ) continue; + + ulong idx = fd_rng_ulong_roll( rng, ref->txn_cnt ); + + txn_t * rtxn = ref->txn_map_head; for( ulong rem=idx; rem; rem-- ) rtxn = rtxn->map_next; + fd_funkier_txn_t * ttxn = fd_funkier_txn_query( xid_set( txid, rtxn->xid ), &txn_map ); + + ulong cnt = ref->txn_cnt; txn_cancel( ref, rtxn ); cnt -= ref->txn_cnt; + FD_TEST( fd_funkier_txn_cancel( tst, ttxn, verbose )==cnt ); + + } else { /* Publish (same rate as cancel) */ + + if( FD_UNLIKELY( !ref->txn_cnt ) ) continue; + + ulong idx = fd_rng_ulong_roll( rng, ref->txn_cnt ); + + txn_t * rtxn = ref->txn_map_head; for( ulong rem=idx; rem; rem-- ) rtxn = rtxn->map_next; + fd_funkier_txn_t * ttxn = fd_funkier_txn_query( xid_set( txid, rtxn->xid ), &txn_map ); + + ulong cnt = txn_publish( ref, rtxn, 0UL ); + FD_TEST( fd_funkier_txn_publish( tst, ttxn, verbose )==cnt ); + + } + } + + funk_delete( ref ); + + fd_wksp_free_laddr( fd_funkier_delete( fd_funkier_leave( tst ) ) ); + if( name ) fd_wksp_detach( wksp ); + else fd_wksp_delete_anonymous( wksp ); + + fd_rng_delete( fd_rng_leave( rng ) ); + + FD_LOG_NOTICE(( "pass" )); + fd_halt(); + return 0; +} + +#else + +int +main( int argc, + char ** argv ) { + fd_boot( &argc, &argv ); + FD_LOG_WARNING(( "skip: unit test requires FD_HAS_HOSTED capabilities" )); + fd_halt(); + return 0; +} + +#endif diff --git a/src/util/tmpl/fd_pool_para.c b/src/util/tmpl/fd_pool_para.c index 24da6fd005..fdabafc535 100644 --- a/src/util/tmpl/fd_pool_para.c +++ b/src/util/tmpl/fd_pool_para.c @@ -505,6 +505,8 @@ POOL_STATIC POOL_ELE_T * POOL_(acquire)( POOL_(t) * join, POOL_ELE_T * sentinel, POOL_STATIC int POOL_(release)( POOL_(t) * join, POOL_ELE_T * ele, int blocking ); +POOL_STATIC int POOL_(is_empty)( POOL_(t) * join ); + POOL_STATIC int POOL_(lock)( POOL_(t) * join, int blocking ); POOL_STATIC void POOL_(reset)( POOL_(t) * join, ulong sentinel_cnt ); @@ -748,6 +750,13 @@ POOL_(release)( POOL_(t) * join, return err; } +POOL_STATIC int +POOL_(is_empty)( POOL_(t) * join ) { + ulong ver_top = join->pool->ver_top; + ulong ele_idx = POOL_(private_vidx_idx)( ver_top ); + return POOL_(idx_is_null)( ele_idx ); +} + POOL_STATIC int POOL_(lock)( POOL_(t) * join, int blocking ) {