Line data Source code
1 : #define _GNU_SOURCE
2 : #include "fd_accdb.h"
3 : #include "fd_accdb_shmem.h"
4 : #include "fd_accdb_private.h"
5 :
6 : #if FD_TMPL_USE_HANDHOLDING
7 : #include "../../ballet/txn/fd_txn.h"
8 : #include "../../ballet/base58/fd_base58.h"
9 : #endif
10 : #include "../../util/racesan/fd_racesan_target.h"
11 :
12 : #include "../../disco/events/generated/fd_event_gen.h"
13 :
14 : FD_STATIC_ASSERT( sizeof(fd_accdb_cache_line_t)==FD_ACCDB_CACHE_META_SZ, cache_meta_sz );
15 :
16 : #if FD_HAS_RACESAN
17 : /* Test-only telemetry: background_compact publishes the pubkey + dest
18 : offset of the record it is about to relocation-CAS at the
19 : accdb_compact:pre_offset_cas hook, so test_accdb_racesan can PROVE the
20 : parked relocation is the account it set up (avoiding a vacuous test).
21 : Zero-cost / absent in production (racesan off). */
22 : uchar fd_accdb_dbg_reloc_pubkey[ 32UL ];
23 : ulong fd_accdb_dbg_reloc_dest;
24 : ulong fd_accdb_dbg_reloc_cnt;
25 : #endif
26 :
27 : #include <stddef.h>
28 : #include <unistd.h>
29 : #include <fcntl.h>
30 : #include <errno.h>
31 : #include <sys/uio.h>
32 :
33 : struct fd_accdb_fork {
34 : fd_accdb_fork_shmem_t * shmem;
35 : descends_set_t * descends;
36 : };
37 :
38 : typedef struct fd_accdb_fork fd_accdb_fork_t;
39 :
40 305103 : #define FD_ACCDB_ACQUIRE_STATE_IDLE (0)
41 351 : #define FD_ACCDB_ACQUIRE_STATE_PHASE_A (1)
42 301107 : #define FD_ACCDB_ACQUIRE_STATE_OPEN (2)
43 :
44 : struct __attribute__((aligned(FD_ACCDB_ALIGN))) fd_accdb_private {
45 : int fd;
46 :
47 : int acquire_state;
48 :
49 : fd_accdb_shmem_t * shmem;
50 :
51 : fd_accdb_fork_t * fork_pool;
52 : fork_pool_t fork_shmem_pool[1];
53 :
54 : fd_accdb_accmeta_t * acc_pool;
55 : acc_pool_t acc_pool_join[1];
56 : uint * acc_map;
57 :
58 : uchar * cache [ FD_ACCDB_CACHE_CLASS_CNT ];
59 :
60 : fd_accdb_partition_t * partition_pool;
61 : compaction_dlist_t * compaction_dlist[ FD_ACCDB_COMPACTION_LAYER_CNT ];
62 : deferred_free_dlist_t * deferred_free_dlist;
63 :
64 : txn_pool_t txn_pool[1];
65 :
66 : /* Pointer into shmem->joiner_epochs[ my_slot ].val for writer
67 : joiners, or into a private per-tile fseq for read-only joiners.
68 : Set to the current global epoch on entry to an epoch-protected
69 : operation, and ULONG_MAX on exit. Used to determine when
70 : deferred frees are safe. */
71 : ulong * my_epoch_slot;
72 :
73 : /* Read-only pointers to external epoch slots (e.g. fseqs owned by
74 : RO consumer tiles like the rpc tile). Scanned in addition to
75 : shmem->joiner_epochs[] by compaction's deferred-free
76 : reclamation. Borrowed; the caller of fd_accdb_new owns the
77 : storage. */
78 : ulong const * const * external_epoch_slots;
79 : ulong external_epoch_cnt;
80 :
81 : /* Side buffer of acc pool indices that have been CAS-unlinked from
82 : their hash chains but cannot be released back to acc_pool yet,
83 : because concurrent readers (acquire / compact) may still be
84 : traversing the removed nodes via map.next. The batch is released
85 : once all joiner_epochs exceed shmem->deferred_acc_epoch. Indices
86 : are written here (not into pool.next) until after the epoch drain
87 : because pool.next is union-aliased to cache_idx, which a concurrent
88 : cold_load_acc may still write through a captured pointer. Backed
89 : by shmem->deferred_acc_buf_off; cnt and epoch live in shmem too. */
90 : uint * deferred_acc_buf;
91 :
92 : /* Chain of fork pool slots whose IDs are still potentially
93 : referenced by concurrent readers (via descends_set_test or
94 : root_fork_id snapshot). The chain is released back to fork_pool
95 : once all joiner_epochs exceed deferred_fork_epoch. NULL head
96 : means no deferred forks. */
97 : fd_accdb_fork_shmem_t * deferred_fork_head;
98 : fd_accdb_fork_shmem_t * deferred_fork_tail;
99 : ulong deferred_fork_epoch;
100 :
101 : fd_accdb_metrics_t metrics[1];
102 :
103 : /* Set by fd_accdb_snapshot_load_begin/end. When non-zero, layer-0
104 : partition handoffs (in change_partition) re-tier the partitions
105 : that fell out of the snapshot-load working set: P-2 to Warm and
106 : P-3 to Cold. This backfills tiering for snapshot-loaded data
107 : that never gets a second write (and therefore would otherwise
108 : never be promoted by compaction). */
109 : int snapshot_loading;
110 :
111 : /* Track account addresses changed since a full snap.
112 : Used to determine which accounts should be packed into a full
113 : snapshot (including tombstones for accounts no longer present in
114 : accdb, but present in the full snapshot). */
115 : struct {
116 : uint * chains;
117 : fd_accdb_delta_t * pool;
118 : struct {
119 : uchar const * pubkey;
120 : uint chain;
121 : } scratch[ FD_ACCDB_MAX_ACQUIRE_CNT ];
122 : } delta;
123 :
124 : /* Write counters that are not published yet. Metrics are aggregated
125 : in batches to avoid expensive atomic operations on each write.
126 : 64-byte aligned to fit in a single cache line. */
127 : struct {
128 : ulong bytes; /* bytes reserved on partition_idx */
129 : ulong num_ops; /* reservations behind those bytes */
130 : ulong partition_idx; /* set while num_ops>0 */
131 : } write_stats __attribute__((aligned(64)));
132 : };
133 :
134 : static inline fd_accdb_cache_line_t *
135 : cache_line( fd_accdb_t * accdb,
136 : ulong cls,
137 2254683 : ulong idx ) {
138 2254683 : return (fd_accdb_cache_line_t *)( accdb->cache[ cls ] + idx * fd_accdb_cache_slot_sz[ cls ] );
139 2254683 : }
140 :
141 : /* Bump the per-partition read counters for the partition that contains
142 : file_offset. Called at preadv2 sites. Writes are counted at
143 : allocate time (see fd_accdb_partition_write_bump) so that they reflect
144 : bytes committed to a partition rather than syscalls — the snapshot
145 : loader bypasses pwritev2 entirely, but every write still goes through
146 : reserve_next_write. */
147 : static inline void
148 : fd_accdb_partition_read_bump( fd_accdb_t * accdb,
149 : ulong file_offset,
150 42 : ulong bytes ) {
151 42 : if( FD_UNLIKELY( !bytes ) ) return;
152 : /* Readonly joiners have no partition_pool join (see
153 : fd_accdb_join_readonly) and do not contribute to per-partition
154 : read telemetry today; their disk reads still show up in the
155 : joiner-local fd_accdb_metrics_t bytes_read/read_ops. */
156 42 : if( FD_UNLIKELY( !accdb->partition_pool ) ) return;
157 42 : ulong partition_idx = file_offset / accdb->shmem->partition_sz;
158 42 : fd_accdb_partition_t * p = partition_pool_ele( accdb->partition_pool, partition_idx );
159 42 : if( FD_UNLIKELY( !p ) ) return;
160 42 : FD_ATOMIC_FETCH_AND_ADD( &p->bytes_read, bytes );
161 42 : FD_ATOMIC_FETCH_AND_ADD( &p->read_ops, 1UL );
162 42 : }
163 :
164 : /* Bump the per-partition write counters. bytes is how much landed on
165 : this partition, over num_ops reservations. */
166 : static inline void
167 : fd_accdb_partition_write_bump( fd_accdb_t * accdb,
168 : ulong partition_idx,
169 : ulong bytes,
170 57 : ulong num_ops ) {
171 57 : if( FD_UNLIKELY( !bytes ) ) return;
172 57 : fd_accdb_partition_t * p = partition_pool_ele( accdb->partition_pool, partition_idx );
173 57 : if( FD_UNLIKELY( !p ) ) return;
174 57 : FD_ATOMIC_FETCH_AND_ADD( &p->bytes_written, bytes );
175 57 : FD_ATOMIC_FETCH_AND_ADD( &p->write_ops, num_ops );
176 57 : }
177 :
178 : void
179 69 : fd_accdb_flush_metrics( fd_accdb_t * accdb ) {
180 69 : ulong bytes = accdb->write_stats.bytes;
181 69 : ulong num_ops = accdb->write_stats.num_ops;
182 69 : ulong part_idx = accdb->write_stats.partition_idx;
183 :
184 69 : if( !num_ops ) return;
185 :
186 54 : memset( &accdb->write_stats, 0, sizeof(accdb->write_stats) );
187 :
188 54 : FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->shmetrics->disk_current_bytes, bytes );
189 54 : fd_accdb_partition_write_bump( accdb, part_idx, bytes, num_ops );
190 54 : }
191 :
192 : static inline ulong
193 : cache_line_idx( fd_accdb_t * accdb,
194 : ulong cls,
195 1935642 : fd_accdb_cache_line_t const * line ) {
196 1935642 : return (ulong)( (uchar const *)line - accdb->cache[ cls ] ) / fd_accdb_cache_slot_sz[ cls ];
197 1935642 : }
198 :
199 : #if FD_TMPL_USE_HANDHOLDING
200 : static inline int
201 : fd_accdb_ptr_in_region( fd_accdb_t const * accdb,
202 : ulong cls,
203 : void const * ptr ) {
204 : if( FD_UNLIKELY( cls>=FD_ACCDB_CACHE_CLASS_CNT ) ) return 0;
205 :
206 : uchar const * base = accdb->cache[ cls ];
207 : if( FD_UNLIKELY( !base ) ) return 0;
208 :
209 : ulong slot_sz = fd_accdb_cache_slot_sz[ cls ];
210 : ulong region_sz = accdb->shmem->cache_class_max[ cls ] * slot_sz;
211 : uchar const * p = (uchar const *)ptr;
212 :
213 : if( FD_UNLIKELY( p<base || p>=base+region_sz ) ) return 0;
214 : return ( (ulong)( p - base ) % slot_sz )==FD_ACCDB_CACHE_META_SZ;
215 : }
216 : #endif
217 :
218 : FD_FN_CONST ulong
219 12600 : fd_accdb_align( void ) {
220 12600 : return FD_ACCDB_ALIGN;
221 12600 : }
222 :
223 : FD_FN_CONST ulong
224 273 : fd_accdb_footprint( ulong max_live_slots ) {
225 273 : ulong l;
226 273 : l = FD_LAYOUT_INIT;
227 273 : l = FD_LAYOUT_APPEND( l, FD_ACCDB_ALIGN, sizeof(fd_accdb_t) );
228 273 : l = FD_LAYOUT_APPEND( l, alignof(fd_accdb_fork_t), max_live_slots*sizeof(fd_accdb_fork_t) );
229 273 : return FD_LAYOUT_FINI( l, FD_ACCDB_ALIGN );
230 273 : }
231 :
232 : void *
233 : fd_accdb_new( void * ljoin,
234 : fd_accdb_shmem_t * shmem,
235 : int fd,
236 : ulong external_epoch_cnt,
237 4110 : ulong const ** external_epoch_slots ) {
238 4110 : if( FD_UNLIKELY( !ljoin ) ) {
239 0 : FD_LOG_WARNING(( "NULL ljoin" ));
240 0 : return NULL;
241 0 : }
242 :
243 4110 : if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)ljoin, fd_accdb_align() ) ) ) {
244 0 : FD_LOG_WARNING(( "misaligned ljoin" ));
245 0 : return NULL;
246 0 : }
247 :
248 4110 : if( FD_UNLIKELY( fd<0 ) ) {
249 0 : FD_LOG_WARNING(( "fd must be a valid file descriptor" ));
250 0 : return NULL;
251 0 : }
252 :
253 4110 : ulong max_live_slots = shmem->max_live_slots;
254 4110 : ulong max_accounts = shmem->max_accounts;
255 4110 : ulong max_account_writes_per_slot = shmem->max_account_writes_per_slot;
256 4110 : ulong partition_cnt = shmem->partition_cnt;
257 :
258 4110 : ulong chain_cnt = fd_ulong_pow2_up( (max_accounts>>1) + (max_accounts&1UL) );
259 4110 : ulong txn_max = max_live_slots * max_account_writes_per_slot;
260 :
261 4110 : FD_SCRATCH_ALLOC_INIT( l, shmem );
262 4110 : FD_SCRATCH_ALLOC_APPEND( l, FD_ACCDB_SHMEM_ALIGN, sizeof(fd_accdb_shmem_t) );
263 4110 : void * _fork_pool_ele = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_accdb_fork_shmem_t), max_live_slots*sizeof(fd_accdb_fork_shmem_t) );
264 4110 : void * _descends_sets = FD_SCRATCH_ALLOC_APPEND( l, descends_set_align(), max_live_slots*descends_set_footprint( max_live_slots ) );
265 4110 : void * _acc_map = FD_SCRATCH_ALLOC_APPEND( l, alignof(uint), chain_cnt*sizeof(uint) );
266 4110 : void * _acc_pool_ele = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_accdb_accmeta_t), max_accounts*sizeof(fd_accdb_accmeta_t) );
267 4110 : void * _txn_pool_ele = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_accdb_txn_t), txn_max*sizeof(fd_accdb_txn_t) );
268 4110 : void * _partition_pool = FD_SCRATCH_ALLOC_APPEND( l, partition_pool_align(), partition_pool_footprint( partition_cnt ) );
269 4110 : void * _compaction_dlists[ FD_ACCDB_COMPACTION_LAYER_CNT ];
270 16440 : for( ulong k=0UL; k<FD_ACCDB_COMPACTION_LAYER_CNT; k++ ) {
271 12330 : _compaction_dlists[ k ] = FD_SCRATCH_ALLOC_APPEND( l, compaction_dlist_align(), compaction_dlist_footprint() );
272 12330 : }
273 4110 : void * _deferred_free_dlist = FD_SCRATCH_ALLOC_APPEND( l, deferred_free_dlist_align(), deferred_free_dlist_footprint() );
274 :
275 4110 : FD_SCRATCH_ALLOC_INIT( l2, ljoin );
276 4110 : fd_accdb_t * accdb = FD_SCRATCH_ALLOC_APPEND( l2, fd_accdb_align(), sizeof(fd_accdb_t) );
277 4110 : void * _local_fork_pool = FD_SCRATCH_ALLOC_APPEND( l2, alignof(fd_accdb_fork_t), max_live_slots*sizeof(fd_accdb_fork_t) );
278 :
279 4110 : accdb->fd = fd;
280 4110 : accdb->acquire_state = FD_ACCDB_ACQUIRE_STATE_IDLE;
281 4110 : accdb->snapshot_loading = 0;
282 :
283 4110 : accdb->shmem = (fd_accdb_shmem_t *)shmem;
284 4110 : FD_TEST( acc_pool_join( accdb->acc_pool_join, shmem->acc_pool, _acc_pool_ele, max_accounts ) );
285 4110 : accdb->acc_pool = accdb->acc_pool_join->ele;
286 4110 : accdb->acc_map = _acc_map;
287 4110 : FD_TEST( txn_pool_join( accdb->txn_pool, shmem->txn_pool, _txn_pool_ele, txn_max ) );
288 36990 : for( ulong c=0UL; c<FD_ACCDB_CACHE_CLASS_CNT; c++ ) accdb->cache[ c ] = (uchar *)shmem + shmem->cache_region_off[ c ];
289 4110 : accdb->partition_pool = partition_pool_join( _partition_pool );
290 4110 : FD_TEST( accdb->partition_pool );
291 16440 : for( ulong k=0UL; k<FD_ACCDB_COMPACTION_LAYER_CNT; k++ ) {
292 12330 : accdb->compaction_dlist[ k ] = compaction_dlist_join( _compaction_dlists[ k ] );
293 12330 : FD_TEST( accdb->compaction_dlist[ k ] );
294 12330 : }
295 4110 : accdb->deferred_free_dlist = deferred_free_dlist_join( _deferred_free_dlist );
296 4110 : FD_TEST( accdb->deferred_free_dlist );
297 :
298 4110 : FD_TEST( fork_pool_join( accdb->fork_shmem_pool, shmem->fork_pool, _fork_pool_ele, max_live_slots ) );
299 4110 : accdb->fork_pool = _local_fork_pool;
300 73527 : for( ulong i=0UL; i<max_live_slots; i++ ) {
301 69417 : fd_accdb_fork_t * fork = &accdb->fork_pool[ i ];
302 69417 : fork->shmem = fork_pool_ele( accdb->fork_shmem_pool, i );
303 69417 : fork->descends = descends_set_join( (uchar *)_descends_sets + i*descends_set_footprint( max_live_slots ) );
304 69417 : FD_TEST( fork->shmem );
305 69417 : FD_TEST( fork->descends );
306 69417 : }
307 :
308 4110 : ulong epoch_idx = FD_ATOMIC_FETCH_AND_ADD( &shmem->joiner_cnt, 1UL );
309 4110 : FD_TEST( epoch_idx<shmem->joiner_cnt_max );
310 4110 : accdb->my_epoch_slot = &shmem->joiner_epochs[ epoch_idx ].val;
311 :
312 4110 : accdb->external_epoch_slots = external_epoch_slots;
313 4110 : accdb->external_epoch_cnt = external_epoch_cnt;
314 :
315 4110 : accdb->deferred_acc_buf = (uint *)( (uchar *)shmem + shmem->deferred_acc_buf_off );
316 :
317 4110 : accdb->delta.chains = (uint *) ( (uchar *)shmem + shmem->delta.chain_off );
318 4110 : accdb->delta.pool = (fd_accdb_delta_t *) ( (uchar *)shmem + shmem->delta.ele_off );
319 :
320 4110 : accdb->deferred_fork_head = NULL;
321 4110 : accdb->deferred_fork_tail = NULL;
322 4110 : accdb->deferred_fork_epoch = 0UL;
323 :
324 4110 : memset( accdb->metrics, 0, sizeof(fd_accdb_metrics_t) );
325 4110 : memset( &accdb->write_stats, 0, sizeof(accdb->write_stats) );
326 :
327 4110 : return accdb;
328 4110 : }
329 :
330 : static inline void wait_cmd( fd_accdb_t * accdb );
331 : static inline void submit_cmd( fd_accdb_t * accdb, uint op, ushort fork_id );
332 :
333 : void
334 3 : fd_accdb_reset( fd_accdb_t * accdb ) {
335 3 : fd_accdb_shmem_t * shmem = accdb->shmem;
336 :
337 : /* Wait for any pending background command (advance_root / purge) on
338 : T2 to finish before clobbering shared state. */
339 3 : wait_cmd( accdb );
340 :
341 : /* Reset pools through the joiner's existing pointers. acc_pool and
342 : txn_pool use POOL_LAZY=1 so reset is O(1). fork_pool and
343 : partition_pool rebuild their free lists in O(max_live_slots) and
344 : O(partition_cnt), both small. */
345 3 : acc_pool_reset( accdb->acc_pool_join );
346 3 : txn_pool_reset( accdb->txn_pool );
347 3 : fork_pool_reset( accdb->fork_shmem_pool );
348 3 : partition_pool_reset( accdb->partition_pool );
349 :
350 : /* Clear hash chains */
351 3 : fd_memset( accdb->acc_map, 0xFF, shmem->chain_cnt*sizeof(uint) );
352 :
353 : /* Empty dlists */
354 12 : for( ulong k=0UL; k<FD_ACCDB_COMPACTION_LAYER_CNT; k++ ) {
355 9 : compaction_dlist_remove_all( accdb->compaction_dlist[ k ], accdb->partition_pool );
356 9 : }
357 3 : deferred_free_dlist_remove_all( accdb->deferred_free_dlist, accdb->partition_pool );
358 :
359 : /* Null descends_sets. */
360 195 : for( ulong i=0UL; i<shmem->max_live_slots; i++ ) {
361 192 : descends_set_null( accdb->fork_pool[ i ].descends );
362 192 : }
363 :
364 : /* Reset shmem scalar fields. */
365 3 : shmem->root_fork_id = (fd_accdb_fork_id_t){ .val = USHORT_MAX };
366 3 : shmem->generation = 0U;
367 3 : shmem->partition_lock = 0;
368 3 : shmem->partition_max = 0UL;
369 :
370 : /* Write heads: sentinel values that force partition-switch on first
371 : write. */
372 12 : for( ulong k=0UL; k<FD_ACCDB_COMPACTION_LAYER_CNT; k++ ) {
373 9 : shmem->whead[ k ] = accdb_offset( shmem->partition_cnt, shmem->partition_sz );
374 9 : shmem->has_partition[ k ] = 0;
375 9 : }
376 :
377 : /* Cache state */
378 27 : for( ulong c=0UL; c<FD_ACCDB_CACHE_CLASS_CNT; c++ ) {
379 24 : shmem->clock_hand[ c ].val = 0UL;
380 24 : shmem->cache_free[ c ].ver_top = (ulong)UINT_MAX;
381 24 : shmem->cache_free_cnt[ c ].val = 0UL;
382 24 : shmem->cache_class_init[ c ].val = 0UL;
383 24 : if( shmem->cache_class_max[ c ]>=shmem->cache_min_reserved*shmem->joiner_cnt_max )
384 24 : shmem->cache_class_used[ c ].val = ULONG_MAX;
385 0 : else
386 0 : shmem->cache_class_used[ c ].val = 0UL;
387 24 : }
388 :
389 : /* Reset every cache slot's metadata to empty sentinels. */
390 27 : for( ulong c=0UL; c<FD_ACCDB_CACHE_CLASS_CNT; c++ ) {
391 24 : ulong slot_sz = fd_accdb_cache_slot_sz[ c ];
392 82200 : for( ulong i=0UL; i<shmem->cache_class_max[ c ]; i++ ) {
393 82176 : fd_accdb_cache_line_t * line = (fd_accdb_cache_line_t *)( accdb->cache[ c ] + i*slot_sz );
394 82176 : line->key.generation = UINT_MAX;
395 82176 : line->acc_idx = UINT_MAX;
396 82176 : line->refcnt = 0U;
397 82176 : line->referenced = 0;
398 82176 : line->persisted = 1;
399 82176 : }
400 24 : }
401 :
402 : /* Epoch system: reset epoch and all slot values to idle, but
403 : preserve joiner_cnt and each tile's my_epoch_slot pointer so that
404 : tiles which joined during init keep their original slot indices. */
405 3 : shmem->epoch = 1UL;
406 771 : for( ulong i=0UL; i<FD_ACCDB_MAX_JOINERS; i++ ) shmem->joiner_epochs[ i ].val = ULONG_MAX;
407 :
408 : /* Deferred acc buffer. */
409 3 : shmem->deferred_acc_buf_cnt = 0UL;
410 3 : shmem->deferred_acc_epoch = 0UL;
411 :
412 : /* Shared metrics: zero gauges that reflect current state (now empty)
413 : but preserve counters and accounts_capacity. */
414 3 : shmem->shmetrics->accounts_total = 0UL;
415 3 : shmem->shmetrics->disk_allocated_bytes = 0UL;
416 3 : shmem->shmetrics->disk_current_bytes = 0UL;
417 3 : shmem->shmetrics->disk_used_bytes = 0UL;
418 3 : shmem->shmetrics->in_compaction = 0;
419 :
420 : /* Command slot */
421 3 : shmem->cmd_op = FD_ACCDB_CMD_IDLE;
422 3 : shmem->cmd_fork_id = USHORT_MAX;
423 :
424 3 : shmem->snapshot_loading = 0;
425 :
426 3 : FD_COMPILER_MFENCE();
427 :
428 : /* Tell the accdb tile to clear its stale deferred fork chain.
429 : Its deferred_fork_head/tail now reference recycled pool elements;
430 : it must discard them before processing any future advance_root or
431 : purge command. The command is asynchronous; the next advance_root
432 : or purge call will wait for it to complete via wait_cmd. */
433 3 : submit_cmd( accdb, FD_ACCDB_CMD_CLEAR_DEFERRED, 0 );
434 :
435 : /* Reset local state */
436 3 : accdb->deferred_fork_head = NULL;
437 3 : accdb->deferred_fork_tail = NULL;
438 3 : accdb->deferred_fork_epoch = 0UL;
439 3 : accdb->snapshot_loading = 0;
440 3 : accdb->acquire_state = FD_ACCDB_ACQUIRE_STATE_IDLE;
441 3 : memset( &accdb->write_stats, 0, sizeof(accdb->write_stats) );
442 3 : }
443 :
444 : void
445 21 : fd_accdb_snapshot_load_begin( fd_accdb_t * accdb ) {
446 21 : accdb->snapshot_loading = 1;
447 21 : FD_VOLATILE( accdb->shmem->snapshot_loading ) = 1;
448 21 : }
449 :
450 : static inline void
451 : change_partition( fd_accdb_t * accdb,
452 : accdb_offset_t const * offset_before,
453 : accdb_offset_t * out_offset,
454 : int * has_partition,
455 : uchar layer );
456 :
457 : void
458 21 : fd_accdb_snapshot_load_end( fd_accdb_t * accdb ) {
459 21 : fd_accdb_flush_metrics( accdb );
460 21 : spin_lock_acquire( &accdb->shmem->partition_lock );
461 :
462 : /* Force the next layer-0 write onto a fresh Hot partition so we do
463 : not keep appending live execution writes to the tail of a partition
464 : that was tagged Cold during snapshot load. Must run while
465 : snapshot_loading is still set so the partition we just closed
466 : (the snapshot-tagged Cold one) is not enqueued for compaction by
467 : change_partition's tail-credit try_enqueue. change_partition will
468 : retag the newly-allocated partition as Cold (because the flag is
469 : still set), so we fix it back to Hot below. */
470 21 : if( FD_LIKELY( accdb->shmem->has_partition[ 0 ] ) ) {
471 21 : change_partition( accdb, &accdb->shmem->whead[ 0 ], &accdb->shmem->whead[ 0 ], &accdb->shmem->has_partition[ 0 ], 0 );
472 21 : ulong new_idx = packed_partition_idx( &accdb->shmem->whead[ 0 ] );
473 21 : fd_accdb_partition_t * newp = partition_pool_ele( accdb->partition_pool, new_idx );
474 21 : FD_VOLATILE( newp->layer ) = 0;
475 21 : }
476 :
477 21 : accdb->snapshot_loading = 0;
478 21 : FD_VOLATILE( accdb->shmem->snapshot_loading ) = 0;
479 :
480 : /* Sweep all partitions written during the load — any that crossed
481 : the fragmentation threshold while enqueue was suppressed are
482 : re-checked now and pushed onto the compaction queue. */
483 21 : ulong partition_max = accdb->shmem->partition_max;
484 90 : for( ulong p=0UL; p<partition_max; p++ ) {
485 69 : fd_accdb_shmem_try_enqueue_compaction( accdb->shmem, p );
486 69 : }
487 :
488 21 : spin_lock_release( &accdb->shmem->partition_lock );
489 21 : }
490 :
491 : static inline uint
492 : delta_chain( fd_accdb_shmem_t const * accdb,
493 0 : uchar const pubkey[ 32 ] ) {
494 0 : uint hash = (uint)fd_accdb_hash( pubkey, accdb->delta.seed );
495 0 : return hash & accdb->delta.chain_mask;
496 0 : }
497 :
498 : static int
499 : delta_insert( fd_accdb_t * accdb,
500 1518 : uchar const pubkey[ 32 ] ) {
501 : /* FIXME consider batch inserting */
502 1518 : fd_accdb_shmem_t * shmem = accdb->shmem;
503 1518 : if( FD_UNLIKELY( shmem->delta.head >= shmem->delta.ele_max ) ) return 0;
504 :
505 0 : uint * chains = accdb->delta.chains;
506 0 : uint * chain = &chains[ delta_chain( shmem, pubkey ) ];
507 0 : fd_accdb_delta_t * pool = accdb->delta.pool;
508 :
509 0 : uint head = *chain;
510 0 : for( uint cur=head; cur!=UINT_MAX; cur=pool[ cur ].next ) {
511 0 : if( FD_UNLIKELY( !memcmp( pool[ cur ].pubkey, pubkey, 32UL ) ) ) return 1;
512 0 : }
513 :
514 0 : ulong idx = shmem->delta.head++;
515 0 : fd_accdb_delta_t * delta = &pool[ idx ];
516 0 : delta->next = head;
517 0 : memcpy( delta->pubkey, pubkey, 32UL );
518 0 : *chain = (uint)idx;
519 0 : return 1;
520 0 : }
521 :
522 : int
523 : fd_accdb_snapshot_recover_delta( fd_accdb_t * accdb,
524 0 : fd_accdb_fork_id_t fork_id ) {
525 0 : if( FD_UNLIKELY( fork_id.val>=fork_pool_ele_max( accdb->fork_shmem_pool ) ) ) {
526 0 : FD_LOG_CRIT(( "fd_accdb_snapshot_populate_delta: invalid fork id %u (capacity %lu)",
527 0 : (uint)fork_id.val, fork_pool_ele_max( accdb->fork_shmem_pool ) ));
528 0 : }
529 :
530 0 : uint txn_idx = accdb->fork_pool[ fork_id.val ].shmem->txn_head;
531 0 : while( txn_idx!=UINT_MAX ) {
532 0 : fd_accdb_txn_t const * txn = txn_pool_ele( accdb->txn_pool, (ulong)txn_idx );
533 0 : fd_accdb_accmeta_t const * acc = &accdb->acc_pool[ txn->acc_pool_idx ];
534 0 : if( FD_UNLIKELY( !delta_insert( accdb, acc->key.pubkey ) ) ) return -1;
535 0 : txn_idx = txn->fork.next;
536 0 : }
537 0 : return 0;
538 0 : }
539 :
540 : void
541 : fd_accdb_snapshot_save_whead( fd_accdb_t * accdb,
542 12 : fd_accdb_snapshot_recovery_t * out ) {
543 : /* Flush metrics to update disk_current_bytes. */
544 12 : fd_accdb_flush_metrics( accdb );
545 :
546 12 : out->whead_val = FD_VOLATILE_CONST( accdb->shmem->whead[ 0 ].val );
547 12 : out->has_partition = FD_VOLATILE_CONST( accdb->shmem->has_partition[ 0 ] );
548 12 : out->partition_max = FD_VOLATILE_CONST( accdb->shmem->partition_max );
549 12 : out->disk_current_bytes = FD_VOLATILE_CONST( accdb->shmem->shmetrics->disk_current_bytes );
550 :
551 12 : if( out->has_partition ) {
552 12 : accdb_offset_t whead = { .val = out->whead_val };
553 12 : ulong idx = packed_partition_idx( &whead );
554 12 : fd_accdb_partition_t * part = partition_pool_ele( accdb->partition_pool, idx );
555 12 : out->savepoint_bytes_freed = FD_VOLATILE_CONST( part->bytes_freed );
556 12 : } else {
557 0 : out->savepoint_bytes_freed = 0UL;
558 0 : }
559 12 : }
560 :
561 : void
562 : fd_accdb_snapshot_revert_whead( fd_accdb_t * accdb,
563 12 : fd_accdb_snapshot_recovery_t const * recover ) {
564 12 : fd_accdb_shmem_t * shmem = accdb->shmem;
565 :
566 : /* Partitions are about to be released, so flush metrics first. */
567 12 : fd_accdb_flush_metrics( accdb );
568 :
569 : /* Wait for any pending background command (purge) on T2 to finish
570 : before releasing partitions. */
571 12 : wait_cmd( accdb );
572 :
573 12 : ulong cur_partition_max = shmem->partition_max;
574 :
575 : /* Materialize the active partition's write_offset from the whead
576 : before releasing. Closed partitions have write_offset set by
577 : change_partition, but the last active partition still has
578 : write_offset == 0 from its initialization. The real byte offset
579 : is encoded in whead[0]. */
580 12 : if( shmem->has_partition[ 0 ] && cur_partition_max>recover->partition_max ) {
581 6 : ulong active_idx = packed_partition_idx( &shmem->whead[ 0 ] );
582 6 : if( active_idx>=recover->partition_max && active_idx<cur_partition_max ) {
583 6 : fd_accdb_partition_t * active = partition_pool_ele( accdb->partition_pool, active_idx );
584 6 : active->write_offset = packed_partition_offset( &shmem->whead[ 0 ] );
585 6 : }
586 6 : }
587 :
588 : /* Release partitions that have been previously allocated. Must hold
589 : partition_lock because partition_pool_ele_release mutates the
590 : pool free list. Before releasing, unlink any partition that sits
591 : on a compaction dlist (queued flag).
592 :
593 : Release in descending index order so that the LIFO free list
594 : re-acquires them in ascending order (P, P+1, P+2, ...). This
595 : keeps reserve_next_write in sync with snapwr, which advances
596 : its flat file offset sequentially. */
597 12 : spin_lock_acquire( &shmem->partition_lock );
598 24 : for( ulong p=cur_partition_max; p>recover->partition_max; p-- ) {
599 12 : fd_accdb_partition_t * part = partition_pool_ele( accdb->partition_pool, p-1UL );
600 12 : if( FD_UNLIKELY( part->queued ) ) {
601 6 : compaction_dlist_ele_remove( accdb->compaction_dlist[ part->layer ], part, accdb->partition_pool );
602 6 : }
603 12 : partition_pool_ele_release( accdb->partition_pool, part );
604 12 : }
605 :
606 12 : shmem->whead[ 0 ].val = recover->whead_val;
607 12 : shmem->has_partition[ 0 ] = recover->has_partition;
608 12 : shmem->partition_max = recover->partition_max;
609 :
610 : /* disk_used_bytes is NOT saved/restored here. It is implicitly
611 : reverted by purge_inner -> acc_unlink, which decrements
612 : disk_used_bytes for each unlinked entry. The caller must
613 : complete the purge before calling revert_whead. */
614 :
615 12 : shmem->shmetrics->disk_current_bytes = recover->disk_current_bytes;
616 12 : shmem->shmetrics->disk_allocated_bytes = recover->partition_max * shmem->partition_sz;
617 :
618 12 : if( recover->has_partition ) {
619 12 : accdb_offset_t sp_off = (accdb_offset_t){ .val = recover->whead_val };
620 12 : ulong sp_idx = packed_partition_idx( &sp_off );
621 12 : fd_accdb_partition_t * sp = partition_pool_ele( accdb->partition_pool, sp_idx );
622 12 : sp->bytes_freed = recover->savepoint_bytes_freed;
623 12 : sp->write_offset = 0UL;
624 12 : }
625 :
626 12 : spin_lock_release( &shmem->partition_lock );
627 12 : }
628 :
629 : fd_accdb_t *
630 4110 : fd_accdb_join( void * shaccdb ) {
631 4110 : if( FD_UNLIKELY( !shaccdb ) ) {
632 0 : FD_LOG_WARNING(( "NULL shaccdb" ));
633 0 : return NULL;
634 0 : }
635 :
636 4110 : if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shaccdb, fd_accdb_align() ) ) ) {
637 0 : FD_LOG_WARNING(( "misaligned shaccdb" ));
638 0 : return NULL;
639 0 : }
640 :
641 4110 : return (fd_accdb_t*)shaccdb;
642 4110 : }
643 :
644 : fd_accdb_t *
645 : fd_accdb_join_readonly( void * ljoin,
646 : fd_accdb_shmem_t * shmem,
647 : ulong * my_epoch_slot_rw,
648 0 : int fd_ro ) {
649 0 : if( FD_UNLIKELY( !ljoin ) ) {
650 0 : FD_LOG_WARNING(( "NULL ljoin" ));
651 0 : return NULL;
652 0 : }
653 :
654 0 : if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)ljoin, fd_accdb_align() ) ) ) {
655 0 : FD_LOG_WARNING(( "misaligned ljoin" ));
656 0 : return NULL;
657 0 : }
658 :
659 0 : if( FD_UNLIKELY( !my_epoch_slot_rw ) ) {
660 0 : FD_LOG_WARNING(( "NULL my_epoch_slot_rw" ));
661 0 : return NULL;
662 0 : }
663 :
664 0 : ulong max_live_slots = shmem->max_live_slots;
665 0 : ulong max_accounts = shmem->max_accounts;
666 0 : ulong max_account_writes_per_slot = shmem->max_account_writes_per_slot;
667 0 : ulong partition_cnt = shmem->partition_cnt;
668 :
669 0 : ulong chain_cnt = fd_ulong_pow2_up( (max_accounts>>1) + (max_accounts&1UL) );
670 0 : ulong txn_max = max_live_slots * max_account_writes_per_slot;
671 :
672 : /* Recompute the same shmem scratch layout that fd_accdb_shmem_new
673 : used. All FD_SCRATCH_ALLOC_APPEND calls here only compute pointer
674 : offsets — they do not write to shmem. */
675 0 : FD_SCRATCH_ALLOC_INIT( l, shmem );
676 0 : FD_SCRATCH_ALLOC_APPEND( l, FD_ACCDB_SHMEM_ALIGN, sizeof(fd_accdb_shmem_t) );
677 0 : void * _fork_pool_ele = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_accdb_fork_shmem_t), max_live_slots*sizeof(fd_accdb_fork_shmem_t) );
678 0 : void * _descends_sets = FD_SCRATCH_ALLOC_APPEND( l, descends_set_align(), max_live_slots*descends_set_footprint( max_live_slots ) );
679 0 : void * _acc_map = FD_SCRATCH_ALLOC_APPEND( l, alignof(uint), chain_cnt*sizeof(uint) );
680 0 : void * _acc_pool_ele = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_accdb_accmeta_t), max_accounts*sizeof(fd_accdb_accmeta_t) );
681 0 : FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_accdb_txn_t), txn_max*sizeof(fd_accdb_txn_t) );
682 0 : FD_SCRATCH_ALLOC_APPEND( l, partition_pool_align(), partition_pool_footprint( partition_cnt ) );
683 0 : for( ulong k=0UL; k<FD_ACCDB_COMPACTION_LAYER_CNT; k++ ) {
684 0 : FD_SCRATCH_ALLOC_APPEND( l, compaction_dlist_align(), compaction_dlist_footprint() );
685 0 : }
686 0 : FD_SCRATCH_ALLOC_APPEND( l, deferred_free_dlist_align(), deferred_free_dlist_footprint() );
687 :
688 0 : FD_SCRATCH_ALLOC_INIT( l2, ljoin );
689 0 : fd_accdb_t * accdb = FD_SCRATCH_ALLOC_APPEND( l2, fd_accdb_align(), sizeof(fd_accdb_t) );
690 0 : void * _local_fork_pool = FD_SCRATCH_ALLOC_APPEND( l2, alignof(fd_accdb_fork_t), max_live_slots*sizeof(fd_accdb_fork_t) );
691 :
692 0 : accdb->fd = fd_ro;
693 0 : accdb->acquire_state = FD_ACCDB_ACQUIRE_STATE_IDLE;
694 0 : accdb->shmem = shmem;
695 0 : FD_TEST( acc_pool_join( accdb->acc_pool_join, shmem->acc_pool, _acc_pool_ele, max_accounts ) );
696 0 : accdb->acc_pool = accdb->acc_pool_join->ele;
697 0 : accdb->acc_map = _acc_map;
698 0 : for( ulong c=0UL; c<FD_ACCDB_CACHE_CLASS_CNT; c++ ) accdb->cache[ c ] = (uchar *)shmem + shmem->cache_region_off[ c ];
699 :
700 : /* Writer-only structures: leave NULL so any accidental writer-path
701 : call from a readonly joiner crashes loudly rather than corrupting
702 : state. */
703 0 : accdb->partition_pool = NULL;
704 0 : for( ulong k=0UL; k<FD_ACCDB_COMPACTION_LAYER_CNT; k++ ) accdb->compaction_dlist[ k ] = NULL;
705 0 : accdb->deferred_free_dlist = NULL;
706 0 : accdb->delta.chains = NULL;
707 0 : accdb->delta.pool = NULL;
708 :
709 0 : FD_TEST( fork_pool_join( accdb->fork_shmem_pool, shmem->fork_pool, _fork_pool_ele, max_live_slots ) );
710 0 : accdb->fork_pool = _local_fork_pool;
711 0 : for( ulong i=0UL; i<max_live_slots; i++ ) {
712 0 : fd_accdb_fork_t * fork = &accdb->fork_pool[ i ];
713 0 : fork->shmem = fork_pool_ele( accdb->fork_shmem_pool, i );
714 0 : fork->descends = descends_set_join( (uchar *)_descends_sets + i*descends_set_footprint( max_live_slots ) );
715 0 : FD_TEST( fork->shmem );
716 0 : FD_TEST( fork->descends );
717 0 : }
718 :
719 : /* my_epoch_slot_rw points at memory owned by this joiner (e.g. a
720 : private per-tile fseq) that the joiner can write to. The
721 : accdb tile sees it via its external_epoch_slots[] array (mapped
722 : read-only) and includes it in its compaction epoch scan.
723 : Storing through this pointer is the only side effect a readonly
724 : joiner has on shared state. */
725 0 : accdb->my_epoch_slot = my_epoch_slot_rw;
726 :
727 : /* Readonly joiners do not own external slots themselves; only the
728 : compaction tile / writer joiners do. */
729 0 : accdb->external_epoch_slots = NULL;
730 0 : accdb->external_epoch_cnt = 0UL;
731 :
732 0 : accdb->deferred_acc_buf = NULL;
733 0 : accdb->deferred_fork_head = NULL;
734 0 : accdb->deferred_fork_tail = NULL;
735 0 : accdb->deferred_fork_epoch = 0UL;
736 :
737 0 : memset( accdb->metrics, 0, sizeof(fd_accdb_metrics_t) );
738 0 : memset( &accdb->write_stats, 0, sizeof(accdb->write_stats) );
739 :
740 0 : return accdb;
741 0 : }
742 :
743 : /* T1 -> T2 cmd channel. Two states on cmd_op:
744 :
745 : IDLE - no cmd in flight
746 : non-IDLE - cmd pending; T2 will process it then flip back to IDLE
747 :
748 : T1 submits by writing fork_id then cmd_op (non-IDLE). T2 processes
749 : by reading fork_id then writing cmd_op = IDLE. T1 waits for IDLE
750 : before submitting again, so T2 never sees a half-written cmd and
751 : never re-processes the same cmd. */
752 :
753 : static inline void
754 9246 : wait_cmd( fd_accdb_t * accdb ) {
755 9246 : fd_accdb_shmem_t * shmem = accdb->shmem;
756 11409527 : while( FD_VOLATILE_CONST( shmem->cmd_op )!=FD_ACCDB_CMD_IDLE ) FD_SPIN_PAUSE();
757 9246 : FD_COMPILER_MFENCE();
758 9246 : }
759 :
760 : static inline void
761 : submit_cmd( fd_accdb_t * accdb,
762 : uint op,
763 525 : ushort fork_id ) {
764 525 : fd_accdb_shmem_t * shmem = accdb->shmem;
765 525 : FD_VOLATILE( shmem->cmd_fork_id ) = fork_id;
766 525 : FD_COMPILER_MFENCE();
767 525 : FD_VOLATILE( shmem->cmd_op ) = op;
768 525 : }
769 :
770 : fd_accdb_fork_id_t
771 : fd_accdb_attach_child( fd_accdb_t * accdb,
772 8709 : fd_accdb_fork_id_t parent_fork_id ) {
773 : /* fork_pool_acquire is not NULL-checked: replay gates attaches on
774 : fd_banks_can_start_bank, and wait_cmd ensures the prior
775 : advance_root has fully run on T2, so
776 : live + deferred forks <= max_live_slots. */
777 8709 : wait_cmd( accdb );
778 :
779 8709 : fd_accdb_fork_shmem_t * acquired = fork_pool_acquire( accdb->fork_shmem_pool );
780 8709 : if( FD_UNLIKELY( !acquired ) ) {
781 3 : submit_cmd( accdb, FD_ACCDB_CMD_DRAIN_DEFERRED, USHORT_MAX );
782 3 : wait_cmd( accdb );
783 3 : acquired = fork_pool_acquire( accdb->fork_shmem_pool );
784 3 : FD_CHECK_CRIT( acquired, "accdb fork pool exhausted after deferred drain" );
785 3 : }
786 8709 : ulong idx = fork_pool_idx( accdb->fork_shmem_pool, acquired );
787 :
788 8709 : fd_accdb_fork_t * fork = &accdb->fork_pool[ idx ];
789 8709 : fd_accdb_fork_id_t fork_id = { .val = (ushort)idx };
790 :
791 8709 : fork->shmem->child_id = (fd_accdb_fork_id_t){ .val = USHORT_MAX };
792 :
793 8709 : if( FD_LIKELY( parent_fork_id.val==USHORT_MAX ) ) {
794 4050 : fork->shmem->parent_id = (fd_accdb_fork_id_t){ .val = USHORT_MAX };
795 4050 : fork->shmem->sibling_id = (fd_accdb_fork_id_t){ .val = USHORT_MAX };
796 :
797 4050 : descends_set_null( fork->descends );
798 4050 : accdb->shmem->root_fork_id = fork_id;
799 4659 : } else {
800 4659 : fd_accdb_fork_t * parent = &accdb->fork_pool[ parent_fork_id.val ];
801 4659 : fork->shmem->parent_id = parent_fork_id;
802 :
803 4659 : descends_set_copy( fork->descends, parent->descends );
804 4659 : descends_set_insert( fork->descends, parent_fork_id.val );
805 :
806 : /* Atomically prepend to parent's child list. T2 (background_purge)
807 : may concurrently unlink a different child from the same list, so
808 : we must CAS here. */
809 4659 : FD_COMPILER_MFENCE();
810 4659 : for(;;) {
811 4659 : ushort old_head = FD_VOLATILE_CONST( parent->shmem->child_id.val );
812 4659 : fork->shmem->sibling_id = (fd_accdb_fork_id_t){ .val = old_head };
813 4659 : FD_COMPILER_MFENCE();
814 4659 : if( FD_LIKELY( FD_ATOMIC_CAS( &parent->shmem->child_id.val, old_head, fork_id.val )==old_head ) ) break;
815 0 : FD_SPIN_PAUSE();
816 0 : }
817 4659 : }
818 :
819 8709 : fork->shmem->generation = accdb->shmem->generation++;
820 8709 : fork->shmem->txn_head = UINT_MAX;
821 :
822 8709 : FD_TEST( !descends_set_test( fork->descends, fork_id.val ) );
823 :
824 8709 : return fork_id;
825 8709 : }
826 :
827 : /* evict_clear_acc_cache_ref atomically tears down acc->cache_idx and
828 : acc->executable_size.CACHE_VALID for an acc that is being evicted
829 : from cache line (size_class, line_idx). The caller must already
830 : hold an exclusive claim on the line (line->refcnt ==
831 : FD_ACCDB_EVICT_SENTINEL) so that no concurrent thread can pin the
832 : line.
833 :
834 : The naive sequence (clear cache_idx, clear VALID) lets a reader in
835 : cold_load_acc see VALID=1 and read a stale INVAL cache_idx, which
836 : decodes to an OOB cache_line pointer. The reverse sequence (clear
837 : VALID, clear cache_idx) lets a concurrent cold_load_acc observe
838 : VALID=0/CLAIM=0 and start publishing a *new* cache_idx + VALID=1
839 : between our two stores; our later cache_idx=INVAL would then
840 : stomp on the cold-loader's published idx.
841 :
842 : We close both races by acquiring CACHE_CLAIM_BIT before mutating
843 : acc->cache_idx. cold_load_acc spins while CLAIM is held, so it
844 : cannot enter the publish path concurrently. If CLAIM is already
845 : held, a cold-loader is already mid-publish; in that case
846 : acc->cache_idx is being repointed away from our line, and we must
847 : not touch it. After mutation we release CLAIM.
848 :
849 : Verifies acc->cache_idx still encodes (size_class, line_idx) before
850 : clobbering, in case the acc was concurrently re-published into a
851 : different line (e.g. by a previous cold_load_acc completing before
852 : we arrived). */
853 :
854 : static inline void
855 : evict_clear_acc_cache_ref( fd_accdb_accmeta_t * accmeta,
856 : ulong size_class,
857 366 : ulong line_idx ) {
858 366 : uint expected_cidx = FD_ACCDB_ACC_CIDX_PACK( (uint)size_class, (uint)line_idx );
859 :
860 : /* CAS-acquire CLAIM. If a cold-loader already holds CLAIM, they
861 : own the publish path; bail without touching accmeta fields (their
862 : republish is repointing accmeta->cache_idx away from our line). */
863 366 : for(;;) {
864 366 : uint cur = FD_VOLATILE_CONST( accmeta->executable_size );
865 366 : if( FD_UNLIKELY( cur & FD_ACCDB_SIZE_CACHE_CLAIM_BIT ) ) return;
866 366 : uint nxt = cur | FD_ACCDB_SIZE_CACHE_CLAIM_BIT;
867 366 : if( FD_LIKELY( FD_ATOMIC_CAS( &accmeta->executable_size, cur, nxt )==cur ) ) break;
868 0 : fd_racesan_hook( "accdb_evict_clear:claim_wait" );
869 0 : FD_SPIN_PAUSE();
870 0 : }
871 :
872 366 : fd_racesan_hook( "accdb_evict_clear:post_claim" );
873 :
874 : /* CLAIM held. If accmeta->cache_idx still points at our line, clear
875 : VALID and INVAL the cache_idx. Otherwise the accmeta was already
876 : re-published into a different line; leave it alone. */
877 366 : if( FD_LIKELY( FD_VOLATILE_CONST( accmeta->cache_idx )==expected_cidx ) ) {
878 366 : FD_ATOMIC_FETCH_AND_AND( &accmeta->executable_size, ~FD_ACCDB_SIZE_CACHE_VALID_BIT );
879 366 : FD_VOLATILE( accmeta->cache_idx ) = FD_ACCDB_ACC_CIDX_INVAL;
880 366 : }
881 :
882 : /* Release CLAIM. */
883 366 : FD_ATOMIC_FETCH_AND_AND( &accmeta->executable_size, ~FD_ACCDB_SIZE_CACHE_CLAIM_BIT );
884 366 : }
885 :
886 : /* cache_free_push pushes a fully-freed cache line onto the per-class
887 : CAS free list (Treiber stack). The caller must have already
888 : invalidated the line (key.generation==UINT_MAX) and set persisted=1
889 : before pushing. */
890 :
891 : static inline void
892 : cache_free_push( fd_accdb_t * accdb,
893 : ulong size_class,
894 807570 : fd_accdb_cache_line_t * line ) {
895 807570 : ulong line_idx = cache_line_idx( accdb, size_class, line );
896 807570 : for(;;) {
897 807570 : ulong old_vt = FD_VOLATILE_CONST( accdb->shmem->cache_free[ size_class ].ver_top );
898 807570 : uint old_top = (uint)( old_vt & (ulong)UINT_MAX );
899 807570 : uint old_ver = (uint)( old_vt >> 32 );
900 807570 : line->next = old_top;
901 807570 : FD_COMPILER_MFENCE();
902 807570 : ulong new_vt = ((ulong)(uint)( old_ver+1U ) << 32) | (ulong)(uint)line_idx;
903 807570 : if( FD_LIKELY( FD_ATOMIC_CAS( &accdb->shmem->cache_free[ size_class ].ver_top, old_vt, new_vt )==old_vt ) ) {
904 807570 : FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->cache_free_cnt[ size_class ].val, 1UL );
905 807570 : return;
906 807570 : }
907 0 : FD_SPIN_PAUSE();
908 0 : }
909 807570 : }
910 :
911 : /* cache_free_pop pops a line from the per-class CAS free list. Returns
912 : NULL if the list is empty. */
913 :
914 : static inline fd_accdb_cache_line_t *
915 : cache_free_pop( fd_accdb_t * accdb,
916 918438 : ulong size_class ) {
917 918438 : for(;;) {
918 918438 : ulong old_vt = FD_VOLATILE_CONST( accdb->shmem->cache_free[ size_class ].ver_top );
919 918438 : uint old_top = (uint)( old_vt & (ulong)UINT_MAX );
920 918438 : if( FD_UNLIKELY( old_top==UINT_MAX ) ) return NULL;
921 776376 : uint old_ver = (uint)( old_vt >> 32 );
922 776376 : fd_accdb_cache_line_t * top = cache_line( accdb, size_class, (ulong)old_top );
923 776376 : uint next = FD_VOLATILE_CONST( top->next );
924 776376 : ulong new_vt = ((ulong)(uint)( old_ver+1U ) << 32) | (ulong)next;
925 776376 : if( FD_LIKELY( FD_ATOMIC_CAS( &accdb->shmem->cache_free[ size_class ].ver_top, old_vt, new_vt )==old_vt ) ) {
926 776376 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->cache_free_cnt[ size_class ].val, 1UL );
927 776376 : return top;
928 776376 : }
929 0 : FD_SPIN_PAUSE();
930 0 : }
931 918438 : }
932 :
933 : /* cache_try_pin attempts a lock-free pin of a cache-hit line. Returns
934 : the line if successfully pinned, or NULL if the line is being evicted
935 : or was recycled (ABA). */
936 :
937 : static inline fd_accdb_cache_line_t *
938 : cache_try_pin( fd_accdb_cache_line_t * line,
939 : uchar const pubkey[ 32 ],
940 95724 : uint generation ) {
941 95724 : for(;;) {
942 95724 : uint old_rc = FD_VOLATILE_CONST( line->refcnt );
943 95724 : if( FD_UNLIKELY( old_rc==FD_ACCDB_EVICT_SENTINEL ) ) return NULL;
944 : /* No saturation guard needed: refcnt is a uint and at most
945 : FD_ACCDB_MAX_JOINERS (256) threads can pin concurrently,
946 : so old_rc+1 can never reach FD_ACCDB_EVICT_SENTINEL
947 : (UINT_MAX) or wrap. */
948 95724 : if( FD_LIKELY( FD_ATOMIC_CAS( &line->refcnt, old_rc, old_rc+1U )==old_rc ) ) {
949 : /* Pinned. ABA check: verify the key hasn't changed under us. */
950 95724 : fd_racesan_hook( "accdb_try_pin:post_cas" );
951 95724 : FD_COMPILER_MFENCE();
952 95724 : if( FD_UNLIKELY( line->key.generation!=generation ||
953 95724 : memcmp( line->key.pubkey, pubkey, 32UL ) ) ) {
954 0 : FD_ATOMIC_FETCH_AND_SUB( &line->refcnt, 1U );
955 0 : return NULL;
956 0 : }
957 95724 : line->referenced = 1;
958 95724 : fd_racesan_hook( "cache_try_pin:pinned" );
959 95724 : return line;
960 95724 : }
961 0 : FD_SPIN_PAUSE();
962 0 : }
963 95724 : }
964 :
965 : /* wait_for_epoch_drain spins until every joiner's published epoch
966 : exceeds tag, meaning all readers that were active at epoch=tag have
967 : since exited their critical sections. */
968 :
969 : static void
970 : wait_for_epoch_drain( fd_accdb_t * accdb,
971 864 : ulong tag ) {
972 864 : for(;;) {
973 864 : ulong min_epoch = ULONG_MAX;
974 864 : ulong joiner_cnt = FD_VOLATILE_CONST( accdb->shmem->joiner_cnt );
975 1746 : for( ulong t=0UL; t<joiner_cnt; t++ ) {
976 882 : ulong e = FD_VOLATILE_CONST( accdb->shmem->joiner_epochs[ t ].val );
977 882 : if( FD_LIKELY( e<min_epoch ) ) min_epoch = e;
978 882 : }
979 864 : for( ulong t=0UL; t<accdb->external_epoch_cnt; t++ ) {
980 0 : ulong e = FD_VOLATILE_CONST( *accdb->external_epoch_slots[ t ] );
981 0 : if( FD_LIKELY( e<min_epoch ) ) min_epoch = e;
982 0 : }
983 864 : if( FD_LIKELY( tag<min_epoch ) ) break;
984 0 : fd_racesan_hook( "accdb_epoch_drain:wait" );
985 0 : FD_SPIN_PAUSE();
986 0 : }
987 864 : }
988 :
989 : /* drain_deferred_frees releases back to their respective pools any acc
990 : batch and/or fork slots that were unlinked in a prior advance_root /
991 : purge call. The resources cannot be released immediately because
992 : concurrent readers may still reference them. We wait until every
993 : joiner's published epoch exceeds the tag stamped when each resource
994 : was unlinked.
995 :
996 : Must be called before creating new deferred batches (there is at most
997 : one of each outstanding at a time). */
998 :
999 : static void
1000 522 : drain_deferred_frees( fd_accdb_t * accdb ) {
1001 522 : if( FD_UNLIKELY( accdb->deferred_fork_head ) ) {
1002 435 : wait_for_epoch_drain( accdb, accdb->deferred_fork_epoch );
1003 435 : fork_pool_release_chain( accdb->fork_shmem_pool, accdb->deferred_fork_head, accdb->deferred_fork_tail );
1004 435 : accdb->deferred_fork_head = NULL;
1005 435 : accdb->deferred_fork_tail = NULL;
1006 435 : }
1007 :
1008 522 : ulong n = accdb->shmem->deferred_acc_buf_cnt;
1009 522 : if( FD_LIKELY( !n ) ) return;
1010 429 : wait_for_epoch_drain( accdb, accdb->shmem->deferred_acc_epoch );
1011 :
1012 : /* All readers that could have been holding a captured pointer to any
1013 : of these accs at unlink time have now exited their epoch sections.
1014 : It is safe to materialize pool.next links and hand the chain to
1015 : acc_pool_release_chain. */
1016 429 : uint * buf = accdb->deferred_acc_buf;
1017 429 : fd_accdb_accmeta_t * acc_pool = accdb->acc_pool;
1018 :
1019 : /* Late-publish sweep: a concurrent acquire evictor may have published
1020 : a new offset into one of these accmetas after acc_unlink's
1021 : xchg-to-INVAL but before exiting its epoch. Now that the epoch has
1022 : drained, any such publish is complete and visible. Free the
1023 : orphaned disk bytes here, before the accmeta is released to the
1024 : pool and its fields recycled. */
1025 429 : ulong acc_pool_cap = acc_pool_ele_max( accdb->acc_pool_join );
1026 1590 : for( ulong i=0UL; i<n; i++ ) {
1027 1161 : FD_TEST( (ulong)buf[ i ]<acc_pool_cap );
1028 1161 : fd_accdb_accmeta_t * accmeta = &acc_pool[ buf[ i ] ];
1029 1161 : ulong off = fd_accdb_acc_offset( accmeta );
1030 1161 : if( FD_UNLIKELY( off!=FD_ACCDB_OFF_INVAL ) ) {
1031 0 : ulong entry_sz = (ulong)FD_ACCDB_SIZE_DATA(accmeta->executable_size)+sizeof(fd_accdb_disk_meta_t);
1032 0 : fd_accdb_shmem_bytes_freed( accdb->shmem, off, entry_sz );
1033 0 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->shmetrics->disk_used_bytes, entry_sz );
1034 0 : }
1035 1161 : }
1036 :
1037 1161 : for( ulong i=0UL; i+1UL<n; i++ ) {
1038 732 : acc_pool[ buf[ i ] ].pool.next = acc_pool_private_cidx( (ulong)buf[ i+1UL ] );
1039 732 : }
1040 429 : fd_accdb_accmeta_t * head = &acc_pool[ buf[ 0UL ] ];
1041 429 : fd_accdb_accmeta_t * tail = &acc_pool[ buf[ n-1UL ] ];
1042 429 : acc_pool_release_chain( accdb->acc_pool_join, head, tail );
1043 429 : accdb->shmem->deferred_acc_buf_cnt = 0UL;
1044 429 : }
1045 :
1046 : /* deferred_acc_append records an unlinked acc index in the side buffer
1047 : for later release after wait_for_epoch_drain. T2 is the sole writer.
1048 : The chain link from acc->pool.next is NOT laid down here: pool.next
1049 : is union-aliased to cache_idx, and a concurrent cold_load_acc may
1050 : still publish through a captured pointer until the epoch drains.
1051 : Materialization of the chain happens in drain_deferred_frees. */
1052 :
1053 : static inline void
1054 : deferred_acc_append( fd_accdb_t * accdb,
1055 1515 : uint acc_idx ) {
1056 1515 : fd_accdb_shmem_t * shmem = accdb->shmem;
1057 1515 : FD_TEST( shmem->deferred_acc_buf_cnt<shmem->deferred_acc_buf_max );
1058 1515 : accdb->deferred_acc_buf[ shmem->deferred_acc_buf_cnt++ ] = acc_idx;
1059 1515 : }
1060 :
1061 : /* acc_unlink unlinks an account from its hash map chain, frees any
1062 : associated disk bytes, and invalidates a stale cache reference. Does
1063 : NOT release the acc pool slot — the caller is responsible for that
1064 : (or for batching releases).
1065 :
1066 : prev is the previous element in the map chain (UINT_MAX if acc_idx is
1067 : the head).
1068 :
1069 : CONCURRENCY: The chain link being removed is swapped out with a CAS
1070 : so that a concurrent fd_accdb_release prepending to the same chain
1071 : cannot lose its update. If a head-removal CAS fails (a new node was
1072 : prepended since we loaded the head), we re-walk from the new head to
1073 : find the target as an interior node. Interior CAS cannot fail from
1074 : inserts (inserts only touch the head) and only one remover exists at
1075 : a time (advance_root / purge are serialized). */
1076 :
1077 : static inline void
1078 : acc_unlink( fd_accdb_t * accdb,
1079 : uint map_idx,
1080 : uint prev,
1081 1515 : uint acc_idx ) {
1082 1515 : fd_accdb_accmeta_t * accmeta = &accdb->acc_pool[ acc_idx ];
1083 :
1084 : /* Atomically capture and clear the offset. Two races to defuse:
1085 :
1086 : (1) A concurrent fd_accdb_acquire_inner that is CLOCK-evicting the
1087 : cache line currently holding this acc's data may have already
1088 : xchg'd the offset to INVAL in step 5-6 and freed the old disk
1089 : bytes. Without atomicity we would re-read the old offset and
1090 : free those same bytes a second time. The xchg here serializes:
1091 : whoever wins sees the real offset and frees; the loser sees
1092 : INVAL and skips.
1093 :
1094 : (2) That same evictor may also be mid-flight to publish a NEW
1095 : offset in step 9 (after step 5-6's free but before step 9's
1096 : store). That late publish lands on an accmeta that is about
1097 : to be chain-unlinked and deferred-released. drain_deferred_
1098 : frees sweeps the deferred buffer after epoch drain to catch
1099 : the late publish and free the orphaned bytes. */
1100 1515 : ulong entry_sz = (ulong)FD_ACCDB_SIZE_DATA(accmeta->executable_size)+sizeof(fd_accdb_disk_meta_t);
1101 1515 : ulong old_offset = fd_accdb_acc_xchg_offset( accmeta, FD_ACCDB_OFF_INVAL );
1102 1515 : if( FD_LIKELY( old_offset!=FD_ACCDB_OFF_INVAL ) ) {
1103 36 : fd_accdb_shmem_bytes_freed( accdb->shmem, old_offset, entry_sz );
1104 36 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->shmetrics->disk_used_bytes, entry_sz );
1105 36 : }
1106 1515 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->shmetrics->accounts_total, 1UL );
1107 1515 : accdb->metrics->accounts_deleted++;
1108 :
1109 1515 : if( FD_LIKELY( prev==UINT_MAX ) ) {
1110 : /* Head removal — CAS may fail if a concurrent insert prepended a
1111 : new node. On failure the target is now interior. */
1112 48 : for(;;) {
1113 48 : uint old_head = FD_VOLATILE_CONST( accdb->acc_map[ map_idx ] );
1114 48 : if( FD_LIKELY( old_head==acc_idx ) ) {
1115 48 : if( FD_LIKELY( FD_ATOMIC_CAS( &accdb->acc_map[ map_idx ], acc_idx, accmeta->map.next )==acc_idx ) ) break;
1116 0 : FD_SPIN_PAUSE();
1117 0 : continue;
1118 48 : }
1119 : /* Head changed — walk from new head to find prev for interior
1120 : removal. The target must still be in the chain because only
1121 : this thread removes elements. */
1122 0 : prev = old_head;
1123 0 : while( FD_VOLATILE_CONST( accdb->acc_pool[ prev ].map.next )!=acc_idx ) prev = FD_VOLATILE_CONST( accdb->acc_pool[ prev ].map.next );
1124 0 : FD_ATOMIC_CAS( &accdb->acc_pool[ prev ].map.next, acc_idx, accmeta->map.next );
1125 0 : break;
1126 48 : }
1127 1467 : } else {
1128 1467 : FD_ATOMIC_CAS( &accdb->acc_pool[ prev ].map.next, acc_idx, accmeta->map.next );
1129 1467 : }
1130 :
1131 1515 : fd_racesan_hook( "accdb_acc_unlink:post_splice" );
1132 :
1133 : /* If the freed acc still has a cached location, invalidate it and
1134 : try to reclaim the cache line so the eviction path does not try
1135 : to write back stale data from a recycled pool slot. Lock-free:
1136 : CAS the refcnt 0 -> EVICT_SENTINEL to claim it exclusively, then
1137 : push to the CAS free list. If the line is pinned (refcnt>0),
1138 : skip, the pinner's release will handle it.
1139 :
1140 : Acquire CACHE_CLAIM_BIT before touching acc->cache_idx /
1141 : CACHE_VALID — see evict_clear_acc_cache_ref for the protocol.
1142 : Without CLAIM, a concurrent cold_load_acc can publish a fresh
1143 : (cache_idx, VALID=1) pair into this acc between our two stores,
1144 : and our subsequent cache_idx=INVAL stomps onto the freelist
1145 : pool.next field (the union sibling of cache_idx), corrupting the
1146 : pool. Unlike evict_clear_acc_cache_ref, we cannot bail when CLAIM
1147 : is held: this acc is being permanently unlinked, so we must
1148 : spin-wait for the cold-loader to release CLAIM and then invalidate
1149 : whatever cache_idx is current. */
1150 1515 : uint cur_es;
1151 1515 : for(;;) {
1152 1515 : cur_es = FD_VOLATILE_CONST( accmeta->executable_size );
1153 1515 : if( FD_UNLIKELY( cur_es & FD_ACCDB_SIZE_CACHE_CLAIM_BIT ) ) { FD_SPIN_PAUSE(); continue; }
1154 1515 : uint nxt_es = cur_es | FD_ACCDB_SIZE_CACHE_CLAIM_BIT;
1155 1515 : if( FD_LIKELY( FD_ATOMIC_CAS( &accmeta->executable_size, cur_es, nxt_es )==cur_es ) ) break;
1156 0 : FD_SPIN_PAUSE();
1157 0 : }
1158 :
1159 1515 : uint cidx = FD_ACCDB_ACC_CIDX_INVAL;
1160 1515 : int had_valid = FD_ACCDB_SIZE_CACHE_VALID( cur_es );
1161 1515 : if( FD_UNLIKELY( had_valid ) ) {
1162 1479 : cidx = FD_VOLATILE_CONST( accmeta->cache_idx );
1163 : /* Clear VALID before INVAL'ing cache_idx — matches the order in
1164 : evict_clear_acc_cache_ref so cold_load_acc's "VALID=1 +
1165 : cidx=INVAL" spin path resolves on the next iteration when it
1166 : observes VALID=0. */
1167 1479 : FD_ATOMIC_FETCH_AND_AND( &accmeta->executable_size, ~FD_ACCDB_SIZE_CACHE_VALID_BIT );
1168 1479 : FD_VOLATILE( accmeta->cache_idx ) = FD_ACCDB_ACC_CIDX_INVAL;
1169 1479 : }
1170 :
1171 : /* Release CLAIM. */
1172 1515 : FD_ATOMIC_FETCH_AND_AND( &accmeta->executable_size, ~FD_ACCDB_SIZE_CACHE_CLAIM_BIT );
1173 :
1174 1515 : if( FD_UNLIKELY( had_valid ) ) {
1175 1479 : fd_accdb_cache_line_t * stale = cache_line( accdb, FD_ACCDB_ACC_CIDX_CLASS( cidx ), FD_ACCDB_ACC_CIDX_IDX( cidx ) );
1176 1479 : fd_racesan_hook( "acc_unlink:pre_reclaim_cas" );
1177 1479 : uint old_rc = FD_ATOMIC_CAS( &stale->refcnt, 0U, FD_ACCDB_EVICT_SENTINEL );
1178 1479 : fd_racesan_hook( "acc_unlink:post_reclaim_cas" );
1179 1479 : if( FD_LIKELY( !old_rc ) ) {
1180 : /* Claimed. Validate key (ABA, slot could have been recycled
1181 : between our read of cache_idx and the CAS). */
1182 1479 : if( FD_LIKELY( stale->key.generation==accmeta->key.generation &&
1183 1479 : !memcmp( stale->key.pubkey, accmeta->key.pubkey, 32UL ) ) ) {
1184 1479 : ulong sc = FD_ACCDB_ACC_CIDX_CLASS( cidx );
1185 1479 : stale->key.generation = UINT_MAX;
1186 1479 : stale->persisted = 1;
1187 1479 : stale->acc_idx = UINT_MAX;
1188 1479 : stale->refcnt = 0;
1189 1479 : cache_free_push( accdb, sc, stale );
1190 1479 : } else {
1191 : /* Wrong line (ABA). Release claim. */
1192 0 : FD_VOLATILE( stale->refcnt ) = 0;
1193 0 : }
1194 1479 : }
1195 0 : else if( FD_LIKELY( old_rc!=FD_ACCDB_EVICT_SENTINEL ) ) {
1196 : /* The CAS lost to a non-sentinel refcnt, but that does not prove
1197 : `stale` is still our line. Between capturing cidx and here we
1198 : released the claim, so we could have evicted `stale` and
1199 : recycled it to an unrelated account. */
1200 0 : fd_accdb_cache_line_t * mine = cache_try_pin( stale, accmeta->key.pubkey, accmeta->key.generation );
1201 0 : if( FD_LIKELY( mine ) ) {
1202 : /* Genuinely our line, still pinned by a reader. The accmeta
1203 : slot is about to be deferred-released and recycled; if a
1204 : later writeback of this dirty line fires, it would pair the
1205 : recycled accmeta's pubkey with the old owner/data. Set
1206 : persisted so the writeback gate never fires. */
1207 0 : FD_VOLATILE( mine->persisted ) = 1;
1208 :
1209 : /* Only the tombstone self-unlink may be pinned here old-version
1210 : and purge unlinks are never pinned, because a reader on a
1211 : live fork resolves to the newest version, not the one these
1212 : unlink. */
1213 0 : FD_TEST( accmeta->lamports==0UL );
1214 :
1215 0 : FD_ATOMIC_FETCH_AND_SUB( &mine->refcnt, 1U );
1216 0 : }
1217 : /* Else was recycled to a foreign account. Nothing to neutralize,
1218 : leave the line alone. */
1219 0 : } else {
1220 : /* A foreground evictor already claimed this line. It holds its
1221 : epoch acquire and writeback, so drain_deferred_frees cannot
1222 : recycle the slot before it finishes. Its writeback names the
1223 : old account correctly, no poison. */
1224 0 : }
1225 1479 : }
1226 1515 : }
1227 :
1228 : /* fork_slot_defer removes fork_id from every descends_set and chains
1229 : the fork pool slot onto the deferred fork chain for later release.
1230 : The slot must not be released immediately because concurrent readers
1231 : may still reference the fork ID via descends_set or stale chain
1232 : walks.
1233 :
1234 : The eager descends_set_remove here is safe despite being a
1235 : non-atomic RMW that races with concurrent descends_set_test in
1236 : fd_accdb_acquire, for two reasons:
1237 :
1238 : (a) Rooted parent forks: after advance_root publishes the new
1239 : root_fork_id, any acquire loads root_generation >=
1240 : parent->generation. Every account from the old parent has
1241 : generation <= parent->generation, so the
1242 : "generation > root_generation" gate in the chain walk is
1243 : never satisfied and the parent's bit is never tested.
1244 :
1245 : (b) Purged / pruned sibling forks: a purged fork is by
1246 : definition not an ancestor of any live fork, so its bit
1247 : was never set in any live fork's descends_set. Clearing
1248 : it is a literal no-op.
1249 :
1250 : Fork-id ABA after slot reuse is also safe: the fork pool slot
1251 : is not released until drain_deferred_frees, which waits until
1252 : all epoch-protected readers have exited. On x86 (TSO), the
1253 : synchronization chain (T2: bit clear -> epoch FAA; reader:
1254 : epoch load -> epoch_slot store -> mfence -> bit read) guarantees
1255 : that any reader entering a new epoch section after the drain
1256 : will observe the cleared bit before the slot is recycled by
1257 : attach_child. */
1258 :
1259 : static inline void
1260 : fork_slot_defer( fd_accdb_t * accdb,
1261 : fd_accdb_fork_id_t fork_id,
1262 : fd_accdb_fork_shmem_t ** fork_head,
1263 531 : fd_accdb_fork_shmem_t ** fork_tail ) {
1264 11340 : for( ulong i=0UL; i<accdb->shmem->max_live_slots; i++ ) descends_set_remove( accdb->fork_pool[ i ].descends, fork_id.val );
1265 531 : fd_accdb_fork_shmem_t * shmem = fork_pool_ele( accdb->fork_shmem_pool, (ulong)fork_id.val );
1266 531 : if( *fork_tail ) (*fork_tail)->pool.next = fork_pool_private_cidx( (ulong)fork_id.val );
1267 519 : else *fork_head = shmem;
1268 531 : *fork_tail = shmem;
1269 531 : }
1270 :
1271 : static void
1272 : purge_inner( fd_accdb_t * accdb,
1273 : fd_accdb_fork_id_t fork_id,
1274 : fd_accdb_fork_shmem_t ** fork_head,
1275 33 : fd_accdb_fork_shmem_t ** fork_tail ) {
1276 33 : fd_accdb_fork_t * fork = &accdb->fork_pool[ fork_id.val ];
1277 :
1278 33 : fd_accdb_fork_id_t child = fork->shmem->child_id;
1279 42 : while( child.val!=USHORT_MAX ) {
1280 9 : fd_accdb_fork_id_t next = accdb->fork_pool[ child.val ].shmem->sibling_id;
1281 9 : purge_inner( accdb, child, fork_head, fork_tail );
1282 9 : child = next;
1283 9 : }
1284 :
1285 33 : uint txn = fork->shmem->txn_head;
1286 33 : if( txn!=UINT_MAX ) {
1287 30 : fd_accdb_txn_t * txn_head = txn_pool_ele( accdb->txn_pool, (ulong)txn );
1288 30 : fd_accdb_txn_t * txn_tail = NULL;
1289 81 : while( txn!=UINT_MAX ) {
1290 51 : fd_accdb_txn_t * txne = txn_pool_ele( accdb->txn_pool, (ulong)txn );
1291 :
1292 51 : uint acc_idx = txne->acc_pool_idx;
1293 :
1294 51 : uint prev = UINT_MAX;
1295 51 : uint cur = FD_VOLATILE_CONST( accdb->acc_map[ txne->acc_map_idx ] );
1296 54 : while( cur!=acc_idx ) {
1297 3 : prev = cur;
1298 3 : cur = FD_VOLATILE_CONST( accdb->acc_pool[ cur ].map.next );
1299 3 : }
1300 :
1301 51 : fd_racesan_hook( "accdb_purge:pre_unlink" );
1302 51 : acc_unlink( accdb, txne->acc_map_idx, prev, acc_idx );
1303 51 : deferred_acc_append( accdb, acc_idx );
1304 :
1305 51 : txn_tail = txne;
1306 51 : txn = txne->fork.next;
1307 51 : }
1308 30 : txn_pool_release_chain( accdb->txn_pool, txn_head, txn_tail );
1309 30 : }
1310 :
1311 33 : fork_slot_defer( accdb, fork_id, fork_head, fork_tail );
1312 33 : }
1313 :
1314 : static inline void
1315 : remove_children( fd_accdb_t * accdb,
1316 : fd_accdb_fork_t * fork,
1317 : fd_accdb_fork_t * except,
1318 : fd_accdb_fork_shmem_t ** fork_head,
1319 498 : fd_accdb_fork_shmem_t ** fork_tail ) {
1320 498 : fd_accdb_fork_id_t sibling_idx = fork->shmem->child_id;
1321 999 : while( sibling_idx.val!=USHORT_MAX ) {
1322 501 : fd_accdb_fork_t * sibling = &accdb->fork_pool[ sibling_idx.val ];
1323 501 : fd_accdb_fork_id_t cur_idx = sibling_idx;
1324 :
1325 501 : sibling_idx = sibling->shmem->sibling_id;
1326 501 : if( FD_UNLIKELY( sibling==except ) ) continue;
1327 :
1328 3 : purge_inner( accdb, cur_idx, fork_head, fork_tail );
1329 3 : }
1330 498 : }
1331 :
1332 : static void
1333 : background_advance_root( fd_accdb_t * accdb,
1334 498 : fd_accdb_fork_id_t fork_id ) {
1335 498 : drain_deferred_frees( accdb );
1336 :
1337 : /* The caller guarantees that rooting is sequential: each call
1338 : advances the root by exactly one slot (the immediate child of the
1339 : current root). Skipping levels is not supported. */
1340 498 : fd_accdb_fork_t * fork = &accdb->fork_pool[ fork_id.val ];
1341 498 : FD_TEST( fork->shmem->parent_id.val==accdb->shmem->root_fork_id.val );
1342 498 : FD_TEST( fork->shmem->parent_id.val!=USHORT_MAX );
1343 :
1344 498 : fd_accdb_fork_t * parent_fork = &accdb->fork_pool[ fork->shmem->parent_id.val ];
1345 :
1346 : /* Accumulate freed fork pool slots across remove_children and the
1347 : old-version cleanup below into a chain that will be deferred-
1348 : released after the epoch bump. Freed acc pool slots are recorded
1349 : in the shmem side buffer via deferred_acc_append (they cannot be
1350 : chained via pool.next yet — see comment on the side buffer). */
1351 498 : fd_accdb_fork_shmem_t * fork_head = NULL;
1352 498 : fd_accdb_fork_shmem_t * fork_tail = NULL;
1353 :
1354 : /* When a fork is rooted, any competing forks can be immediately
1355 : removed as they will not be needed again. This includes child
1356 : forks of the pruned siblings as well. */
1357 498 : remove_children( accdb, parent_fork, fork, &fork_head, &fork_tail );
1358 :
1359 : /* And for any accounts which were updated in the newly rooted slot,
1360 : we will now never need to access any older version, so we can
1361 : discard any slots earlier than the one we are rooting. */
1362 498 : uint txn = fork->shmem->txn_head;
1363 498 : if( txn!=UINT_MAX ) {
1364 495 : fd_accdb_txn_t * txn_head = txn_pool_ele( accdb->txn_pool, (ulong)txn );
1365 495 : fd_accdb_txn_t * txn_tail = NULL;
1366 2013 : while( txn!=UINT_MAX ) {
1367 1518 : fd_accdb_txn_t * txne = txn_pool_ele( accdb->txn_pool, (ulong)txn );
1368 :
1369 1518 : fd_accdb_accmeta_t const * new_acc = &accdb->acc_pool[ txne->acc_pool_idx ];
1370 :
1371 1518 : delta_insert( accdb, new_acc->key.pubkey );
1372 :
1373 1518 : uint prev = UINT_MAX;
1374 1518 : uint new_acc_prev = UINT_MAX; /* prev of new_acc on the chain when we encounter it (UINT_MAX if head or never seen) */
1375 1518 : int new_acc_seen = 0;
1376 1518 : uint acc = FD_VOLATILE_CONST( accdb->acc_map[ txne->acc_map_idx ] );
1377 1518 : FD_TEST( acc!=UINT_MAX );
1378 4824 : while( acc!=UINT_MAX ) {
1379 3306 : fd_accdb_accmeta_t const * cur_acc = &accdb->acc_pool[ acc ];
1380 3306 : uint cur_next = FD_VOLATILE_CONST( cur_acc->map.next );
1381 :
1382 3306 : if( FD_LIKELY( acc==txne->acc_pool_idx ) ) {
1383 1518 : new_acc_prev = prev;
1384 1518 : new_acc_seen = 1;
1385 1518 : prev = acc;
1386 1518 : acc = cur_next;
1387 1518 : continue;
1388 1518 : }
1389 :
1390 1788 : if( FD_LIKELY( (cur_acc->key.generation<=parent_fork->shmem->generation || descends_set_test( fork->descends, fd_accdb_acc_fork_id(cur_acc) ) ) && !memcmp( new_acc->key.pubkey, cur_acc->key.pubkey, 32UL ) ) ) {
1391 1464 : uint next = cur_next;
1392 1464 : fd_racesan_hook( "accdb_advance:pre_unlink" );
1393 1464 : acc_unlink( accdb, txne->acc_map_idx, prev, acc );
1394 1464 : deferred_acc_append( accdb, acc );
1395 1464 : acc = next;
1396 1464 : } else {
1397 324 : prev = acc;
1398 324 : acc = cur_next;
1399 324 : }
1400 1788 : }
1401 :
1402 : /* If the newly rooted version is a tombstone (lamports==0, e.g.
1403 : account was closed), drop it from the index too: no fork can
1404 : reach it anymore, and keeping it around just wastes a hash
1405 : slot and the disk bytes it occupies.
1406 :
1407 : If a later txn on this same fork wrote the same pubkey, that
1408 : txn's inner walk above would have already unlinked this txn's
1409 : new_acc as an "older version" - in that case new_acc_seen=0
1410 : and we skip, since the freelist cleanup is already done. */
1411 1518 : if( FD_UNLIKELY( new_acc_seen && new_acc->lamports==0UL ) ) {
1412 0 : uint new_acc_idx = (uint)txne->acc_pool_idx;
1413 0 : acc_unlink( accdb, txne->acc_map_idx, new_acc_prev, new_acc_idx );
1414 0 : deferred_acc_append( accdb, new_acc_idx );
1415 0 : }
1416 :
1417 1518 : txn_tail = txne;
1418 1518 : txn = txne->fork.next;
1419 1518 : }
1420 495 : txn_pool_release_chain( accdb->txn_pool, txn_head, txn_tail );
1421 495 : }
1422 :
1423 498 : uint parent_txn = parent_fork->shmem->txn_head;
1424 498 : if( parent_txn!=UINT_MAX ) {
1425 66 : fd_accdb_txn_t * parent_head = txn_pool_ele( accdb->txn_pool, (ulong)parent_txn );
1426 66 : fd_accdb_txn_t * parent_tail = NULL;
1427 1365 : while( parent_txn!=UINT_MAX ) {
1428 1299 : fd_accdb_txn_t * t = txn_pool_ele( accdb->txn_pool, (ulong)parent_txn );
1429 1299 : parent_tail = t;
1430 1299 : parent_txn = t->fork.next;
1431 1299 : }
1432 66 : txn_pool_release_chain( accdb->txn_pool, parent_head, parent_tail );
1433 66 : }
1434 :
1435 : /* Remove the parent from all descends_sets and chain it for deferred
1436 : release, so that when the slot is eventually recycled to a new
1437 : fork, no concurrent reader can mistake the new fork for the old
1438 : ancestor. Entries from the freed parent are still visible via the
1439 : generation <= root_generation fast path in reads. */
1440 498 : fd_accdb_fork_id_t old_parent_id = fork->shmem->parent_id;
1441 498 : fork_slot_defer( accdb, old_parent_id, &fork_head, &fork_tail );
1442 :
1443 498 : fork->shmem->parent_id = (fd_accdb_fork_id_t){ .val = USHORT_MAX };
1444 498 : fork->shmem->sibling_id = (fd_accdb_fork_id_t){ .val = USHORT_MAX };
1445 498 : fork->shmem->txn_head = UINT_MAX;
1446 498 : descends_set_null( fork->descends );
1447 :
1448 : /* Publish the new root_fork_id BEFORE bumping the epoch and deferring
1449 : the parent slot. On x86-64 (TSO) a concurrent reader that still
1450 : loads the old root_fork_id is guaranteed to see the parent shmem in
1451 : its original (not-yet-recycled) state because the slot has not been
1452 : released yet. A reader that loads the new root_fork_id uses the
1453 : new fork. */
1454 498 : fd_racesan_hook( "accdb_advance:pre_publish_root" );
1455 498 : accdb->shmem->root_fork_id = fork_id;
1456 498 : FD_COMPILER_MFENCE();
1457 498 : fd_racesan_hook( "accdb_advance:post_publish_root" );
1458 :
1459 : /* Bump epoch and defer both the acc batch and parent fork slot. They
1460 : will be released at the next drain_deferred_frees call once all
1461 : concurrent readers have exited. The acc batch lives in the shmem
1462 : side buffer; only its epoch tag needs setting here. */
1463 498 : ulong tag = FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->epoch, 1UL );
1464 498 : if( FD_LIKELY( accdb->shmem->deferred_acc_buf_cnt ) ) {
1465 492 : accdb->shmem->deferred_acc_epoch = tag;
1466 492 : }
1467 498 : if( FD_LIKELY( fork_head ) ) {
1468 498 : accdb->deferred_fork_head = fork_head;
1469 498 : accdb->deferred_fork_tail = fork_tail;
1470 498 : accdb->deferred_fork_epoch = tag;
1471 498 : }
1472 498 : }
1473 :
1474 : void
1475 : fd_accdb_advance_root( fd_accdb_t * accdb,
1476 498 : fd_accdb_fork_id_t fork_id ) {
1477 498 : FD_CHECK_CRIT( fd_accdb_snapshot_sync_state( &accdb->shmem->snapshot_sync )!=FD_ACCDB_SNAPSHOT_SYNC_RUNNING,
1478 498 : "fd_accdb_advance_root called during snapshot production" );
1479 498 : wait_cmd( accdb );
1480 498 : submit_cmd( accdb, FD_ACCDB_CMD_ADVANCE_ROOT, fork_id.val );
1481 498 : }
1482 :
1483 : /* background_purge does the heavy lifting of purge on T2: unlink the
1484 : fork from the parent's child list, drain deferred frees, recursively
1485 : purge the fork subtree, and defer-release the freed acc pool
1486 : elements. The sibling-list unlink is done here (not on T1) because
1487 : advance_root / remove_children also mutate sibling lists on T2, and
1488 : T2 is single-threaded so plain stores are safe. */
1489 :
1490 : static void
1491 : background_purge( fd_accdb_t * accdb,
1492 21 : fd_accdb_fork_id_t fork_id ) {
1493 : /* Unlink fork_id from its parent's child list. This runs on T2
1494 : which is the sole mutator of sibling lists (advance_root and
1495 : remove_children also run on T2), so plain stores are safe. */
1496 21 : fd_accdb_fork_t * fork = &accdb->fork_pool[ fork_id.val ];
1497 21 : fd_accdb_fork_id_t parent_id = fork->shmem->parent_id;
1498 21 : if( FD_LIKELY( parent_id.val!=USHORT_MAX ) ) {
1499 21 : fd_accdb_fork_t * parent = &accdb->fork_pool[ parent_id.val ];
1500 21 : if( FD_UNLIKELY( parent->shmem->child_id.val==fork_id.val ) ) {
1501 21 : parent->shmem->child_id = fork->shmem->sibling_id;
1502 21 : } else {
1503 0 : fd_accdb_fork_id_t prev_id = parent->shmem->child_id;
1504 0 : while( prev_id.val!=USHORT_MAX ) {
1505 0 : fd_accdb_fork_t * prev = &accdb->fork_pool[ prev_id.val ];
1506 0 : if( prev->shmem->sibling_id.val==fork_id.val ) {
1507 0 : prev->shmem->sibling_id = fork->shmem->sibling_id;
1508 0 : break;
1509 0 : }
1510 0 : prev_id = prev->shmem->sibling_id;
1511 0 : }
1512 0 : }
1513 21 : }
1514 :
1515 21 : drain_deferred_frees( accdb );
1516 :
1517 21 : fd_accdb_fork_shmem_t * fork_head = NULL;
1518 21 : fd_accdb_fork_shmem_t * fork_tail = NULL;
1519 21 : purge_inner( accdb, fork_id, &fork_head, &fork_tail );
1520 :
1521 21 : ulong tag = FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->epoch, 1UL );
1522 21 : if( FD_LIKELY( accdb->shmem->deferred_acc_buf_cnt ) ) {
1523 18 : accdb->shmem->deferred_acc_epoch = tag;
1524 18 : }
1525 21 : if( FD_LIKELY( fork_head ) ) {
1526 21 : accdb->deferred_fork_head = fork_head;
1527 21 : accdb->deferred_fork_tail = fork_tail;
1528 21 : accdb->deferred_fork_epoch = tag;
1529 21 : }
1530 21 : }
1531 :
1532 : void
1533 : fd_accdb_purge( fd_accdb_t * accdb,
1534 21 : fd_accdb_fork_id_t fork_id ) {
1535 21 : FD_TEST( fork_id.val!=accdb->shmem->root_fork_id.val );
1536 :
1537 21 : wait_cmd( accdb );
1538 21 : submit_cmd( accdb, FD_ACCDB_CMD_PURGE, fork_id.val );
1539 21 : }
1540 :
1541 : static inline fd_accdb_cache_line_t *
1542 : acquire_cache_line( fd_accdb_t * accdb,
1543 : ulong size_class,
1544 918438 : uint * out_evicted_acc_idx ) {
1545 : /* Priority 1: CAS free list — already invalidated,
1546 : persisted==1, generation==UINT_MAX. Cheapest path. */
1547 918438 : fd_accdb_cache_line_t * result = cache_free_pop( accdb, size_class );
1548 918438 : if( FD_LIKELY( result ) ) {
1549 776376 : while( FD_UNLIKELY( FD_ATOMIC_CAS( &result->refcnt, 0U, 1U )!=0U ) ) {
1550 0 : fd_racesan_hook( "accdb_freepop:refcnt_wait" );
1551 0 : FD_SPIN_PAUSE();
1552 0 : }
1553 776376 : result->referenced = 0;
1554 776376 : *out_evicted_acc_idx = UINT_MAX;
1555 776376 : return result;
1556 776376 : }
1557 :
1558 : /* Priority 2: Lazy initial allocation — atomic FAA with undo on
1559 : overflow. Safe for concurrent callers. */
1560 142062 : ulong old_init = FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->cache_class_init[ size_class ].val, 1UL );
1561 142062 : if( FD_LIKELY( old_init<accdb->shmem->cache_class_max[ size_class ] ) ) {
1562 142050 : result = cache_line( accdb, size_class, old_init );
1563 142050 : result->refcnt = 1;
1564 142050 : result->persisted = 1;
1565 142050 : result->referenced = 0;
1566 142050 : result->acc_idx = UINT_MAX;
1567 142050 : result->key.generation = UINT_MAX;
1568 142050 : *out_evicted_acc_idx = UINT_MAX;
1569 142050 : return result;
1570 142050 : }
1571 12 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->cache_class_init[ size_class ].val, 1UL );
1572 :
1573 : /* Priority 3: CLOCK sweep ... scan forward giving second chances. */
1574 30 : for(;;) {
1575 30 : ulong hand = FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->clock_hand[ size_class ].val, 1UL ) % accdb->shmem->cache_class_max[ size_class ];
1576 30 : fd_accdb_cache_line_t * line = cache_line( accdb, size_class, hand );
1577 :
1578 30 : if( FD_UNLIKELY( line->key.generation==UINT_MAX && line->acc_idx==UINT_MAX ) ) continue;
1579 :
1580 30 : uint rc = FD_VOLATILE_CONST( line->refcnt );
1581 30 : if( FD_UNLIKELY( rc!=0U ) ) continue; /* Pinned or being evicted */
1582 :
1583 30 : if( FD_UNLIKELY( line->referenced ) ) {
1584 18 : line->referenced = 0;
1585 18 : continue; /* Second chance */
1586 18 : }
1587 :
1588 12 : if( FD_UNLIKELY( FD_ATOMIC_CAS( &line->refcnt, 0U, FD_ACCDB_EVICT_SENTINEL )!=0U ) ) continue;
1589 :
1590 : /* The line is now claimed for eviction (refcnt==EVICT_SENTINEL). A
1591 : concurrent acc_unlink that targets this same line's accmeta will
1592 : observe the sentinel here and take its do-nothing branch — see the
1593 : test_accdb_racesan SENTINEL case. */
1594 12 : fd_racesan_hook( "clock_evict:post_sentinel" );
1595 :
1596 12 : if( FD_LIKELY( line->acc_idx!=UINT_MAX ) ) {
1597 12 : evict_clear_acc_cache_ref( &accdb->acc_pool[ line->acc_idx ], size_class, hand );
1598 12 : }
1599 12 : *out_evicted_acc_idx = line->persisted ? UINT_MAX : line->acc_idx;
1600 12 : line->key.generation = UINT_MAX;
1601 12 : line->refcnt = 1;
1602 12 : line->referenced = 0;
1603 12 : return line;
1604 12 : }
1605 :
1606 0 : FD_TEST( 0 );
1607 0 : return NULL;
1608 0 : }
1609 :
1610 : static inline void
1611 : change_partition( fd_accdb_t * accdb,
1612 : accdb_offset_t const * offset_before,
1613 : accdb_offset_t * out_offset,
1614 : int * has_partition,
1615 63 : uchar layer ) {
1616 : /* New data will not fit in the current partition, so we need to
1617 : move to the next one. */
1618 63 : ulong partition_idx_before = packed_partition_idx( offset_before );
1619 63 : ulong partition_offset_before = packed_partition_offset( offset_before );
1620 63 : if( FD_LIKELY( *has_partition ) ) {
1621 39 : fd_accdb_partition_t * before = partition_pool_ele( accdb->partition_pool, partition_idx_before );
1622 39 : before->write_offset = partition_offset_before;
1623 39 : }
1624 :
1625 : /* Single rdtsc per partition lifecycle event: stamp the closing
1626 : partition's filled time and the new partition's created time off
1627 : the same sample. */
1628 63 : long now_ticks = (long)fd_tickcount();
1629 :
1630 63 : ulong free_size = accdb->shmem->partition_sz - partition_offset_before;
1631 63 : if( FD_LIKELY( *has_partition ) ) {
1632 39 : fd_accdb_partition_t * old = partition_pool_ele( accdb->partition_pool, partition_idx_before );
1633 39 : FD_ATOMIC_FETCH_AND_ADD( &old->bytes_freed, free_size );
1634 39 : FD_VOLATILE( old->filled_ticks ) = now_ticks;
1635 : /* The tail slack is now committed dead — count it as current
1636 : (written-through) so fragmentation reflects it. */
1637 39 : FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->shmetrics->disk_current_bytes, free_size );
1638 39 : }
1639 :
1640 63 : if( FD_UNLIKELY( !partition_pool_free( accdb->partition_pool ) ) ) FD_LOG_ERR(( "accounts database file is at capacity" ));
1641 63 : fd_accdb_partition_t * partition = partition_pool_ele_acquire( accdb->partition_pool );
1642 63 : partition->bytes_freed = 0UL;
1643 63 : partition->marked_compaction = 0;
1644 63 : partition->layer = layer;
1645 63 : partition->read_ops = 0UL;
1646 63 : partition->bytes_read = 0UL;
1647 63 : partition->write_ops = 0UL;
1648 63 : partition->bytes_written = 0UL;
1649 63 : partition->write_offset = 0UL;
1650 63 : partition->compaction_offset = 0UL;
1651 63 : partition->created_ticks = now_ticks;
1652 63 : partition->filled_ticks = 0L;
1653 63 : partition->queued = 0;
1654 63 : partition->compacting_now = 0;
1655 :
1656 63 : ulong new_partition_idx = partition_pool_idx( accdb->partition_pool, partition );
1657 63 : int had_partition = *has_partition;
1658 63 : *out_offset = accdb_offset( new_partition_idx, 0UL );
1659 63 : *has_partition = 1;
1660 :
1661 : /* Now that the write head has been rotated away from the old
1662 : partition, check if it should be enqueued for compaction. We call
1663 : try_enqueue directly because the caller already holds
1664 : partition_lock (calling fd_accdb_shmem_bytes_freed here would
1665 : deadlock on the non-reentrant lock). Skip when
1666 : has_partition was 0, because the sentinel partition_idx is
1667 : not a valid pool element. */
1668 63 : if( FD_LIKELY( had_partition && partition_idx_before!=new_partition_idx ) ) {
1669 39 : fd_accdb_shmem_try_enqueue_compaction( accdb->shmem, partition_idx_before );
1670 39 : }
1671 :
1672 : /* Snapshot-load tiering: accounts loaded from a snapshot never get
1673 : a second write, so compaction-driven promotion never fires and
1674 : they would otherwise live in Hot forever. When snapshot_loading
1675 : is set, tag the new partition as Cold up front. We do not set
1676 : has_partition[Cold] / whead[Cold] — those are owned by the
1677 : compaction tile and represent the live Cold write head, which is
1678 : independent of snapshot-loaded partitions that happen to be
1679 : labeled Cold. */
1680 63 : if( FD_UNLIKELY( accdb->snapshot_loading && layer==0 ) ) {
1681 51 : FD_VOLATILE( partition->layer ) = FD_ACCDB_COMPACTION_LAYER_CNT-1UL;
1682 51 : }
1683 :
1684 63 : if( FD_UNLIKELY( new_partition_idx>=accdb->shmem->partition_max ) ) {
1685 63 : FD_LOG_INFO(( "growing accounts database from %lu GiB to %lu GiB", accdb->shmem->partition_max*accdb->shmem->partition_sz/(1UL<<30UL), (new_partition_idx+1UL)*accdb->shmem->partition_sz/(1UL<<30UL) ));
1686 :
1687 63 : int result = fallocate( accdb->fd, 0, (long)(new_partition_idx*accdb->shmem->partition_sz), (long)accdb->shmem->partition_sz );
1688 63 : if( FD_UNLIKELY( -1==result ) ) {
1689 0 : if( FD_LIKELY( errno==ENOSPC ) ) FD_LOG_ERR(( "fallocate() failed (%d-%s). The accounts database filled "
1690 0 : "the disk it is on, trying to grow from %lu GiB to %lu GiB. Please "
1691 0 : "free up disk space and restart the validator.",
1692 0 : errno, fd_io_strerror( errno ), accdb->shmem->partition_max*accdb->shmem->partition_sz/(1UL<<30UL), (new_partition_idx+1UL)*accdb->shmem->partition_sz/(1UL<<30UL) ));
1693 0 : else FD_LOG_ERR(( "fallocate() failed (%d-%s)", errno, fd_io_strerror( errno ) ));
1694 0 : }
1695 :
1696 : /* CAS loop: the compaction tile may also be growing the file
1697 : concurrently, so neither path may clobber the other. */
1698 63 : for(;;) {
1699 63 : ulong cur = accdb->shmem->partition_max;
1700 63 : if( FD_LIKELY( new_partition_idx+1UL<=cur ) ) break;
1701 63 : if( FD_LIKELY( FD_ATOMIC_CAS( &accdb->shmem->partition_max, cur, new_partition_idx+1UL )==cur ) ) {
1702 63 : fd_event_accdb_partition_added_t ev = {
1703 63 : .partition_idx = new_partition_idx,
1704 63 : .prior_partition_idx = had_partition ? partition_idx_before : ULONG_MAX,
1705 63 : .layer = layer,
1706 63 : .old_partition_max = cur,
1707 63 : .new_partition_max = new_partition_idx+1UL,
1708 63 : .partition_sz = accdb->shmem->partition_sz,
1709 63 : .disk_allocated_bytes = (new_partition_idx+1UL)*accdb->shmem->partition_sz,
1710 63 : };
1711 63 : fd_event_report_accdb_partition_added( &ev );
1712 63 : break;
1713 63 : }
1714 63 : }
1715 63 : accdb->shmem->shmetrics->disk_allocated_bytes = accdb->shmem->partition_max*accdb->shmem->partition_sz;
1716 63 : }
1717 63 : }
1718 :
1719 : /* Reserve sz bytes in the layer-0 write head and set
1720 : out_partition_idx to where they landed. Bumps no counters. */
1721 :
1722 : static inline ulong
1723 : reserve_next_write( fd_accdb_t * accdb,
1724 : ulong sz,
1725 99 : ulong * out_partition_idx ) {
1726 138 : for(;;) {
1727 138 : accdb_offset_t offset = { .val = FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->whead[ 0 ].val, sz ) };
1728 138 : if( FD_LIKELY( packed_partition_offset( &offset )+sz<=accdb->shmem->partition_sz ) ) {
1729 99 : *out_partition_idx = packed_partition_idx( &offset );
1730 99 : return packed_partition_file_offset( &offset, accdb->shmem->partition_sz );
1731 99 : }
1732 :
1733 39 : if( FD_UNLIKELY( packed_partition_offset( &offset )>accdb->shmem->partition_sz ) ) {
1734 : /* This can happen if another thread also raced to allocate the
1735 : next write and won. Wait for the partition switch to finish
1736 : before retrying, so we do not keep doing fetch-and-adds that
1737 : advance the offset further past the boundary.
1738 :
1739 : A switch is detected by the head moving to a different
1740 : partition index OR its offset dropping back to a valid position
1741 : (a switch resets the offset to 0). We must not key the wait
1742 : solely on the index changing: the initial write head is a
1743 : sentinel whose packed index can coincide with a real pool
1744 : index. */
1745 0 : ulong stale_partition = packed_partition_idx( &offset );
1746 0 : for(;;) {
1747 0 : accdb_offset_t cur = { .val = FD_VOLATILE_CONST( accdb->shmem->whead[ 0 ].val ) };
1748 0 : if( packed_partition_idx( &cur )!=stale_partition ) break;
1749 0 : if( packed_partition_offset( &cur )<=accdb->shmem->partition_sz ) break;
1750 0 : FD_SPIN_PAUSE();
1751 0 : }
1752 0 : continue;
1753 0 : }
1754 :
1755 39 : spin_lock_acquire( &accdb->shmem->partition_lock );
1756 39 : change_partition( accdb, &offset, &accdb->shmem->whead[ 0 ], &accdb->shmem->has_partition[ 0 ], 0 );
1757 39 : spin_lock_release( &accdb->shmem->partition_lock );
1758 39 : }
1759 99 : }
1760 :
1761 : /* Reserve sz bytes. Layer-0 write metrics are deferred until
1762 : explicitly flushed. */
1763 :
1764 : static inline ulong
1765 : allocate_next_write( fd_accdb_t * accdb,
1766 99 : ulong sz ) {
1767 99 : ulong partition_idx;
1768 99 : ulong file_offset = reserve_next_write( accdb, sz, &partition_idx );
1769 :
1770 : /* Very rarely the reservation crosses into a new partition. Since
1771 : stats are aggregated per-partition, any accumulated stats are
1772 : flushed and reset to accommodate the new partition. */
1773 99 : if( FD_LIKELY( accdb->write_stats.num_ops ) &&
1774 99 : FD_UNLIKELY( accdb->write_stats.partition_idx!=partition_idx ) ) {
1775 15 : fd_accdb_flush_metrics( accdb );
1776 15 : }
1777 :
1778 99 : accdb->write_stats.partition_idx = partition_idx;
1779 99 : accdb->write_stats.bytes += sz;
1780 99 : accdb->write_stats.num_ops++;
1781 99 : return file_offset;
1782 99 : }
1783 :
1784 : /* Compaction write allocation. Single-threaded: only the compaction
1785 : tile calls these, so the compaction write heads do not need atomic
1786 : fetch-and-add. dest_layer is the target layer (1..N-1). */
1787 :
1788 : static inline ulong
1789 : allocate_next_compaction_write( fd_accdb_t * accdb,
1790 : ulong sz,
1791 3 : ulong dest_layer ) {
1792 3 : accdb_offset_t offset = accdb->shmem->whead[ dest_layer ];
1793 3 : if( FD_UNLIKELY( !accdb->shmem->has_partition[ dest_layer ] ||
1794 3 : packed_partition_offset( &offset )+sz>accdb->shmem->partition_sz ) ) {
1795 3 : spin_lock_acquire( &accdb->shmem->partition_lock );
1796 3 : change_partition( accdb, &offset, &accdb->shmem->whead[ dest_layer ], &accdb->shmem->has_partition[ dest_layer ], (uchar)dest_layer );
1797 3 : spin_lock_release( &accdb->shmem->partition_lock );
1798 3 : offset = accdb->shmem->whead[ dest_layer ];
1799 3 : }
1800 3 : accdb->shmem->whead[ dest_layer ].val += sz;
1801 3 : FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->shmetrics->disk_current_bytes, sz );
1802 3 : ulong file_offset = packed_partition_file_offset( &offset, accdb->shmem->partition_sz );
1803 3 : fd_accdb_partition_write_bump( accdb, packed_partition_idx( &offset ), sz, 1UL );
1804 3 : return file_offset;
1805 3 : }
1806 :
1807 : /* fd_accdb_compact relocates one record from the oldest partition
1808 : queued for compaction at src_layer into the write head for the
1809 : next colder tier, or the same tier for the deepest layer. It is
1810 : designed to be called repeatedly from a dedicated compaction tile.
1811 : If there is work to do, *charge_busy is set to 1; otherwise 0 is
1812 : left unchanged and the call returns immediately.
1813 :
1814 : src_layer must be in 0..FD_ACCDB_COMPACTION_LAYER_CNT-1. */
1815 :
1816 : static void
1817 : background_compact( fd_accdb_t * accdb,
1818 : ulong src_layer,
1819 10126770 : int * charge_busy ) {
1820 10126770 : FD_COMPILER_MFENCE();
1821 10126770 : FD_VOLATILE( *accdb->my_epoch_slot ) = FD_VOLATILE_CONST( accdb->shmem->epoch );
1822 10126770 : FD_HW_MFENCE(); /* StoreLoad: epoch store must be globally visible
1823 : before any subsequent loads so the deferred
1824 : reclamation scan does not miss us. */
1825 :
1826 : /* Reclaim any deferred-free partitions whose epoch has been observed
1827 : by all joiners (i.e. no epoch-publishing joiner could still be
1828 : referencing data in them). Scan writer slots [0, joiner_cnt)
1829 : plus each external (read-only) joiner's private epoch fseq. */
1830 10126770 : ulong min_epoch = ULONG_MAX;
1831 10126770 : ulong joiner_cnt = FD_VOLATILE_CONST( accdb->shmem->joiner_cnt );
1832 25560726 : for( ulong t=0UL; t<joiner_cnt; t++ ) {
1833 15433956 : ulong e = FD_VOLATILE_CONST( accdb->shmem->joiner_epochs[ t ].val );
1834 15433956 : if( FD_LIKELY( e<min_epoch ) ) min_epoch = e;
1835 15433956 : }
1836 10126770 : for( ulong t=0UL; t<accdb->external_epoch_cnt; t++ ) {
1837 0 : ulong e = FD_VOLATILE_CONST( *accdb->external_epoch_slots[ t ] );
1838 0 : if( FD_LIKELY( e<min_epoch ) ) min_epoch = e;
1839 0 : }
1840 10126773 : for(;;) {
1841 10126773 : if( FD_LIKELY( deferred_free_dlist_is_empty( accdb->deferred_free_dlist, accdb->partition_pool ) ) ) break;
1842 3 : fd_accdb_partition_t * p = deferred_free_dlist_ele_peek_head( accdb->deferred_free_dlist, accdb->partition_pool );
1843 3 : if( FD_LIKELY( p->epoch_tag>=min_epoch ) ) break;
1844 :
1845 3 : fd_racesan_hook( "accdb_reclaim:pre_free_partition" );
1846 :
1847 3 : spin_lock_acquire( &accdb->shmem->partition_lock );
1848 3 : deferred_free_dlist_ele_pop_head( accdb->deferred_free_dlist, accdb->partition_pool );
1849 3 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->shmetrics->disk_current_bytes, accdb->shmem->partition_sz );
1850 3 : partition_pool_ele_release( accdb->partition_pool, p );
1851 3 : spin_lock_release( &accdb->shmem->partition_lock );
1852 3 : }
1853 :
1854 10126770 : if( FD_LIKELY( compaction_dlist_is_empty( accdb->compaction_dlist[ src_layer ], accdb->partition_pool ) ) ) {
1855 10126761 : FD_COMPILER_MFENCE();
1856 10126761 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
1857 10126761 : return;
1858 10126761 : }
1859 9 : fd_accdb_partition_t * compact = compaction_dlist_ele_peek_head( accdb->compaction_dlist[ src_layer ], accdb->partition_pool );
1860 9 : if( FD_UNLIKELY( !compact ) ) {
1861 0 : FD_COMPILER_MFENCE();
1862 0 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
1863 0 : return;
1864 0 : }
1865 :
1866 : /* Wait until all epoch-publishing joiners that were active when this
1867 : partition was enqueued for compaction have exited, ensuring any
1868 : in-flight pwritev2 to this partition has completed before we start
1869 : reading from it. */
1870 9 : if( FD_UNLIKELY( compact->compaction_ready_epoch>=min_epoch ) ) {
1871 0 : FD_COMPILER_MFENCE();
1872 0 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
1873 0 : return;
1874 0 : }
1875 :
1876 9 : *charge_busy = 1;
1877 :
1878 9 : if( FD_UNLIKELY( !compact->compacting_now ) ) {
1879 3 : compact->compaction_start_wallclock = fd_log_wallclock();
1880 3 : compact->compaction_accounts_relocated = 0UL;
1881 3 : compact->compaction_bytes_relocated = 0UL;
1882 3 : compact->compaction_dead_records = 0UL;
1883 3 : }
1884 9 : FD_VOLATILE( compact->queued ) = 0;
1885 9 : FD_VOLATILE( compact->compacting_now ) = 1;
1886 :
1887 9 : fd_accdb_disk_meta_t meta[1];
1888 :
1889 9 : ulong compact_base = partition_pool_idx( accdb->partition_pool, compact )*accdb->shmem->partition_sz;
1890 :
1891 : /* Read the on-disk metadata header at the current compaction
1892 : cursor within the partition being compacted. */
1893 9 : ulong bytes_read = 0UL;
1894 18 : while( FD_UNLIKELY( bytes_read<sizeof(fd_accdb_disk_meta_t) ) ) {
1895 9 : long result = pread( accdb->fd, ((uchar *)meta)+bytes_read, sizeof(fd_accdb_disk_meta_t)-bytes_read, (long)(compact_base+compact->compaction_offset+bytes_read) );
1896 9 : if( FD_UNLIKELY( -1==result && (errno==EINTR || errno==EAGAIN || errno==EWOULDBLOCK ) ) ) continue;
1897 9 : else if( FD_UNLIKELY( -1==result ) ) FD_LOG_ERR(( "pread() failed (%d-%s)", errno, fd_io_strerror( errno ) ));
1898 9 : else if( FD_UNLIKELY( !result ) ) FD_LOG_ERR(( "accounts database is corrupt, data expected at offset %lu with size %lu exceeded file extents",
1899 9 : compact_base+compact->compaction_offset+bytes_read, sizeof(fd_accdb_disk_meta_t) ));
1900 9 : fd_accdb_partition_read_bump( accdb, compact_base+compact->compaction_offset, (ulong)result );
1901 9 : bytes_read += (ulong)result;
1902 9 : }
1903 :
1904 : /* Walk the hash chain to find a live index entry whose on-disk
1905 : offset matches the record we are compacting. */
1906 9 : fd_accdb_accmeta_t * accmeta = NULL;
1907 9 : ulong source_packed = 0UL;
1908 9 : uint acc_idx = FD_VOLATILE_CONST( accdb->acc_map[ fd_accdb_hash( meta->pubkey, accdb->shmem->seed )&(accdb->shmem->chain_cnt-1UL) ] );
1909 15 : while( acc_idx!=UINT_MAX ) {
1910 9 : fd_accdb_accmeta_t * candidate = &accdb->acc_pool[ acc_idx ];
1911 9 : uint next_idx = FD_VOLATILE_CONST( candidate->map.next );
1912 9 : ulong candidate_packed = FD_VOLATILE_CONST( candidate->offset_fork );
1913 9 : if( FD_LIKELY( (candidate_packed & FD_ACCDB_OFF_MASK)==compact_base+compact->compaction_offset ) ) {
1914 3 : accmeta = candidate;
1915 3 : source_packed = candidate_packed;
1916 3 : break;
1917 3 : }
1918 6 : acc_idx = next_idx;
1919 6 : }
1920 :
1921 9 : ulong record_sz = sizeof(fd_accdb_disk_meta_t) + (ulong)meta->size;
1922 9 : ulong bytes_copied = 0UL;
1923 9 : if( FD_UNLIKELY( !accmeta ) ) {
1924 : /* Dead record — the index entry was already removed, so this
1925 : on-disk extent is garbage. Nothing to relocate. */
1926 6 : compact->compaction_dead_records++;
1927 6 : } else {
1928 3 : ulong dest_layer = fd_ulong_min( src_layer+1UL, FD_ACCDB_COMPACTION_LAYER_CNT-1UL );
1929 3 : ulong dest_offset = allocate_next_compaction_write( accdb, record_sz, dest_layer );
1930 :
1931 6 : while( FD_UNLIKELY( bytes_copied<record_sz ) ) {
1932 3 : long in_off = (long)(compact_base + compact->compaction_offset + bytes_copied);
1933 3 : long out_off = (long)(dest_offset + bytes_copied);
1934 :
1935 3 : long result = copy_file_range( accdb->fd, &in_off, accdb->fd, &out_off, record_sz-bytes_copied, 0 );
1936 3 : if( FD_UNLIKELY( -1==result && (errno==EINTR || errno==EAGAIN || errno==EWOULDBLOCK ) ) ) continue;
1937 3 : else if( FD_UNLIKELY( -1==result ) ) FD_LOG_ERR(( "copy_file_range() failed (%d-%s)", errno, fd_io_strerror( errno ) ));
1938 3 : else if( FD_UNLIKELY( !result ) ) FD_LOG_ERR(( "accounts database is corrupt, data expected at offset %lu with size %lu exceeded file extents",
1939 3 : compact_base+compact->compaction_offset+bytes_copied, record_sz ));
1940 3 : fd_accdb_partition_read_bump( accdb, compact_base+compact->compaction_offset+bytes_copied, (ulong)result );
1941 3 : bytes_copied += (ulong)result;
1942 3 : accdb->metrics->copy_ops++;
1943 3 : }
1944 :
1945 3 : accdb->shmem->shmetrics->accounts_relocated++;
1946 3 : accdb->shmem->shmetrics->accounts_relocated_bytes += bytes_copied;
1947 3 : compact->compaction_accounts_relocated++;
1948 3 : compact->compaction_bytes_relocated += bytes_copied;
1949 :
1950 : /* Ensure the data is on disk before publishing the new offset,
1951 : so concurrent acquire threads do not preadv2 from a location
1952 : that hasn't been written yet. */
1953 3 : FD_COMPILER_MFENCE();
1954 :
1955 : /* CAS the offset from the exact source record we copied to the new
1956 : destination. If a concurrent release overwrote the offset to
1957 : FD_ACCDB_OFF_INVAL (dirty sentinel for a new commit), or later
1958 : published a newer on-disk location, the CAS fails and we treat
1959 : the relocated copy as stale. We CAS the full packed
1960 : offset_fork so the fork_id is preserved and so we only publish
1961 : the relocation if the copied source record is still current. */
1962 3 : ulong new_packed = ( source_packed & ~FD_ACCDB_OFF_MASK ) | ( dest_offset & FD_ACCDB_OFF_MASK );
1963 :
1964 : #if FD_HAS_RACESAN
1965 : fd_memcpy( fd_accdb_dbg_reloc_pubkey, accmeta->key.pubkey, 32UL );
1966 : fd_accdb_dbg_reloc_dest = dest_offset;
1967 : fd_accdb_dbg_reloc_cnt++;
1968 : #endif
1969 :
1970 3 : fd_racesan_hook( "accdb_compact:pre_offset_cas" );
1971 3 : if( FD_UNLIKELY( FD_ATOMIC_CAS( &accmeta->offset_fork, source_packed, new_packed )!=source_packed ) ) {
1972 : /* Record was superseded by a concurrent overwrite commit.
1973 : The disk space we just wrote is dead on arrival — account
1974 : it as freed so compaction can reclaim it later. */
1975 0 : fd_accdb_shmem_bytes_freed( accdb->shmem, dest_offset, record_sz );
1976 0 : bytes_copied = 0UL;
1977 0 : }
1978 3 : }
1979 :
1980 9 : fd_racesan_hook( "accdb_compact:post_relocate" );
1981 :
1982 9 : compact->compaction_offset += record_sz;
1983 :
1984 9 : if( FD_UNLIKELY( compact->compaction_offset>=compact->write_offset ) ) {
1985 3 : FD_LOG_INFO(( "compaction of partition %lu completed", partition_pool_idx( accdb->partition_pool, compact ) ));
1986 :
1987 3 : fd_event_accdb_compaction_completed_t ev = {
1988 3 : .partition_idx = partition_pool_idx( accdb->partition_pool, compact ),
1989 3 : .src_layer = (uchar)src_layer,
1990 3 : .dest_layer = (uchar)fd_ulong_min( src_layer+1UL, FD_ACCDB_COMPACTION_LAYER_CNT-1UL ),
1991 3 : .bytes_scanned = compact->write_offset,
1992 3 : .bytes_freed = compact->bytes_freed,
1993 3 : .accounts_relocated = compact->compaction_accounts_relocated,
1994 3 : .bytes_relocated = compact->compaction_bytes_relocated,
1995 3 : .dead_records = compact->compaction_dead_records,
1996 3 : .start_time = (ulong)compact->compaction_start_wallclock,
1997 3 : .end_time = (ulong)fd_log_wallclock(),
1998 3 : };
1999 3 : fd_event_report_accdb_compaction_completed( &ev );
2000 :
2001 : /* Ensure the new acc->offset_fork stores above are visible to other
2002 : cores before the source partition is moved to the deferred-free
2003 : list. On x86 (TSO) hardware store ordering already guarantees
2004 : this, but the compiler fence prevents the compiler from sinking
2005 : the offset store past the inlined pool/dlist mutations below. */
2006 3 : FD_COMPILER_MFENCE();
2007 :
2008 : /* Bump the global epoch and tag this partition so the reclamation
2009 : scan knows when all epoch-publishing joiners that could reference
2010 : data in this partition have exited. */
2011 3 : ulong tag = FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->epoch, 1UL );
2012 3 : compact->epoch_tag = tag;
2013 :
2014 : /* partition_lock serializes these dlist/pool mutations with
2015 : concurrent push_tail in fd_accdb_shmem_bytes_freed and
2016 : partition_pool_ele_acquire in change_partition. Neither fd_dlist
2017 : nor fd_pool are thread-safe, so all mutations must be under the
2018 : same lock. */
2019 3 : spin_lock_acquire( &accdb->shmem->partition_lock );
2020 :
2021 3 : accdb->shmem->shmetrics->partitions_freed++;
2022 3 : compaction_dlist_ele_pop_head( accdb->compaction_dlist[ src_layer ], accdb->partition_pool );
2023 3 : FD_VOLATILE( compact->compacting_now ) = 0;
2024 3 : FD_VOLATILE( compact->queued ) = 0;
2025 3 : deferred_free_dlist_ele_push_tail( accdb->deferred_free_dlist, compact, accdb->partition_pool );
2026 :
2027 3 : accdb->shmem->shmetrics->compactions_completed++;
2028 3 : if( FD_LIKELY( compaction_dlist_is_empty( accdb->compaction_dlist[ src_layer ], accdb->partition_pool ) ) ) {
2029 3 : accdb->shmem->shmetrics->in_compaction = 0;
2030 3 : } else {
2031 0 : fd_accdb_partition_t * next = compaction_dlist_ele_peek_head( accdb->compaction_dlist[ src_layer ], accdb->partition_pool );
2032 0 : FD_LOG_INFO(( "compaction of layer %lu partition %lu started", src_layer, partition_pool_idx( accdb->partition_pool, next ) ));
2033 0 : }
2034 :
2035 3 : spin_lock_release( &accdb->shmem->partition_lock );
2036 3 : }
2037 :
2038 9 : accdb->metrics->bytes_read += bytes_read + bytes_copied;
2039 9 : accdb->metrics->bytes_written += bytes_copied;
2040 :
2041 9 : FD_COMPILER_MFENCE();
2042 9 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
2043 9 : }
2044 :
2045 : /* cold_load_acc resolves the cache slot for `acc` when STEP 1's
2046 : cache_try_pin failed. It uses bit 29 of executable_size as a
2047 : single-claimer lock so that two concurrent acquirers cannot each
2048 : install their own cache slot for the same acc (which would orphan
2049 : one slot with a dangling line->acc_idx and eventually corrupt
2050 : acc->cache_valid via CLOCK).
2051 :
2052 : Protocol per acc:
2053 : - If cache_valid is set, retry cache_try_pin (another thread
2054 : finished the cold-load while we were here). On success, mark
2055 : exists_in_cache so STEP 4 will not write back the slot.
2056 : - If claim is set, spin (another thread is mid-cold-load).
2057 : - Otherwise CAS-set the claim bit. Winner allocates a cache
2058 : line, populates the placeholder (acc_idx=UINT_MAX), publishes
2059 : cache_idx, then atomically (CAS-loop) sets cache_valid and
2060 : clears claim.
2061 :
2062 : The eviction sites that clear cache_valid must use FETCH_AND with
2063 : ~CACHE_VALID_BIT (preserving the claim bit) to interact correctly
2064 : with this protocol. */
2065 :
2066 : static fd_accdb_cache_line_t *
2067 : cold_load_acc( fd_accdb_t * accdb,
2068 : fd_accdb_accmeta_t * accmeta,
2069 : uchar const * pubkey,
2070 : int * out_exists_in_cache,
2071 30 : uint * out_evicted_acc_idx ) {
2072 30 : for(;;) {
2073 30 : uint old_es = FD_VOLATILE_CONST( accmeta->executable_size );
2074 30 : int valid = FD_ACCDB_SIZE_CACHE_VALID( old_es );
2075 30 : int claimed = FD_ACCDB_SIZE_CACHE_CLAIM( old_es );
2076 :
2077 30 : if( FD_UNLIKELY( valid ) ) {
2078 : /* old_es snapshot saw VALID=1 but a concurrent
2079 : evict_clear_acc_cache_ref may have cleared VALID and stored
2080 : cache_idx=INVAL between our snapshot and this load. Decoding
2081 : INVAL would yield a wild cache_line pointer; retry the loop
2082 : instead (next iteration will see VALID=0). */
2083 0 : uint cidx = FD_VOLATILE_CONST( accmeta->cache_idx );
2084 0 : if( FD_UNLIKELY( cidx==FD_ACCDB_ACC_CIDX_INVAL ) ) { FD_SPIN_PAUSE(); continue; }
2085 0 : fd_accdb_cache_line_t * hit = cache_line( accdb, FD_ACCDB_ACC_CIDX_CLASS( cidx ), FD_ACCDB_ACC_CIDX_IDX( cidx ) );
2086 0 : fd_racesan_hook( "accdb_cold_load:pre_try_pin" );
2087 0 : fd_accdb_cache_line_t * pinned = cache_try_pin( hit, pubkey, accmeta->key.generation );
2088 0 : if( FD_LIKELY( pinned ) ) {
2089 0 : *out_exists_in_cache = 1;
2090 0 : *out_evicted_acc_idx = UINT_MAX;
2091 0 : return pinned;
2092 0 : }
2093 0 : FD_SPIN_PAUSE();
2094 0 : continue;
2095 0 : }
2096 :
2097 30 : if( FD_UNLIKELY( claimed ) ) {
2098 0 : fd_racesan_hook( "accdb_cold_load:claim_wait" );
2099 0 : FD_SPIN_PAUSE();
2100 0 : continue;
2101 0 : }
2102 :
2103 30 : if( FD_UNLIKELY( FD_ATOMIC_CAS( &accmeta->executable_size, old_es, old_es | FD_ACCDB_SIZE_CACHE_CLAIM_BIT )!=old_es ) ) {
2104 0 : FD_SPIN_PAUSE();
2105 0 : continue;
2106 0 : }
2107 :
2108 : /* We hold the claim. Allocate a cache line and publish. */
2109 30 : ulong size_class = fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( old_es ) );
2110 30 : fd_accdb_cache_line_t * line = acquire_cache_line( accdb, size_class, out_evicted_acc_idx );
2111 30 : fd_memcpy( line->key.pubkey, accmeta->key.pubkey, 32UL );
2112 30 : line->key.generation = accmeta->key.generation;
2113 : /* Leave acc_idx at UINT_MAX (the "loading" sentinel) until step 12
2114 : publishes it after the preadv2 fence. Concurrent threads that
2115 : pin via cache_idx will spin on this in step 13. */
2116 30 : line->acc_idx = UINT_MAX;
2117 30 : FD_COMPILER_MFENCE();
2118 30 : FD_VOLATILE( accmeta->cache_idx ) = FD_ACCDB_ACC_CIDX_PACK( (uint)size_class, (uint)cache_line_idx( accdb, size_class, line ) );
2119 30 : FD_COMPILER_MFENCE();
2120 :
2121 30 : fd_racesan_hook( "accdb_cold_load:pre_valid" );
2122 :
2123 : /* Atomically set CACHE_VALID_BIT and clear CACHE_CLAIM_BIT.
2124 : Eviction may have flipped CACHE_VALID_BIT on us between our
2125 : claim and now (it preserves CLAIM but can clear VALID); the
2126 : CAS loop tolerates that. The data length and exec bits stay
2127 : unchanged. */
2128 30 : for(;;) {
2129 30 : uint cur = FD_VOLATILE_CONST( accmeta->executable_size );
2130 30 : uint nxt = (cur & ~FD_ACCDB_SIZE_CACHE_CLAIM_BIT) | FD_ACCDB_SIZE_CACHE_VALID_BIT;
2131 30 : if( FD_LIKELY( FD_ATOMIC_CAS( &accmeta->executable_size, cur, nxt )==cur ) ) break;
2132 0 : FD_SPIN_PAUSE();
2133 0 : }
2134 :
2135 30 : *out_exists_in_cache = 0;
2136 30 : return line;
2137 30 : }
2138 30 : }
2139 :
2140 300756 : #define RESERVATION_TYPE_SIMPLE (0)
2141 351 : #define RESERVATION_TYPE_MAYBE_PROGRAMDATA (1)
2142 351 : #define RESERVATION_TYPE_ALREADY_RESERVED (2)
2143 :
2144 : static void
2145 : fd_accdb_acquire_inner( fd_accdb_t * accdb,
2146 : fd_accdb_fork_id_t fork_id,
2147 : int reservation_type,
2148 : ulong reserved_cnt,
2149 : ulong pubkeys_cnt,
2150 : uchar const * const * pubkeys,
2151 : int * writable,
2152 301458 : fd_acc_t * out_accs ) {
2153 301458 : accdb->metrics->acquire_calls++;
2154 :
2155 301458 : ulong max_acquire_cnt = accdb->shmem->bundle_enabled ? FD_ACCDB_MAX_ACQUIRE_CNT : FD_ACCDB_MAX_TX_ACCOUNT_LOCKS;
2156 301458 : FD_TEST( pubkeys_cnt<=max_acquire_cnt );
2157 :
2158 301458 : FD_TEST( FD_VOLATILE_CONST( *accdb->my_epoch_slot )==ULONG_MAX );
2159 :
2160 301458 : FD_COMPILER_MFENCE();
2161 301458 : FD_VOLATILE( *accdb->my_epoch_slot ) = FD_VOLATILE_CONST( accdb->shmem->epoch );
2162 301458 : FD_HW_MFENCE(); /* StoreLoad: epoch store must be globally visible
2163 : before any subsequent loads so the deferred
2164 : reclamation scan does not miss us */
2165 :
2166 : // STEP 1.
2167 : // Locate each account in the fork and index structure, to determine
2168 : // if it already exists, its size and other metadata, and which
2169 : // specific slot (generation) it was last written in.
2170 :
2171 301458 : fd_accdb_fork_t * fork = &accdb->fork_pool[ fork_id.val ];
2172 301458 : uint root_generation = accdb->fork_pool[ accdb->shmem->root_fork_id.val ].shmem->generation;
2173 :
2174 301458 : fd_racesan_hook( "accdb_acquire:post_root_gen" );
2175 :
2176 301458 : fd_accdb_accmeta_t * accmetas[ FD_ACCDB_MAX_ACQUIRE_CNT ];
2177 301458 : ulong acc_map_idxs[ FD_ACCDB_MAX_ACQUIRE_CNT ];
2178 :
2179 : /* Walk the hash chain for each pubkey and take the first visible
2180 : match. Correctness relies on newer entries always being prepended
2181 : to the chain head, which is guaranteed because replay processes
2182 : writes in slot order and release always inserts at the head.
2183 :
2184 : CONCURRENCY: This chain walk runs epoch-protected. A concurrent
2185 : fd_accdb_release may prepend a new node to the same chain while
2186 : we walk it. This is safe on x86-64 (TSO): the releasing thread
2187 : stores all acc fields (pubkey, generation, map.next, ...) before
2188 : publishing the new head via a CAS on acc_map[idx], and TSO
2189 : guarantees a reading core that observes the new head also observes
2190 : all prior stores to the node. A reader that does not yet see the
2191 : new head simply sees an older (still valid) version of the chain.
2192 : On weakly-ordered architectures an explicit acquire fence would be
2193 : needed before the chain walk and a release fence in
2194 : fd_accdb_release before the head-pointer store. Multiple
2195 : concurrent releases serialize on the CAS of the chain head. */
2196 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2197 302340 : acc_map_idxs[ i ] = fd_accdb_hash( pubkeys[ i ], accdb->shmem->seed )&(accdb->shmem->chain_cnt-1UL);
2198 302340 : uint acc = FD_VOLATILE_CONST( accdb->acc_map[ acc_map_idxs[ i ] ] );
2199 376794 : while( acc!=UINT_MAX ) {
2200 170208 : fd_accdb_accmeta_t const * candidate_acc = &accdb->acc_pool[ acc ];
2201 170208 : uint next_acc = FD_VOLATILE_CONST( candidate_acc->map.next );
2202 :
2203 170208 : fd_racesan_hook( "accdb_acquire:post_next" );
2204 :
2205 170208 : if( FD_UNLIKELY( (candidate_acc->key.generation>root_generation &&
2206 170208 : fd_accdb_acc_fork_id(candidate_acc)!=fork_id.val &&
2207 170208 : !descends_set_test( fork->descends, fd_accdb_acc_fork_id(candidate_acc) )) ) ||
2208 170208 : memcmp( pubkeys[ i ], candidate_acc->key.pubkey, 32UL ) ) {
2209 74454 : acc = next_acc;
2210 74454 : continue;
2211 74454 : }
2212 :
2213 95754 : break;
2214 170208 : }
2215 302340 : if( FD_UNLIKELY( acc==UINT_MAX ) ) accmetas[ i ] = NULL;
2216 95754 : else accmetas[ i ] = &accdb->acc_pool[ acc ];
2217 :
2218 : #if FD_TMPL_USE_HANDHOLDING
2219 : if( FD_UNLIKELY( accmetas[ i ] ) ) {
2220 : fd_accdb_accmeta_t const * sel = accmetas[ i ];
2221 : FD_TEST( !memcmp( sel->key.pubkey, pubkeys[ i ], 32UL ) );
2222 : FD_TEST( sel->key.generation<=root_generation ||
2223 : fd_accdb_acc_fork_id( sel )==fork_id.val ||
2224 : descends_set_test( fork->descends, fd_accdb_acc_fork_id( sel ) ) );
2225 : FD_TEST( sel->key.generation<=FD_VOLATILE_CONST( accdb->shmem->generation ) );
2226 : }
2227 : #endif
2228 :
2229 302340 : if( FD_UNLIKELY( accmetas[ i ] && !writable[ i ] && !accmetas[ i ]->lamports ) ) accmetas[ i ] = NULL;
2230 :
2231 : /* Attribute this acquired account to a size class for per-class
2232 : rate metrics. Use the account's current size class when known;
2233 : otherwise (new account) bucket as class 0. */
2234 302340 : ulong acq_class = 0UL;
2235 302340 : if( FD_LIKELY( accmetas[ i ] ) ) acq_class = fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size ) );
2236 302340 : if( FD_LIKELY( writable[ i ] ) ) accdb->metrics->writable_accounts_acquired_per_class[ acq_class ]++;
2237 187539 : else accdb->metrics->accounts_acquired_per_class[ acq_class ]++;
2238 302340 : }
2239 :
2240 : // STEP 2.
2241 : // The two-phase programdata acquire (acquire_a then acquire_b)
2242 : // works as follows: acquire_a (RESERVATION_TYPE_MAYBE_PROGRAMDATA)
2243 : // over-reserves one slot in every live size class per candidate
2244 : // account (reserved_cnt total per class), because it does not yet
2245 : // know which accounts have programdata or what size class it lands
2246 : // in. acquire_b then resolves the actual programdata pubkeys and
2247 : // re-enters here with RESERVATION_TYPE_ALREADY_RESERVED to refund
2248 : // the surplus. Keep one reservation per found programdata account
2249 : // in its own size class (consumed later by release) and give the
2250 : // rest back.
2251 301458 : if( FD_UNLIKELY( reservation_type==RESERVATION_TYPE_ALREADY_RESERVED ) ) {
2252 351 : ulong refund[ FD_ACCDB_CACHE_CLASS_CNT ] = {0};
2253 3159 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) {
2254 2808 : if( FD_LIKELY( accdb->shmem->cache_class_used[ j ].val!=ULONG_MAX ) ) refund[ j ] = reserved_cnt;
2255 2808 : }
2256 390 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2257 39 : if( FD_LIKELY( accmetas[ i ] ) ) {
2258 36 : ulong cls = fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size ) );
2259 36 : if( FD_LIKELY( accdb->shmem->cache_class_used[ cls ].val!=ULONG_MAX ) ) {
2260 3 : FD_TEST( refund[ cls ]>0UL );
2261 3 : refund[ cls ]--;
2262 3 : }
2263 36 : }
2264 39 : }
2265 3159 : for( ulong k=0UL; k<FD_ACCDB_CACHE_CLASS_CNT; k++ ) {
2266 2808 : if( FD_UNLIKELY( refund[ k ] ) ) FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->cache_class_used[ k ].val, refund[ k ] );
2267 2808 : }
2268 351 : }
2269 :
2270 : // STEP 3.
2271 : // We are potentially going to need to read the account data off of
2272 : // disk into the cache, if the account(s) are not in the cache so
2273 : // reserve the necessary cache space. This is done with an "atomic
2274 : // subtract" spin loop on the cache class counters, which is
2275 : // actually faster than doing a real CAS on a packed ulong.
2276 : //
2277 : // For reads, we only need space to copy the account data into a
2278 : // single right-sized cache line, but for writes ... we need to
2279 : // reserve one of every size class. The reason is we are going to
2280 : // need a 10MiB staging buffer for the executor to write to (it may
2281 : // grow the account, so needs the max size class). Even if the
2282 : // account is already in the 10MiB cache class, we need another one
2283 : // because a transaction can fail half way, so we need scratch space
2284 : // to be able to unwind.
2285 : //
2286 : // So we acquire one of each size class. Then when the transaction
2287 : // finishes, if it succeeded, we will copy the data back to the
2288 : // whichever size-class is now right-sized post execution.
2289 301458 : if( FD_LIKELY( reservation_type==RESERVATION_TYPE_SIMPLE || reservation_type==RESERVATION_TYPE_MAYBE_PROGRAMDATA ) ) {
2290 301107 : ulong requested_buckets[ FD_ACCDB_CACHE_CLASS_CNT ] = {0};
2291 603408 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2292 302301 : if( FD_LIKELY( accmetas[ i ] || writable[ i ] ) ) {
2293 195873 : if( FD_LIKELY( accmetas[ i ] ) ) {
2294 95718 : if( FD_UNLIKELY( accdb->shmem->cache_class_used[ fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size ) ) ].val!=ULONG_MAX ) ) {
2295 0 : requested_buckets[ fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size ) ) ]++;
2296 0 : }
2297 95718 : }
2298 195873 : if( FD_UNLIKELY( writable[ i ] ) ) {
2299 1033209 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) {
2300 918408 : if( FD_UNLIKELY( accdb->shmem->cache_class_used[ j ].val!=ULONG_MAX ) ) {
2301 54 : requested_buckets[ j ]++;
2302 54 : }
2303 918408 : }
2304 114801 : }
2305 195873 : }
2306 :
2307 302301 : if( FD_LIKELY( reservation_type==RESERVATION_TYPE_MAYBE_PROGRAMDATA ) ) {
2308 : /* Any account could also have an implied reference to a
2309 : programdata account, which we don't know yet ... so we need to
2310 : reserve worst case space if they all went to the same size
2311 : class. This reservation runs unconditionally per pubkey (not
2312 : gated on accmetas/writable) so that acquire_b can refund based on
2313 : pubkeys_cnt without needing to re-derive the live-account set. */
2314 13284 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) {
2315 11808 : if( FD_UNLIKELY( accdb->shmem->cache_class_used[ j ].val!=ULONG_MAX ) ) {
2316 36 : requested_buckets[ j ]++;
2317 36 : }
2318 11808 : }
2319 1476 : }
2320 302301 : }
2321 :
2322 : /* TODO: This over-reserves cache slots for writable accounts that
2323 : already exist. For each such account we reserve one line in the
2324 : account's size class (for the read into cache) AND one line in
2325 : every size class (for the write destination buffers). But if the
2326 : account is already resident in cache (which is the common case
2327 : for hot accounts), the read-into-cache line is unnecessary — we
2328 : will get a cache hit in step 4 and never use it. The fix is to
2329 : probe acc->cache_idx here and skip the per-account size class
2330 : reservation per-account size class reservation when a hit is
2331 : found. This would reduce peak reservation by up to one line per
2332 : writable account per acquire batch, lowering contention on the
2333 : cache class counters and allowing smaller cache provisioning. */
2334 :
2335 : /* Reserve cache slots by atomically incrementing the shared used
2336 : counters. If any class exceeds its max, the reservation
2337 : overflowed — subtract back partial grabs and retry. */
2338 301107 : for(;;) {
2339 301107 : int acquire_failed = 0;
2340 301107 : ulong grabbed[ FD_ACCDB_CACHE_CLASS_CNT ] = {0};
2341 2709963 : for( ulong i=0UL; i<FD_ACCDB_CACHE_CLASS_CNT; i++ ) {
2342 2408856 : if( FD_LIKELY( !requested_buckets[ i ] ) ) continue;
2343 72 : ulong new_used = FD_ATOMIC_ADD_AND_FETCH( &accdb->shmem->cache_class_used[ i ].val, requested_buckets[ i ] );
2344 72 : if( FD_UNLIKELY( new_used>accdb->shmem->cache_class_max[ i ] ) ) {
2345 0 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->cache_class_used[ i ].val, requested_buckets[ i ] );
2346 0 : acquire_failed = 1;
2347 72 : } else {
2348 72 : grabbed[ i ] = requested_buckets[ i ];
2349 72 : }
2350 72 : if( FD_UNLIKELY( acquire_failed ) ) {
2351 0 : accdb->metrics->acquire_failed++;
2352 0 : for( ulong j=0UL; j<i; j++ ) {
2353 0 : if( grabbed[ j ] ) FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->cache_class_used[ j ].val, grabbed[ j ] );
2354 0 : }
2355 0 : FD_SPIN_PAUSE();
2356 0 : break;
2357 0 : }
2358 72 : }
2359 301107 : if( FD_LIKELY( !acquire_failed ) ) break;
2360 301107 : }
2361 301107 : }
2362 :
2363 : // STEP 4.
2364 : // For any accounts that are not in cache, we now need to actually
2365 : // retrieve the cache pointers from our structures. Space has been
2366 : // reserved already, so this step is guaranteed to succeed, and is
2367 : // just pulling the cache lines out of the free lists and marking
2368 : // them as in-use.
2369 : //
2370 : // This step is fully lock-free. Cache hits are pinned with an
2371 : // atomic CAS on refcnt (cache_try_pin). Eviction uses the CLOCK
2372 : // algorithm. The CAS free list provides immediate recycling of
2373 : // fully-freed lines.
2374 :
2375 301458 : int exists_in_cache[ FD_ACCDB_MAX_ACQUIRE_CNT ];
2376 301458 : fd_accdb_cache_line_t * original_cache_line[ FD_ACCDB_MAX_ACQUIRE_CNT ];
2377 301458 : fd_accdb_cache_line_t * destination_cache_lines[ FD_ACCDB_MAX_ACQUIRE_CNT ][ FD_ACCDB_CACHE_CLASS_CNT ];
2378 :
2379 : /* Saved acc_pool indices of evicted dirty cache lines. These are
2380 : captured before clearing acc_idx to UINT_MAX on the line struct, so
2381 : that the sentinel protocol (step 14) works correctly while the
2382 : evicted account metadata is still available for writeback in steps
2383 : 4 and 6. */
2384 301458 : uint evicted_dest_acc[ FD_ACCDB_MAX_ACQUIRE_CNT ][ FD_ACCDB_CACHE_CLASS_CNT ];
2385 301458 : uint evicted_orig_acc[ FD_ACCDB_MAX_ACQUIRE_CNT ];
2386 :
2387 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2388 302340 : if( FD_UNLIKELY( !accmetas[ i ] && !writable[ i ] ) ) continue;
2389 :
2390 195909 : original_cache_line[ i ] = NULL;
2391 195909 : if( FD_LIKELY( accmetas[ i ] ) ) {
2392 95754 : if( FD_LIKELY( FD_ACCDB_SIZE_CACHE_VALID( FD_VOLATILE_CONST( accmetas[ i ]->executable_size ) ) ) ) {
2393 : /* Concurrent evict_clear_acc_cache_ref clears VALID then stores
2394 : cache_idx=INVAL. We may have observed VALID=1 just before the
2395 : writer cleared it, so cidx can read as INVAL here; decoding it
2396 : would yield a wild cache_line pointer. Skip on INVAL. Any
2397 : other stale cidx is harmless: cache_try_pin's ABA generation
2398 : check rejects a recycled line. */
2399 95724 : uint cidx = FD_VOLATILE_CONST( accmetas[ i ]->cache_idx );
2400 95724 : if( FD_LIKELY( cidx!=FD_ACCDB_ACC_CIDX_INVAL ) ) {
2401 95724 : fd_accdb_cache_line_t * hit = cache_line( accdb, FD_ACCDB_ACC_CIDX_CLASS( cidx ), FD_ACCDB_ACC_CIDX_IDX( cidx ) );
2402 95724 : fd_racesan_hook( "accdb_acquire:pre_try_pin" );
2403 95724 : original_cache_line[ i ] = cache_try_pin( hit, pubkeys[ i ], accmetas[ i ]->key.generation );
2404 : #if FD_TMPL_USE_HANDHOLDING
2405 : if( FD_LIKELY( original_cache_line[ i ] ) ) {
2406 : FD_TEST( original_cache_line[ i ]->key.generation==accmetas[ i ]->key.generation &&
2407 : !memcmp( original_cache_line[ i ]->key.pubkey, pubkeys[ i ], 32UL ) );
2408 : uint rc = FD_VOLATILE_CONST( original_cache_line[ i ]->refcnt );
2409 : FD_TEST( rc>0U && rc!=FD_ACCDB_EVICT_SENTINEL );
2410 : }
2411 : #endif
2412 95724 : }
2413 95724 : }
2414 95754 : }
2415 195909 : exists_in_cache[ i ] = original_cache_line[ i ]!=NULL;
2416 :
2417 195909 : if( FD_UNLIKELY( writable[ i ] ) ) {
2418 1033209 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) destination_cache_lines[ i ][ j ] = acquire_cache_line( accdb, j, &evicted_dest_acc[ i ][ j ] );
2419 114801 : if( FD_UNLIKELY( accmetas[ i ] && !original_cache_line[ i ] ) ) {
2420 0 : original_cache_line[ i ] = cold_load_acc( accdb, accmetas[ i ], pubkeys[ i ], &exists_in_cache[ i ], &evicted_orig_acc[ i ] );
2421 0 : }
2422 114801 : } else {
2423 81108 : if( FD_UNLIKELY( !original_cache_line[ i ] ) ) {
2424 30 : original_cache_line[ i ] = cold_load_acc( accdb, accmetas[ i ], pubkeys[ i ], &exists_in_cache[ i ], &evicted_orig_acc[ i ] );
2425 30 : }
2426 81108 : }
2427 195909 : }
2428 :
2429 : // STEP 5.
2430 : // For any cache lines we have retrieved, which we might potentially
2431 : // be about to trash (by writing stuff in there), we need to write
2432 : // them back to disk first if they are dirty. This is the process of
2433 : // "persisting" (a/k/a evicting) whatever was previously in the
2434 : // cache line we are about to use.
2435 : //
2436 : // This step does not actually persist the data to disk, it just
2437 : // constructs a series of iovecs (write instructions) which will be
2438 : // used later to do the actual write. The reason is that we want to
2439 : // batch all the writes together into a single writev call, to
2440 : // minimize overhead, and also keep the actual writes at the end of
2441 : // the function and independent of the specific control flow, so
2442 : // that they could be offloaded to another thread of made
2443 : // asynchronous (e.g. with io_uring) in the future without needing
2444 : // to change the rest of the logic.
2445 :
2446 301458 : int write_ops_cnt = 0;
2447 301458 : int write_meta_cnt = 0;
2448 301458 : ulong total_write_sz = 0UL;
2449 301458 : fd_accdb_disk_meta_t write_metas[ (FD_ACCDB_CACHE_CLASS_CNT+1UL)*FD_ACCDB_MAX_ACQUIRE_CNT ];
2450 301458 : struct iovec write_ops[ 2UL*(FD_ACCDB_CACHE_CLASS_CNT+1UL)*FD_ACCDB_MAX_ACQUIRE_CNT ];
2451 :
2452 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2453 302340 : if( FD_UNLIKELY( !accmetas[ i ] && !writable[ i ] ) ) continue;
2454 :
2455 195909 : if( FD_UNLIKELY( writable[ i ] ) ) {
2456 1033209 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) {
2457 918408 : if( FD_LIKELY( evicted_dest_acc[ i ][ j ]==UINT_MAX ) ) continue;
2458 0 : accdb->metrics->accounts_evicted++;
2459 0 : accdb->metrics->accounts_evicted_per_class[ j ]++;
2460 :
2461 0 : fd_accdb_accmeta_t const * evicted = &accdb->acc_pool[ evicted_dest_acc[ i ][ j ] ];
2462 0 : fd_racesan_hook( "writeback:pre_synth" );
2463 0 : total_write_sz += sizeof(fd_accdb_disk_meta_t) + FD_ACCDB_SIZE_DATA( evicted->executable_size );
2464 0 : FD_TEST( write_meta_cnt<(int)(sizeof(write_metas)/sizeof(write_metas[0])) );
2465 0 : fd_memcpy( write_metas[ write_meta_cnt ].pubkey, evicted->key.pubkey, 32UL );
2466 0 : write_metas[ write_meta_cnt ].size = FD_ACCDB_SIZE_DATA( evicted->executable_size );
2467 0 : fd_memcpy( write_metas[ write_meta_cnt ].owner, destination_cache_lines[ i ][ j ]->owner, 32UL );
2468 0 : write_ops[ write_ops_cnt++ ] = (struct iovec){ .iov_base = &write_metas[ write_meta_cnt ], .iov_len = sizeof(fd_accdb_disk_meta_t) };
2469 0 : write_meta_cnt++;
2470 0 : write_ops[ write_ops_cnt++ ] = (struct iovec){ .iov_base = destination_cache_lines[ i ][ j ]+1UL, .iov_len = FD_ACCDB_SIZE_DATA( evicted->executable_size ) };
2471 0 : }
2472 114801 : if( FD_UNLIKELY( accmetas[ i ] && !exists_in_cache[ i ] && evicted_orig_acc[ i ]!=UINT_MAX ) ) {
2473 0 : fd_accdb_accmeta_t const * evicted = &accdb->acc_pool[ evicted_orig_acc[ i ] ];
2474 0 : accdb->metrics->accounts_evicted++;
2475 0 : accdb->metrics->accounts_evicted_per_class[ fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( evicted->executable_size ) ) ]++;
2476 :
2477 0 : total_write_sz += sizeof(fd_accdb_disk_meta_t) + FD_ACCDB_SIZE_DATA( evicted->executable_size );
2478 0 : FD_TEST( write_meta_cnt<(int)(sizeof(write_metas)/sizeof(write_metas[0])) );
2479 0 : fd_memcpy( write_metas[ write_meta_cnt ].pubkey, evicted->key.pubkey, 32UL );
2480 0 : write_metas[ write_meta_cnt ].size = FD_ACCDB_SIZE_DATA( evicted->executable_size );
2481 0 : fd_memcpy( write_metas[ write_meta_cnt ].owner, original_cache_line[ i ]->owner, 32UL );
2482 0 : write_ops[ write_ops_cnt++ ] = (struct iovec){ .iov_base = &write_metas[ write_meta_cnt ], .iov_len = sizeof(fd_accdb_disk_meta_t) };
2483 0 : write_meta_cnt++;
2484 0 : write_ops[ write_ops_cnt++ ] = (struct iovec){ .iov_base = original_cache_line[ i ]+1UL, .iov_len = FD_ACCDB_SIZE_DATA( evicted->executable_size ) };
2485 0 : }
2486 114801 : } else {
2487 81108 : if( FD_LIKELY( exists_in_cache[ i ] || evicted_orig_acc[ i ]==UINT_MAX ) ) continue;
2488 0 : fd_accdb_accmeta_t const * evicted = &accdb->acc_pool[ evicted_orig_acc[ i ] ];
2489 0 : accdb->metrics->accounts_evicted++;
2490 0 : accdb->metrics->accounts_evicted_per_class[ fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( evicted->executable_size ) ) ]++;
2491 0 : total_write_sz += sizeof(fd_accdb_disk_meta_t) + FD_ACCDB_SIZE_DATA( evicted->executable_size );
2492 0 : FD_TEST( write_meta_cnt<(int)(sizeof(write_metas)/sizeof(write_metas[0])) );
2493 0 : fd_memcpy( write_metas[ write_meta_cnt ].pubkey, evicted->key.pubkey, 32UL );
2494 0 : write_metas[ write_meta_cnt ].size = FD_ACCDB_SIZE_DATA( evicted->executable_size );
2495 0 : fd_memcpy( write_metas[ write_meta_cnt ].owner, original_cache_line[ i ]->owner, 32UL );
2496 0 : write_ops[ write_ops_cnt++ ] = (struct iovec){ .iov_base = &write_metas[ write_meta_cnt ], .iov_len = sizeof(fd_accdb_disk_meta_t) };
2497 0 : write_meta_cnt++;
2498 0 : write_ops[ write_ops_cnt++ ] = (struct iovec){ .iov_base = original_cache_line[ i ]+1UL, .iov_len = FD_ACCDB_SIZE_DATA( evicted->executable_size ) };
2499 0 : }
2500 195909 : }
2501 :
2502 : // STEP 6-7.
2503 : // Compute the file offset for the writes we are about to do and
2504 : // build the pending offset table. The common case is a single
2505 : // atomic fetch-add on the write head, reserving a contiguous
2506 : // region. If the total eviction batch is too large to fit in one
2507 : // partition (extremely unlikely — requires many dirty 10MiB
2508 : // evictions), fall back to per-entry allocation so that each
2509 : // individual write fits in a single partition.
2510 : //
2511 : // The actual stores to evicted->offset_fork and line->persisted
2512 : // are deferred until after pwritev2 completes (Step 9-10), so
2513 : // a concurrent acquire spinning on offset==FD_ACCDB_OFF_INVAL
2514 : // does not proceed to preadv2 from a location that hasn't been
2515 : // written.
2516 301458 : int pending_cnt = 0;
2517 301458 : fd_accdb_accmeta_t * pending_accs [ (FD_ACCDB_CACHE_CLASS_CNT+1UL)*FD_ACCDB_MAX_ACQUIRE_CNT ];
2518 301458 : ulong pending_offs [ (FD_ACCDB_CACHE_CLASS_CNT+1UL)*FD_ACCDB_MAX_ACQUIRE_CNT ];
2519 301458 : fd_accdb_cache_line_t * pending_lines[ (FD_ACCDB_CACHE_CLASS_CNT+1UL)*FD_ACCDB_MAX_ACQUIRE_CNT ];
2520 :
2521 301458 : ulong file_offset;
2522 301458 : int batch_contiguous;
2523 301458 : if( FD_LIKELY( total_write_sz && total_write_sz<=accdb->shmem->partition_sz ) ) {
2524 0 : file_offset = allocate_next_write( accdb, total_write_sz );
2525 0 : batch_contiguous = 1;
2526 301458 : } else {
2527 301458 : file_offset = 0UL;
2528 301458 : batch_contiguous = 0;
2529 301458 : }
2530 :
2531 301458 : ulong cumulative_offset = 0UL;
2532 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2533 302340 : if( FD_UNLIKELY( !accmetas[ i ] && !writable[ i ] ) ) continue;
2534 :
2535 195909 : if( FD_UNLIKELY( writable[ i ] ) ) {
2536 1033209 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) {
2537 918408 : if( FD_LIKELY( evicted_dest_acc[ i ][ j ]==UINT_MAX ) ) continue;
2538 :
2539 0 : fd_accdb_accmeta_t * evicted = &accdb->acc_pool[ evicted_dest_acc[ i ][ j ] ];
2540 0 : ulong entry_sz = sizeof(fd_accdb_disk_meta_t) + (ulong)FD_ACCDB_SIZE_DATA( evicted->executable_size );
2541 : /* xchg-to-INVAL atomically captures the old offset and prevents
2542 : a concurrent acc_unlink from also reading and freeing it (the
2543 : xchg there will see INVAL and skip). Step 10 republishes the
2544 : new offset; the spinner at line ~2082 tolerates the transient
2545 : INVAL. Same pattern as the overwrite path at line ~2388. */
2546 0 : ulong old_off = fd_accdb_acc_xchg_offset( evicted, FD_ACCDB_OFF_INVAL );
2547 0 : if( FD_LIKELY( old_off!=FD_ACCDB_OFF_INVAL ) ) {
2548 0 : fd_accdb_shmem_bytes_freed( accdb->shmem, old_off, entry_sz );
2549 0 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->shmetrics->disk_used_bytes, entry_sz );
2550 0 : }
2551 0 : FD_TEST( pending_cnt<(int)(sizeof(pending_accs)/sizeof(pending_accs[0])) );
2552 0 : pending_accs [ pending_cnt ] = evicted;
2553 0 : if( FD_LIKELY( batch_contiguous ) ) pending_offs[ pending_cnt ] = file_offset + cumulative_offset;
2554 0 : else pending_offs[ pending_cnt ] = allocate_next_write( accdb, entry_sz );
2555 0 : pending_lines[ pending_cnt ] = destination_cache_lines[ i ][ j ];
2556 0 : pending_cnt++;
2557 0 : cumulative_offset += entry_sz;
2558 0 : FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->shmetrics->disk_used_bytes, entry_sz );
2559 0 : }
2560 114801 : if( FD_UNLIKELY( accmetas[ i ] && !exists_in_cache[ i ] && evicted_orig_acc[ i ]!=UINT_MAX ) ) {
2561 0 : fd_accdb_accmeta_t * evicted = &accdb->acc_pool[ evicted_orig_acc[ i ] ];
2562 0 : ulong entry_sz = sizeof(fd_accdb_disk_meta_t) + (ulong)FD_ACCDB_SIZE_DATA( evicted->executable_size );
2563 0 : ulong old_off = fd_accdb_acc_xchg_offset( evicted, FD_ACCDB_OFF_INVAL );
2564 0 : if( FD_LIKELY( old_off!=FD_ACCDB_OFF_INVAL ) ) {
2565 0 : fd_accdb_shmem_bytes_freed( accdb->shmem, old_off, entry_sz );
2566 0 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->shmetrics->disk_used_bytes, entry_sz );
2567 0 : }
2568 0 : FD_TEST( pending_cnt<(int)(sizeof(pending_accs)/sizeof(pending_accs[0])) );
2569 0 : pending_accs [ pending_cnt ] = evicted;
2570 0 : if( FD_LIKELY( batch_contiguous ) ) pending_offs[ pending_cnt ] = file_offset + cumulative_offset;
2571 0 : else pending_offs[ pending_cnt ] = allocate_next_write( accdb, entry_sz );
2572 0 : pending_lines[ pending_cnt ] = original_cache_line[ i ];
2573 0 : pending_cnt++;
2574 0 : cumulative_offset += entry_sz;
2575 0 : FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->shmetrics->disk_used_bytes, entry_sz );
2576 0 : }
2577 114801 : } else {
2578 81108 : if( FD_LIKELY( exists_in_cache[ i ] || evicted_orig_acc[ i ]==UINT_MAX ) ) continue;
2579 :
2580 0 : fd_accdb_accmeta_t * evicted = &accdb->acc_pool[ evicted_orig_acc[ i ] ];
2581 0 : ulong entry_sz = sizeof(fd_accdb_disk_meta_t) + (ulong)FD_ACCDB_SIZE_DATA( evicted->executable_size );
2582 0 : ulong old_off = fd_accdb_acc_xchg_offset( evicted, FD_ACCDB_OFF_INVAL );
2583 0 : if( FD_LIKELY( old_off!=FD_ACCDB_OFF_INVAL ) ) {
2584 0 : fd_accdb_shmem_bytes_freed( accdb->shmem, old_off, entry_sz );
2585 0 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->shmetrics->disk_used_bytes, entry_sz );
2586 0 : }
2587 0 : FD_TEST( pending_cnt<(int)(sizeof(pending_accs)/sizeof(pending_accs[0])) );
2588 0 : pending_accs [ pending_cnt ] = evicted;
2589 0 : if( FD_LIKELY( batch_contiguous ) ) pending_offs[ pending_cnt ] = file_offset + cumulative_offset;
2590 0 : else pending_offs[ pending_cnt ] = allocate_next_write( accdb, entry_sz );
2591 0 : pending_lines[ pending_cnt ] = original_cache_line[ i ];
2592 0 : pending_cnt++;
2593 0 : cumulative_offset += entry_sz;
2594 0 : FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->shmetrics->disk_used_bytes, entry_sz );
2595 0 : }
2596 195909 : }
2597 :
2598 : // STEP 8.
2599 : // Fill the output entries with cache pointers and metadata based on
2600 : // the accounts we have located and the cache lines we have
2601 : // reserved.
2602 :
2603 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2604 302340 : if( FD_UNLIKELY( !accmetas[ i ] && !writable[ i ] ) ) {
2605 106431 : out_accs[ i ].data = NULL;
2606 106431 : out_accs[ i ].data_len = 0UL;
2607 106431 : out_accs[ i ].lamports = 0UL;
2608 106431 : out_accs[ i ].executable = 0;
2609 106431 : memset( out_accs[ i ].owner, 0, 32UL );
2610 106431 : fd_memcpy( out_accs[ i ].pubkey, pubkeys[ i ], 32UL );
2611 106431 : out_accs[ i ].prior_lamports = 0UL;
2612 106431 : out_accs[ i ].prior_data_len = 0UL;
2613 106431 : out_accs[ i ].prior_executable = 0;
2614 106431 : memset( out_accs[ i ].prior_owner, 0, 32UL );
2615 106431 : out_accs[ i ].prior_data = NULL;
2616 106431 : out_accs[ i ].commit = 0;
2617 106431 : out_accs[ i ].pd_write = 0;
2618 106431 : out_accs[ i ]._writable = 0;
2619 106431 : out_accs[ i ]._original_size_class = ULONG_MAX;
2620 106431 : out_accs[ i ]._original_cache_idx = ULONG_MAX;
2621 106431 : continue;
2622 106431 : }
2623 :
2624 195909 : if( FD_LIKELY( !writable[ i ] ) ) out_accs[ i ].data = (uchar *)(original_cache_line[ i ]+1UL);
2625 114801 : else out_accs[ i ].data = (uchar *)(destination_cache_lines[ i ][ 7UL ]+1UL);
2626 : /* Tombstone reset: agave's account loader returns AccountSharedData::default()
2627 : (System owner, empty data, exec=0) for any account with lamports==0.
2628 : https://github.com/anza-xyz/agave/blob/v2.3.1/svm/src/account_loader.rs#L199-L228 */
2629 195909 : fd_racesan_hook( "accdb_acquire:pre_step7_meta" );
2630 195909 : int tombstone = accmetas[ i ] && accmetas[ i ]->lamports==0UL;
2631 195909 : out_accs[ i ].data_len = ( accmetas[ i ] && !tombstone ) ? FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size ) : 0UL;
2632 195909 : out_accs[ i ].executable = ( accmetas[ i ] && !tombstone ) ? FD_ACCDB_SIZE_EXEC( accmetas[ i ]->executable_size ) : 0;
2633 195909 : fd_racesan_hook( "accdb_acquire:mid_step7_meta" );
2634 195909 : out_accs[ i ].lamports = accmetas[ i ] ? accmetas[ i ]->lamports : 0UL;
2635 195909 : if( FD_UNLIKELY( !accmetas[ i ] ) ) memset( out_accs[ i ].owner, 0, 32UL );
2636 : /* For accmetas[i] != NULL, the owner is copied from the cache line
2637 : below in step 15, after step 12 has populated it from disk for
2638 : cold loads. */
2639 :
2640 195909 : out_accs[ i ].prior_lamports = out_accs[ i ].lamports;
2641 195909 : out_accs[ i ].prior_data_len = out_accs[ i ].data_len;
2642 195909 : out_accs[ i ].prior_executable = out_accs[ i ].executable;
2643 195909 : out_accs[ i ].prior_data = (uchar *)(original_cache_line[ i ] ? (original_cache_line[ i ]+1UL) : NULL);
2644 :
2645 195909 : out_accs[ i ].commit = 0;
2646 195909 : out_accs[ i ].pd_write = 0;
2647 195909 : out_accs[ i ]._writable = writable[ i ];
2648 195909 : if( FD_UNLIKELY( writable[ i ] && accmetas[ i ] ) ) out_accs[ i ]._overwrite = accdb->fork_pool[ fork_id.val ].shmem->generation==accmetas[ i ]->key.generation;
2649 181263 : else out_accs[ i ]._overwrite = 0;
2650 :
2651 195909 : FD_TEST( out_accs[ i ].data_len<=(10UL<<20) );
2652 195909 : FD_TEST( !out_accs[ i ]._overwrite || accdb->fork_pool[ fork_id.val ].shmem->generation==accmetas[ i ]->key.generation );
2653 :
2654 : #if FD_TMPL_USE_HANDHOLDING
2655 : if( FD_UNLIKELY( !writable[ i ] && accmetas[ i ] && !tombstone ) ) {
2656 : ulong cls = fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size ) );
2657 : FD_TEST( fd_accdb_ptr_in_region( accdb, cls, out_accs[ i ].data ) );
2658 : }
2659 : #endif
2660 :
2661 195909 : if( FD_UNLIKELY( writable[ i ] ) ) {
2662 114801 : out_accs[ i ]._fork_id = fork_id.val;
2663 114801 : out_accs[ i ]._generation = fork->shmem->generation;
2664 114801 : out_accs[ i ]._acc_map_idx = acc_map_idxs[ i ];
2665 114801 : }
2666 195909 : fd_memcpy( out_accs[ i ].pubkey, pubkeys[ i ], 32UL );
2667 :
2668 195909 : if( FD_UNLIKELY( !accmetas[ i ] ) ) {
2669 100155 : out_accs[ i ]._original_size_class = ULONG_MAX;
2670 100155 : out_accs[ i ]._original_cache_idx = ULONG_MAX;
2671 100155 : } else {
2672 95754 : out_accs[ i ]._original_size_class = fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size ) );
2673 95754 : out_accs[ i ]._original_cache_idx = cache_line_idx( accdb, out_accs[ i ]._original_size_class, original_cache_line[ i ] );
2674 95754 : }
2675 :
2676 195909 : if( FD_UNLIKELY( writable[ i ] ) ) {
2677 1033209 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) {
2678 918408 : out_accs[ i ]._write.destination_cache_idx[ j ] = cache_line_idx( accdb, j, destination_cache_lines[ i ][ j ] );
2679 918408 : }
2680 114801 : }
2681 195909 : }
2682 :
2683 : // STEP 9.
2684 : // Write the dirty eviction data to disk and publish the new offsets
2685 : // BEFORE constructing read iovecs. This is critical: step 4 may
2686 : // have evicted a dirty cache line belonging to another account in
2687 : // the same batch whose acc->offset is still FD_ACCDB_OFF_INVAL.
2688 : // The read-iovec loop below spin-waits on
2689 : // offset!=FD_ACCDB_OFF_INVAL, so publishing evicted offsets first
2690 : // prevents an intra-batch deadlock where the thread waits on an
2691 : // offset that only it can resolve.
2692 301458 : if( FD_LIKELY( batch_contiguous ) ) {
2693 : /* Fast path: all evictions fit in one contiguous region. Use the
2694 : pre-built iovec array for a single batched pwritev2 call. */
2695 0 : ulong bytes_written = 0UL;
2696 0 : struct iovec * write_ptr = write_ops;
2697 0 : while( FD_LIKELY( bytes_written<total_write_sz ) ) {
2698 0 : long result = pwritev2( accdb->fd, write_ptr, fd_int_min( write_ops_cnt, IOV_MAX ), (long)(file_offset+bytes_written), 0 );
2699 0 : if( FD_UNLIKELY( -1==result && (errno==EINTR || errno==EAGAIN || errno==EWOULDBLOCK ) ) ) continue;
2700 0 : else if( FD_UNLIKELY( -1==result ) ) FD_LOG_ERR(( "pwritev2() failed (%d-%s)", errno, fd_io_strerror( errno ) ));
2701 0 : else if( FD_UNLIKELY( !result ) ) FD_LOG_ERR(( "accounts database is corrupt, pwritev2() returned 0 at offset %lu with %lu bytes remaining",
2702 0 : file_offset+bytes_written, total_write_sz-bytes_written ));
2703 0 : bytes_written += (ulong)result;
2704 0 : accdb->metrics->bytes_written += (ulong)result;
2705 0 : accdb->metrics->write_ops++;
2706 :
2707 0 : while( write_ops_cnt && (ulong)result>=(ulong)write_ptr[ 0 ].iov_len ) {
2708 0 : result -= (long)write_ptr[ 0 ].iov_len;
2709 0 : write_ptr++;
2710 0 : write_ops_cnt--;
2711 0 : }
2712 0 : if( FD_LIKELY( write_ops_cnt ) ) {
2713 0 : write_ptr[ 0 ].iov_base = (uchar *)write_ptr[ 0 ].iov_base + result;
2714 0 : write_ptr[ 0 ].iov_len -= (ulong)result;
2715 0 : }
2716 0 : }
2717 301458 : } else {
2718 : /* Slow path: total eviction batch exceeds a single partition.
2719 : Write each entry individually using its own allocated offset.
2720 : This path is only taken in extreme edge cases (many concurrent
2721 : dirty 10 MiB evictions). */
2722 301458 : struct iovec * wp = write_ops;
2723 301458 : for( int k=0; k<pending_cnt; k++ ) {
2724 0 : ulong entry_sz = sizeof(fd_accdb_disk_meta_t) + (ulong)FD_ACCDB_SIZE_DATA( pending_accs[ k ]->executable_size );
2725 0 : ulong entry_off = pending_offs[ k ];
2726 0 : struct iovec entry_iovs[2] = { wp[0], wp[1] };
2727 0 : wp += 2;
2728 :
2729 0 : ulong written = 0UL;
2730 0 : while( FD_LIKELY( written<entry_sz ) ) {
2731 0 : long result = pwritev2( accdb->fd, entry_iovs, 2, (long)(entry_off+written), 0 );
2732 0 : if( FD_UNLIKELY( -1==result && (errno==EINTR || errno==EAGAIN || errno==EWOULDBLOCK ) ) ) continue;
2733 0 : else if( FD_UNLIKELY( -1==result ) ) FD_LOG_ERR(( "pwritev2() failed (%d-%s)", errno, fd_io_strerror( errno ) ));
2734 0 : else if( FD_UNLIKELY( !result ) ) FD_LOG_ERR(( "accounts database is corrupt, pwritev2() returned 0 at offset %lu with %lu bytes remaining", entry_off+written, entry_sz-written ));
2735 0 : written += (ulong)result;
2736 0 : accdb->metrics->bytes_written += (ulong)result;
2737 0 : accdb->metrics->write_ops++;
2738 :
2739 0 : for( int v=0; v<2; v++ ) {
2740 0 : if( (ulong)result>=(ulong)entry_iovs[ v ].iov_len ) {
2741 0 : result -= (long)entry_iovs[ v ].iov_len;
2742 0 : entry_iovs[ v ].iov_len = 0UL;
2743 0 : } else {
2744 0 : entry_iovs[ v ].iov_base = (uchar *)entry_iovs[ v ].iov_base + result;
2745 0 : entry_iovs[ v ].iov_len -= (ulong)result;
2746 0 : break;
2747 0 : }
2748 0 : }
2749 0 : }
2750 0 : }
2751 301458 : }
2752 :
2753 : // STEP 10.
2754 : // Now that the data is on disk, publish the evicted account offsets
2755 : // so concurrent acquire threads spinning on
2756 : // offset==FD_ACCDB_OFF_INVAL can proceed. The fence ensures
2757 : // pwritev2 data is globally visible before the offset stores.
2758 301458 : FD_COMPILER_MFENCE();
2759 301458 : for( int k=0; k<pending_cnt; k++ ) {
2760 0 : pending_accs[ k ]->offset_fork = fd_accdb_acc_pack_offset_fork( pending_offs[ k ], fd_accdb_acc_fork_id(pending_accs[ k ]) );
2761 0 : pending_lines[ k ]->persisted = 1;
2762 0 : }
2763 :
2764 : // STEP 11.
2765 : // Now construct iovecs for any reads we need to do of accounts into
2766 : // the cache. For reading accounts, we read them directly into the
2767 : // sole cache line we took (and maybe just evicted). For writing
2768 : // accounts, we read them into the right sized cache line, and later
2769 : // it will be copied to the staging buffer. This is to prevent
2770 : // repeatedly reading the same account off disk into cache, if it is
2771 : // being written cold multiple times and every write fails.
2772 :
2773 301458 : ulong read_ops_cnt = 0UL;
2774 301458 : ulong read_offsets[ FD_ACCDB_CACHE_CLASS_CNT*FD_ACCDB_MAX_ACQUIRE_CNT ];
2775 301458 : uchar * read_bases[ FD_ACCDB_CACHE_CLASS_CNT*FD_ACCDB_MAX_ACQUIRE_CNT ];
2776 301458 : ulong read_sizes[ FD_ACCDB_CACHE_CLASS_CNT*FD_ACCDB_MAX_ACQUIRE_CNT ];
2777 301458 : struct iovec read_ops[ FD_ACCDB_CACHE_CLASS_CNT*FD_ACCDB_MAX_ACQUIRE_CNT ];
2778 :
2779 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2780 302340 : if( FD_UNLIKELY( !accmetas[ i ] || exists_in_cache[ i ] ) ) continue;
2781 :
2782 30 : accdb->metrics->accounts_not_found_per_class[ fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size ) ) ]++;
2783 :
2784 : /* Tombstones (lamports==0) have no on-disk payload to read, and
2785 : background_advance_root may unlink the acc and never assign it a
2786 : disk offset, so the offset_fork spin below would hang forever.
2787 : Step 15's tombstone reset zeros the owner for these accounts. */
2788 30 : if( FD_UNLIKELY( !accmetas[ i ]->lamports ) ) continue;
2789 :
2790 : /* We are guaranteed that if an account is in the cache, the bytes
2791 : are available (all cache operations are atomic via refcnt CAS),
2792 : but we are not guaranteed that if something is _not_ in the cache
2793 : that it has been written back to disk yet. In particular, if we
2794 : are trying to read an account that another thread is in the
2795 : process of evicting, we know they removed it from the cache, but
2796 : we don't know exactly when they will have written it back fully
2797 : to disk, so we may need to wait for that here.
2798 :
2799 : Compaction may concurrently relocate this record, but
2800 : epoch-based safe reclamation guarantees the source partition
2801 : is not freed until all epoch-protected operations that could
2802 : have snapshotted the old offset have exited. So the data at the
2803 : snapshotted offset remains stable for the duration of our
2804 : read and no post-read validation is needed. */
2805 30 : ulong off_packed = FD_VOLATILE_CONST( accmetas[ i ]->offset_fork );
2806 30 : while( FD_UNLIKELY( (off_packed & FD_ACCDB_OFF_MASK)==FD_ACCDB_OFF_INVAL ) ) {
2807 0 : FD_SPIN_PAUSE();
2808 0 : off_packed = FD_VOLATILE_CONST( accmetas[ i ]->offset_fork );
2809 0 : }
2810 30 : fd_racesan_hook( "accdb_coldload:pre_iovec" );
2811 :
2812 30 : read_offsets[ read_ops_cnt ] = fd_accdb_acc_offset(accmetas[ i ]) + offsetof(fd_accdb_disk_meta_t, owner);
2813 30 : read_bases[ read_ops_cnt ] = original_cache_line[ i ]->owner;
2814 30 : read_sizes[ read_ops_cnt ] = 32UL + FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size );
2815 30 : read_ops[ read_ops_cnt++ ] = (struct iovec){ .iov_base = original_cache_line[ i ]->owner, .iov_len = 32UL + FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size ) };
2816 30 : }
2817 :
2818 : // STEP 12.
2819 : // Almost done... now do the actual reads of accounts into cache,
2820 : // using the iovecs we constructed. This is basically the same loop
2821 : // as the writes, but with preadv2 instead of pwritev2, and that the
2822 : // reads are not necessarily all contiguous, but occur at random
2823 : // offsets.
2824 : //
2825 : // CONCURRENCY: The compaction tile may concurrently relocate a
2826 : // record we are about to read (both are epoch-protected). Epoch-
2827 : // based safe reclamation guarantees the source partition is not
2828 : // freed until all epoch-protected operations that could have
2829 : // snapshotted the old offset have exited, so the data at the
2830 : // remains stable for the duration of this read — no post-read
2831 : // validation or retry is needed.
2832 301488 : for( ulong i=0UL; i<read_ops_cnt; i++ ) {
2833 30 : ulong bytes_read = 0UL;
2834 60 : while( FD_LIKELY( bytes_read<read_sizes[ i ] ) ) {
2835 30 : long result = preadv2( accdb->fd, &read_ops[ i ], 1, (long)(read_offsets[ i ]+bytes_read), 0 );
2836 30 : if( FD_UNLIKELY( -1==result && (errno==EINTR || errno==EAGAIN || errno==EWOULDBLOCK ) ) ) continue;
2837 30 : else if( FD_UNLIKELY( -1==result ) ) FD_LOG_ERR(( "preadv2() failed (%d-%s)", errno, fd_io_strerror( errno ) ));
2838 30 : else if( FD_UNLIKELY( !result ) ) FD_LOG_ERR(( "accounts database is corrupt, data expected at offset %lu with size %lu exceeded file extents",
2839 30 : read_offsets[ i ]+bytes_read, read_sizes[ i ] ));
2840 30 : fd_accdb_partition_read_bump( accdb, read_offsets[ i ]+bytes_read, (ulong)result );
2841 30 : bytes_read += (ulong)result;
2842 30 : accdb->metrics->bytes_read += (ulong)result;
2843 30 : accdb->metrics->read_ops++;
2844 :
2845 30 : read_ops[ i ].iov_base = read_bases[ i ] + bytes_read;
2846 30 : read_ops[ i ].iov_len = read_sizes[ i ] - bytes_read;
2847 30 : }
2848 30 : }
2849 :
2850 : // STEP 13.
2851 : // Publish the real acc index for any cache lines we just loaded
2852 : // from disk, so concurrent threads spinning on acc_idx==UINT_MAX
2853 : // can proceed. The fence ensures all preadv2 data is visible
2854 : // before the sentinel is cleared.
2855 301458 : FD_COMPILER_MFENCE();
2856 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2857 302340 : if( FD_UNLIKELY( !accmetas[ i ] || exists_in_cache[ i ] ) ) continue;
2858 30 : FD_VOLATILE( original_cache_line[ i ]->acc_idx ) = (uint)( accmetas[ i ] - accdb->acc_pool );
2859 30 : FD_TEST( FD_VOLATILE_CONST( original_cache_line[ i ]->acc_idx )==(uint)( accmetas[ i ] - accdb->acc_pool ) );
2860 30 : }
2861 :
2862 : // STEP 14.
2863 : // Spin-wait for any cache lines found via acc->cache_idx that are
2864 : // still being loaded by another thread's preadv2. The loading
2865 : // thread sets acc_idx to UINT_MAX before publishing cache_idx
2866 : // and publishes the real acc index after its read completes.
2867 : // This step is placed as late as possible to give the loading
2868 : // thread maximum time to finish before we need to spin.
2869 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2870 302340 : if( FD_UNLIKELY( !accmetas[ i ] && !writable[ i ] ) ) continue;
2871 :
2872 195909 : if( FD_UNLIKELY( !original_cache_line[ i ] ) ) continue;
2873 95754 : if( FD_LIKELY( FD_VOLATILE_CONST( original_cache_line[ i ]->acc_idx )!=UINT_MAX ) ) goto step13_check;
2874 0 : accdb->metrics->accounts_waited++;
2875 0 : while( FD_UNLIKELY( FD_VOLATILE_CONST( original_cache_line[ i ]->acc_idx )==UINT_MAX ) ) {
2876 0 : fd_racesan_hook( "accdb_acquire:step14_load_wait" );
2877 0 : FD_SPIN_PAUSE();
2878 0 : }
2879 95754 : step13_check:;
2880 : #if FD_TMPL_USE_HANDHOLDING
2881 : FD_TEST( original_cache_line[ i ]->key.generation==accmetas[ i ]->key.generation &&
2882 : !memcmp( original_cache_line[ i ]->key.pubkey, pubkeys[ i ], 32UL ) );
2883 : #endif
2884 95754 : }
2885 :
2886 : // STEP 15.
2887 : // Now that all reads from disk into original_cache_line have
2888 : // completed (and any concurrent loaders have published their
2889 : // acc_idx in step 14), copy the owner into the output entries.
2890 : // This must happen here rather than in step 8 because the cache
2891 : // line owner is only valid post-read for cold loads.
2892 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2893 302340 : if( FD_UNLIKELY( !accmetas[ i ] ) ) continue;
2894 95754 : fd_racesan_hook( "accdb_acquire:pre_step14_owner" );
2895 : /* Tombstone reset: see STEP 7 comment. */
2896 95754 : if( FD_UNLIKELY( accmetas[ i ]->lamports==0UL ) ) {
2897 30 : memset( out_accs[ i ].owner, 0, 32UL );
2898 30 : memset( out_accs[ i ].prior_owner, 0, 32UL );
2899 95724 : } else {
2900 95724 : fd_memcpy( out_accs[ i ].owner, original_cache_line[ i ]->owner, 32UL );
2901 95724 : fd_memcpy( out_accs[ i ].prior_owner, original_cache_line[ i ]->owner, 32UL );
2902 95724 : }
2903 95754 : }
2904 :
2905 : // STEP 16.
2906 : // Finally, copy any accounts we are writing into the staging
2907 : // buffers, so they occupy a 10MiB cache line for the execution
2908 : // system.
2909 603798 : for( ulong i=0UL; i<pubkeys_cnt; i++ ) {
2910 302340 : if( FD_UNLIKELY( !accmetas[ i ] || !writable[ i ] ) ) continue;
2911 :
2912 14646 : ulong copy_sz = (ulong)FD_ACCDB_SIZE_DATA( accmetas[ i ]->executable_size );
2913 14646 : fd_memcpy( destination_cache_lines[ i ][ 7UL ]+1UL, original_cache_line[ i ]+1UL, copy_sz );
2914 14646 : accdb->metrics->bytes_copied += copy_sz;
2915 14646 : }
2916 :
2917 301458 : FD_COMPILER_MFENCE();
2918 301458 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
2919 301458 : }
2920 :
2921 : void
2922 : fd_accdb_acquire( fd_accdb_t * accdb,
2923 : fd_accdb_fork_id_t fork_id,
2924 : ulong pubkeys_cnt,
2925 : uchar const * const * pubkeys,
2926 : int * writable,
2927 300756 : fd_acc_t * out_accs ) {
2928 300756 : FD_TEST( accdb->acquire_state==FD_ACCDB_ACQUIRE_STATE_IDLE );
2929 300756 : accdb->acquire_state = FD_ACCDB_ACQUIRE_STATE_OPEN;
2930 300756 : fd_accdb_acquire_inner( accdb, fork_id, RESERVATION_TYPE_SIMPLE, 0UL, pubkeys_cnt, pubkeys, writable, out_accs );
2931 300756 : }
2932 :
2933 : void
2934 : fd_accdb_acquire_a( fd_accdb_t * accdb,
2935 : fd_accdb_fork_id_t fork_id,
2936 : ulong pubkeys_cnt,
2937 : uchar const * const * pubkeys,
2938 : int * writable,
2939 351 : fd_acc_t * out_accs ) {
2940 351 : FD_TEST( accdb->acquire_state==FD_ACCDB_ACQUIRE_STATE_IDLE );
2941 351 : accdb->acquire_state = FD_ACCDB_ACQUIRE_STATE_PHASE_A;
2942 351 : fd_accdb_acquire_inner( accdb, fork_id, RESERVATION_TYPE_MAYBE_PROGRAMDATA, 0UL, pubkeys_cnt, pubkeys, writable, out_accs );
2943 351 : }
2944 :
2945 : void
2946 : fd_accdb_acquire_b( fd_accdb_t * accdb,
2947 : fd_accdb_fork_id_t fork_id,
2948 : ulong reserved_cnt,
2949 : ulong pubkeys_cnt,
2950 : uchar const * const * pubkeys,
2951 : int * writable,
2952 351 : fd_acc_t * out_accs ) {
2953 351 : FD_TEST( accdb->acquire_state==FD_ACCDB_ACQUIRE_STATE_PHASE_A );
2954 351 : accdb->acquire_state = FD_ACCDB_ACQUIRE_STATE_OPEN;
2955 351 : fd_accdb_acquire_inner( accdb, fork_id, RESERVATION_TYPE_ALREADY_RESERVED, reserved_cnt, pubkeys_cnt, pubkeys, writable, out_accs );
2956 351 : }
2957 :
2958 : /* release_inner drains one group of acquired accs but does NOT change the
2959 : handle's acquire_state. The public fd_accdb_release / fd_accdb_release_ab
2960 : wrappers below own the state transition (a single-phase release closes
2961 : the bracket; release_ab drains both phase groups then closes). */
2962 : static void
2963 : release_inner( fd_accdb_t * accdb,
2964 : ulong accs_cnt,
2965 301026 : fd_acc_t * accs ) {
2966 301026 : FD_TEST( accdb->acquire_state==FD_ACCDB_ACQUIRE_STATE_OPEN );
2967 :
2968 301026 : {
2969 301026 : ulong prev = FD_VOLATILE_CONST( *accdb->my_epoch_slot );
2970 301026 : FD_TEST( prev==ULONG_MAX || prev<=FD_VOLATILE_CONST( accdb->shmem->epoch ) );
2971 301026 : }
2972 :
2973 301026 : FD_COMPILER_MFENCE();
2974 301026 : FD_VOLATILE( *accdb->my_epoch_slot ) = FD_VOLATILE_CONST( accdb->shmem->epoch );
2975 301026 : FD_HW_MFENCE(); /* StoreLoad: epoch store must be globally visible
2976 : before any subsequent loads so the deferred
2977 : reclamation scan does not miss us. */
2978 :
2979 : // STEP 1.
2980 : // For each cache line which was written to in the 10MiB staging
2981 : // buffer, we may need to copy to the data out to a right sized
2982 : // cache line. Figuring out the target cache line is non-obvious,
2983 : // but follows the more complete logic below this, we just pull the
2984 : // memcpy out so they are not done inside the cache lock.
2985 :
2986 602937 : for( ulong i=0UL; i<accs_cnt; i++ ) {
2987 301911 : if( FD_UNLIKELY( accs[ i ]._original_size_class==ULONG_MAX && !accs[ i ]._writable ) ) continue;
2988 :
2989 : #if FD_TMPL_USE_HANDHOLDING
2990 : if( FD_LIKELY( accs[ i ]._original_size_class!=ULONG_MAX ) ) {
2991 : FD_TEST( accs[ i ]._original_cache_idx<accdb->shmem->cache_class_max[ accs[ i ]._original_size_class ] );
2992 : }
2993 : if( FD_UNLIKELY( accs[ i ].commit ) ) FD_TEST( accs[ i ]._writable );
2994 : #endif
2995 :
2996 195507 : if( FD_LIKELY( !accs[ i ]._writable || !accs[ i ].commit ) ) continue;
2997 : #if FD_TMPL_USE_HANDHOLDING
2998 : if( FD_UNLIKELY( accs[ i ]._overwrite ) ) {
2999 : FD_TEST( accs[ i ]._writable );
3000 : FD_TEST( accs[ i ]._original_cache_idx!=ULONG_MAX );
3001 : FD_TEST( accs[ i ]._original_size_class!=ULONG_MAX );
3002 : }
3003 : #endif
3004 :
3005 113880 : ulong original_size_class = accs[ i ]._original_size_class;
3006 113880 : ulong new_size_class = fd_accdb_cache_class( accs[ i ].data_len );
3007 113880 : if( FD_UNLIKELY( new_size_class==7UL ) ) continue;
3008 :
3009 113568 : fd_accdb_cache_line_t * target_cache_line;
3010 113568 : if( FD_LIKELY( original_size_class==new_size_class && accs[ i ]._overwrite ) ) target_cache_line = cache_line( accdb, original_size_class, accs[ i ]._original_cache_idx );
3011 110487 : else target_cache_line = cache_line( accdb, new_size_class, accs[ i ]._write.destination_cache_idx[ new_size_class ] );
3012 :
3013 113568 : fd_accdb_cache_line_t * staging_line = cache_line( accdb, 7UL, accs[ i ]._write.destination_cache_idx[ 7UL ] );
3014 :
3015 113568 : fd_racesan_hook( "accdb_commit:pre_owner_write" );
3016 :
3017 : #if FD_TMPL_USE_HANDHOLDING
3018 : if( FD_UNLIKELY( original_size_class==new_size_class && accs[ i ]._overwrite ) ) {
3019 : uint rc = FD_VOLATILE_CONST( target_cache_line->refcnt );
3020 : FD_TEST( target_cache_line->key.generation==accs[ i ]._generation &&
3021 : !memcmp( target_cache_line->key.pubkey, accs[ i ].pubkey, 32UL ) &&
3022 : rc>0U &&
3023 : rc!=FD_ACCDB_EVICT_SENTINEL );
3024 : }
3025 : #endif
3026 :
3027 113568 : fd_memcpy( target_cache_line->owner, accs[ i ].owner, 32UL );
3028 113568 : fd_memcpy( target_cache_line+1UL, staging_line+1UL, accs[ i ].data_len );
3029 113568 : accdb->metrics->bytes_copied += accs[ i ].data_len;
3030 113568 : }
3031 :
3032 : // STEP 2.
3033 : // Now update the metadata structures and free lists to reflect the
3034 : // fact that we are done with these cache lines. This is fully
3035 : // atomic with CLOCK.
3036 :
3037 602937 : for( ulong i=0UL; i<accs_cnt; i++ ) {
3038 301911 : if( FD_UNLIKELY( accs[ i ]._original_size_class==ULONG_MAX && !accs[ i ]._writable ) ) continue;
3039 :
3040 195507 : ulong original_size_class = accs[ i ]._original_size_class;
3041 195507 : fd_accdb_cache_line_t * original_cache_line = accs[ i ]._original_cache_idx==ULONG_MAX ? NULL : cache_line( accdb, original_size_class, accs[ i ]._original_cache_idx );
3042 : /* For overwrite commits, defer the refcnt decrement on
3043 : original_cache_line until after invalidation completes. If
3044 : we dropped refcnt to 0 here, a concurrent CLOCK sweep could
3045 : CAS(refcnt, 0, EVICT_SENTINEL) and steal the line before we
3046 : get to invalidate it, causing data corruption.
3047 : Non-overwrite and non-commit paths unpin
3048 : immediately because they never invalidate the original line. */
3049 195507 : if( FD_LIKELY( original_cache_line ) ) {
3050 : #if FD_TMPL_USE_HANDHOLDING
3051 : FD_TEST( original_cache_line->refcnt>0U );
3052 : #endif
3053 95352 : if( FD_LIKELY( !accs[ i ]._writable || !accs[ i ].commit || !accs[ i ]._overwrite ) ) {
3054 91917 : FD_ATOMIC_FETCH_AND_SUB( &original_cache_line->refcnt, 1U );
3055 91917 : }
3056 95352 : }
3057 :
3058 195507 : if( FD_LIKELY( !accs[ i ]._writable ) ) {
3059 : /* For readonly accounts, mark as recently used so the CLOCK
3060 : algorithm gives it a second chance before eviction. */
3061 : #if FD_TMPL_USE_HANDHOLDING
3062 : FD_TEST( original_cache_line );
3063 : #endif
3064 80940 : original_cache_line->referenced = 1;
3065 80940 : continue;
3066 80940 : }
3067 :
3068 114567 : fd_accdb_cache_line_t * destination_cache_lines[ FD_ACCDB_CACHE_CLASS_CNT ];
3069 1031103 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) destination_cache_lines[ j ] = cache_line( accdb, j, accs[ i ]._write.destination_cache_idx[ j ] );
3070 114567 : int destination_committed[ FD_ACCDB_CACHE_CLASS_CNT ] = {0};
3071 :
3072 114567 : if( FD_LIKELY( !accs[ i ].commit ) ) {
3073 : /* If it's writable but it didn't commit, all of the destination
3074 : cache lines (including the staging buffer which is trashed) are
3075 : unused and can be pushed to the CAS free list for immediate
3076 : reuse. Whatever buffer it was accessing also gets marked as
3077 : recently used. */
3078 687 : if( FD_LIKELY( original_cache_line ) ) original_cache_line->referenced = 1;
3079 6183 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) {
3080 : /* acquire_cache_line via CLOCK leaves line->acc_idx pointing
3081 : at the prior owner. cache_free_push consumers (CLOCK,
3082 : background_preevict) skip lines only when acc_idx==UINT_MAX
3083 : AND gen==UINT_MAX; if we leave the stale acc_idx, a future
3084 : CLOCK pick would call line 849/853 against the wrong acc
3085 : and corrupt its cache_idx/valid. */
3086 5496 : destination_cache_lines[ j ]->acc_idx = UINT_MAX;
3087 5496 : destination_cache_lines[ j ]->key.generation = UINT_MAX;
3088 5496 : destination_cache_lines[ j ]->persisted = 1;
3089 5496 : while( FD_UNLIKELY( FD_ATOMIC_CAS( &destination_cache_lines[ j ]->refcnt, 1U, 0U )!=1U ) ) {
3090 0 : fd_racesan_hook( "accdb_release:dest_refcnt_wait" );
3091 0 : FD_SPIN_PAUSE();
3092 0 : }
3093 5496 : cache_free_push( accdb, j, destination_cache_lines[ j ] );
3094 5496 : }
3095 687 : continue;
3096 687 : }
3097 :
3098 113880 : ulong new_size_class = fd_accdb_cache_class( accs[ i ].data_len );
3099 113880 : uint original_acc_idx = original_cache_line ? original_cache_line->acc_idx : UINT_MAX;
3100 113880 : fd_accdb_cache_line_t * committed_line;
3101 :
3102 : /* For overwrites, invalidate the on-disk offset BEFORE removing
3103 : the cache acc. This ensures a concurrent acquire that misses
3104 : the cache will see offset==FD_ACCDB_OFF_INVAL and spin-wait,
3105 : rather than reading stale on-disk bytes from the old location.
3106 : The CAS-loop exchange also serializes with a concurrent
3107 : compaction CAS (old_offset -> dest_offset). */
3108 113880 : ulong old_offset = FD_ACCDB_OFF_INVAL;
3109 113880 : if( FD_LIKELY( accs[ i ]._overwrite ) ) {
3110 3435 : fd_accdb_accmeta_t * ow_accmeta = &accdb->acc_pool[ original_acc_idx ];
3111 3435 : fd_racesan_hook( "accdb_overwrite:pre_xchg_offset" );
3112 3435 : old_offset = fd_accdb_acc_xchg_offset( ow_accmeta, FD_ACCDB_OFF_INVAL );
3113 3435 : if( FD_LIKELY( old_offset!=FD_ACCDB_OFF_INVAL ) ) {
3114 0 : fd_accdb_shmem_bytes_freed( accdb->shmem, old_offset, (ulong)FD_ACCDB_SIZE_DATA(ow_accmeta->executable_size)+sizeof(fd_accdb_disk_meta_t) );
3115 0 : FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->shmetrics->disk_used_bytes, (ulong)FD_ACCDB_SIZE_DATA(ow_accmeta->executable_size)+sizeof(fd_accdb_disk_meta_t) );
3116 0 : }
3117 3435 : }
3118 :
3119 113880 : if( FD_UNLIKELY( new_size_class==7UL ) ) {
3120 : /* The account belongs in the largest size class, and we already
3121 : have it resident in a 10MiB buffer anyway, so no need to copy
3122 : back. If we are "overwriting" (same generation as the account
3123 : came from), then the original can be discarded (pushed to
3124 : the CAS free list) and removed from the cache. */
3125 312 : destination_cache_lines[ 7UL ]->persisted = 0;
3126 312 : destination_committed[ 7UL ] = 1;
3127 312 : if( FD_LIKELY( accs[ i ]._overwrite ) ) {
3128 : /* Atomically clear acc.VALID and acc.cache_idx BEFORE freeing
3129 : the line, so a reader cannot observe acc.VALID=1 with
3130 : acc.cache_idx pointing at a line that has been recycled to
3131 : another acc. evict_clear_acc_cache_ref uses the CLAIM
3132 : protocol to serialize with cold_load_acc. */
3133 303 : evict_clear_acc_cache_ref( &accdb->acc_pool[ original_acc_idx ], original_size_class, accs[ i ]._original_cache_idx );
3134 :
3135 : /* Convert our own pin directly into the eviction claim
3136 : (CAS refcnt 1 -> EVICT_SENTINEL) so refcnt never passes
3137 : through 0 while acc_idx/key.generation are still valid:
3138 : in such a window background_preevict could claim and free
3139 : the line, and a claim of our own would then succeed on
3140 : the free-listed line and push it a second time. The CAS
3141 : fails if a concurrent reader that pinned the line via
3142 : cache_try_pin BEFORE evict_clear_acc_cache_ref completed
3143 : still holds a reference (its ABA check on
3144 : line->key.generation is not synchronized with our writes
3145 : to that field); then just drop our pin and leave
3146 : acc_idx/key.generation intact so CLOCK reclaims the line
3147 : once the reader unpins. That reclaim's
3148 : evict_clear_acc_cache_ref is a no-op, but its dirty-
3149 : writeback gate keys on persisted/acc_idx and would
3150 : republish the pre-overwrite bytes over the committed
3151 : version, so set persisted while still pinned. */
3152 303 : original_cache_line->persisted = 1;
3153 303 : fd_racesan_hook( "accdb_release:pre_discard_claim" );
3154 303 : if( FD_LIKELY( FD_ATOMIC_CAS( &original_cache_line->refcnt, 1U, FD_ACCDB_EVICT_SENTINEL )==1U ) ) {
3155 303 : original_cache_line->acc_idx = UINT_MAX;
3156 303 : original_cache_line->key.generation = UINT_MAX;
3157 303 : original_cache_line->refcnt = 0;
3158 303 : cache_free_push( accdb, original_size_class, original_cache_line );
3159 303 : } else {
3160 0 : FD_ATOMIC_FETCH_AND_SUB( &original_cache_line->refcnt, 1U );
3161 0 : }
3162 303 : }
3163 312 : committed_line = destination_cache_lines[ 7UL ];
3164 113568 : } else {
3165 : /* The account started in some arbitrary size class, transited
3166 : through a 10MiB staging buffer, and is now being written back
3167 : to some arbitrary (non-10MiB) size class, so we need to copy it
3168 : there. The staging buffer is discarded. If we are going to
3169 : a different size class, and we are "overwriting" (same
3170 : generation), then the original can also be discarded, but if
3171 : we are staying in the same size class, we can reuse the cache
3172 : line in place. */
3173 113568 : fd_accdb_cache_line_t * target_cache_line;
3174 113568 : if( FD_LIKELY( original_size_class==new_size_class ) ) {
3175 13698 : if( FD_LIKELY( accs[ i ]._overwrite ) ) {
3176 : /* a reader holding a stale acc->cache_idx from this line's
3177 : previous life may hold a transient cache_try_pin pin, so
3178 : our own pin only bounds refcnt from below. */
3179 3081 : uint ow_rc = FD_VOLATILE_CONST( original_cache_line->refcnt );
3180 3081 : FD_TEST( ow_rc>0U && ow_rc!=FD_ACCDB_EVICT_SENTINEL );
3181 3081 : original_cache_line->key.generation = UINT_MAX;
3182 : /* Keep refcnt>=1 through the reuse window so CLOCK cannot
3183 : steal the line between invalidation and re-publish. The
3184 : pin is released in the destination cleanup loop after
3185 : acc->cache_idx has been republished. */
3186 3081 : original_cache_line->acc_idx = UINT_MAX;
3187 3081 : target_cache_line = original_cache_line;
3188 10617 : } else {
3189 10617 : target_cache_line = destination_cache_lines[ new_size_class ];
3190 10617 : destination_committed[ new_size_class ] = 1;
3191 10617 : }
3192 99870 : } else {
3193 99870 : if( FD_LIKELY( accs[ i ]._overwrite ) ) {
3194 : /* Atomically clear acc.VALID and acc.cache_idx BEFORE freeing
3195 : the line, so a reader cannot observe acc.VALID=1 with
3196 : acc.cache_idx pointing at a line that has been recycled to
3197 : another acc. evict_clear_acc_cache_ref uses the CLAIM
3198 : protocol to serialize with cold_load_acc. See the
3199 : size_class==7 path above for the refcnt claim and
3200 : persisted rationale. */
3201 51 : evict_clear_acc_cache_ref( &accdb->acc_pool[ original_acc_idx ], original_size_class, accs[ i ]._original_cache_idx );
3202 51 : original_cache_line->persisted = 1;
3203 51 : fd_racesan_hook( "accdb_release:pre_discard_claim" );
3204 51 : if( FD_LIKELY( FD_ATOMIC_CAS( &original_cache_line->refcnt, 1U, FD_ACCDB_EVICT_SENTINEL )==1U ) ) {
3205 51 : original_cache_line->acc_idx = UINT_MAX;
3206 51 : original_cache_line->key.generation = UINT_MAX;
3207 51 : original_cache_line->refcnt = 0;
3208 51 : cache_free_push( accdb, original_size_class, original_cache_line );
3209 51 : } else {
3210 0 : FD_ATOMIC_FETCH_AND_SUB( &original_cache_line->refcnt, 1U );
3211 0 : }
3212 51 : }
3213 :
3214 99870 : destination_committed[ new_size_class ] = 1;
3215 99870 : target_cache_line = destination_cache_lines[ new_size_class ];
3216 99870 : }
3217 :
3218 113568 : target_cache_line->persisted = 0;
3219 : /* If target is the original cache line (overwrite, same size
3220 : class), mark as referenced directly since the cleanup loop
3221 : only handles destination lines. */
3222 113568 : if( FD_LIKELY( !destination_committed[ new_size_class ] ) ) target_cache_line->referenced = 1;
3223 113568 : committed_line = target_cache_line;
3224 113568 : }
3225 :
3226 : /* For non-overwrite commits, the original cache line (if any) still
3227 : holds valid ancestor data but is no longer pinned. Mark it as
3228 : recently used so the CLOCK algorithm retains it. */
3229 113880 : if( FD_UNLIKELY( !accs[ i ]._overwrite && original_cache_line ) ) {
3230 10659 : original_cache_line->referenced = 1;
3231 10659 : }
3232 :
3233 : /* Handle every destination cache line: committed ones keep
3234 : refcnt>=1 until acc->cache_idx is published (the deferred
3235 : unpin happens after the publish below), uncommitted ones are
3236 : fully freed to the CAS free list. */
3237 1024920 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) {
3238 911040 : if( destination_committed[ j ] ) {
3239 110799 : destination_cache_lines[ j ]->referenced = 1;
3240 800241 : } else {
3241 : /* See note above (no-commit path): clear stale acc_idx/gen
3242 : before pushing, otherwise CLOCK can pick this line and
3243 : stomp the prior owner's cache_idx/valid. CAS on refcnt for
3244 : the same stray-pin reason as the no-commit path. */
3245 800241 : destination_cache_lines[ j ]->acc_idx = UINT_MAX;
3246 800241 : destination_cache_lines[ j ]->key.generation = UINT_MAX;
3247 800241 : destination_cache_lines[ j ]->persisted = 1;
3248 800241 : while( FD_UNLIKELY( FD_ATOMIC_CAS( &destination_cache_lines[ j ]->refcnt, 1U, 0U )!=1U ) ) {
3249 0 : fd_racesan_hook( "accdb_release:dest_refcnt_wait" );
3250 0 : FD_SPIN_PAUSE();
3251 0 : }
3252 800241 : cache_free_push( accdb, j, destination_cache_lines[ j ] );
3253 800241 : }
3254 911040 : }
3255 :
3256 : /* Update the accounts index for this committed write. For an
3257 : overwrite (same fork+generation), update the existing acc
3258 : acc in place. Otherwise allocate a new acc, prepend it
3259 : to the hash chain, and record the write in a txn linked to
3260 : the fork so advance_root can clean up old versions. */
3261 113880 : if( FD_LIKELY( accs[ i ]._overwrite ) ) {
3262 3435 : accdb->metrics->accounts_committed_overwrite_per_class[ new_size_class ]++;
3263 3435 : committed_line->acc_idx = original_acc_idx;
3264 :
3265 3435 : fd_accdb_accmeta_t * accmeta = &accdb->acc_pool[ original_acc_idx ];
3266 : /* The offset was already atomically swapped to FD_ACCDB_OFF_INVAL
3267 : and bytes freed above, so just update the metadata and
3268 : re-publish the cache location. CAS-loop preserves CLAIM bit
3269 : (a concurrent evict_clear_acc_cache_ref or acc_unlink may
3270 : hold it) and clears VALID; a plain store would clobber CLAIM
3271 : and break those protocols. */
3272 3435 : uint pd = accs[ i ].pd_write ? FD_ACCDB_SIZE_PD_WRITE_BIT : 0U;
3273 3435 : for(;;) {
3274 3435 : uint cur = FD_VOLATILE_CONST( accmeta->executable_size );
3275 3435 : uint nxt = (cur & (FD_ACCDB_SIZE_CACHE_CLAIM_BIT|FD_ACCDB_SIZE_PD_WRITE_BIT))
3276 3435 : | FD_ACCDB_SIZE_PACK( (uint)accs[ i ].data_len, accs[ i ].executable )
3277 3435 : | pd;
3278 3435 : if( FD_LIKELY( FD_ATOMIC_CAS( &accmeta->executable_size, cur, nxt )==cur ) ) break;
3279 0 : FD_SPIN_PAUSE();
3280 0 : }
3281 3435 : accmeta->lamports = accs[ i ].lamports;
3282 3435 : fd_racesan_hook( "accdb_overwrite:mid_inplace" );
3283 :
3284 3435 : fd_memcpy( committed_line->owner, accs[ i ].owner, 32UL );
3285 3435 : fd_memcpy( committed_line->key.pubkey, accmeta->key.pubkey, 32UL );
3286 3435 : committed_line->key.generation = accmeta->key.generation;
3287 3435 : committed_line->acc_idx = original_acc_idx;
3288 3435 : FD_VOLATILE( accmeta->cache_idx ) = FD_ACCDB_ACC_CIDX_PACK( (uint)new_size_class, (uint)cache_line_idx( accdb, new_size_class, committed_line ) );
3289 : /* Atomic OR so a concurrent evict_clear_acc_cache_ref's CLAIM
3290 : clear (FETCH_AND_AND with ~CLAIM) cannot be lost by an RMW
3291 : race with a plain |= store. */
3292 3435 : FD_ATOMIC_FETCH_AND_OR( &accmeta->executable_size, FD_ACCDB_SIZE_CACHE_VALID_BIT );
3293 :
3294 : /* Now that acc->cache_idx is published, unpin so CLOCK can
3295 : eventually evict it. For same-size overwrites, committed_line
3296 : IS the reused original_cache_line. For cross-size overwrites,
3297 : committed_line is a destination line whose refcnt decrement was
3298 : deferred from the cleanup loop. */
3299 3435 : FD_ATOMIC_FETCH_AND_SUB( &committed_line->refcnt, 1U );
3300 3435 : committed_line->referenced = 1;
3301 110445 : } else {
3302 110445 : accdb->metrics->accounts_committed_new_per_class[ new_size_class ]++;
3303 110445 : fd_accdb_accmeta_t * accmeta = acc_pool_acquire( accdb->acc_pool_join );
3304 110445 : FD_TEST( accmeta );
3305 110445 : ulong acc_idx = acc_pool_idx( accdb->acc_pool_join, accmeta );
3306 110445 : fd_memcpy( accmeta->key.pubkey, accs[ i ].pubkey, 32UL );
3307 110445 : accmeta->lamports = accs[ i ].lamports;
3308 110445 : accmeta->executable_size = FD_ACCDB_SIZE_PACK( (uint)accs[ i ].data_len, accs[ i ].executable )
3309 110445 : | (accs[ i ].pd_write ? FD_ACCDB_SIZE_PD_WRITE_BIT : 0U);
3310 110445 : accmeta->key.generation = accs[ i ]._generation;
3311 110445 : accmeta->offset_fork = fd_accdb_acc_pack_offset_fork( FD_ACCDB_OFF_INVAL, accs[ i ]._fork_id );
3312 :
3313 : /* Publish in the cache BEFORE the acc_map head so that a
3314 : concurrent acquire that finds this acc in the hash chain will
3315 : also find a cache hit, rather than inserting a conflicting
3316 : placeholder cache acc. */
3317 110445 : committed_line->acc_idx = (uint)acc_idx;
3318 110445 : fd_memcpy( committed_line->owner, accs[ i ].owner, 32UL );
3319 110445 : fd_memcpy( committed_line->key.pubkey, accmeta->key.pubkey, 32UL );
3320 110445 : committed_line->key.generation = accmeta->key.generation;
3321 110445 : FD_VOLATILE( accmeta->cache_idx ) = FD_ACCDB_ACC_CIDX_PACK( (uint)new_size_class, (uint)cache_line_idx( accdb, new_size_class, committed_line ) );
3322 : /* Atomic OR so a concurrent evict_clear_acc_cache_ref's CLAIM
3323 : clear (FETCH_AND_AND with ~CLAIM) cannot be lost by an RMW
3324 : race with a plain |= store. */
3325 110445 : FD_ATOMIC_FETCH_AND_OR( &accmeta->executable_size, FD_ACCDB_SIZE_CACHE_VALID_BIT );
3326 :
3327 : /* Now that acc->cache_idx is published, unpin it so
3328 : CLOCK can eventually evict it. */
3329 110445 : FD_ATOMIC_FETCH_AND_SUB( &committed_line->refcnt, 1U );
3330 110445 : committed_line->referenced = 1;
3331 :
3332 : /* CAS loop to prepend to the hash chain. Succeeds on the first
3333 : try in most cases, but a concurrent acc_unlink CAS removing
3334 : the old head can change acc_map[idx] between our load and
3335 : CAS. Multiple concurrent releases may also race on the head
3336 : pointer — the CAS retry handles this. */
3337 110445 : for(;;) {
3338 110445 : uint old_head = FD_VOLATILE_CONST( accdb->acc_map[ accs[ i ]._acc_map_idx ] );
3339 110445 : accmeta->map.next = old_head;
3340 110445 : FD_COMPILER_MFENCE();
3341 110445 : fd_racesan_hook( "accdb_release:pre_chain_cas" );
3342 110445 : if( FD_LIKELY( FD_ATOMIC_CAS( &accdb->acc_map[ accs[ i ]._acc_map_idx ], old_head, (uint)acc_idx )==old_head ) ) break;
3343 0 : FD_SPIN_PAUSE();
3344 0 : }
3345 :
3346 : /* CONCURRENCY: The cache acc is published before the acc_map
3347 : head so that a concurrent fd_accdb_acquire reader that
3348 : observes the new head also finds a cache hit, preventing
3349 : duplicate cache insertion.
3350 :
3351 : (1) The CAS on acc_map[idx] serializes head-pointer mutations
3352 : from concurrent releases onto the same chain without any
3353 : external lock.
3354 :
3355 : (2) The FD_COMPILER_MFENCE above ensures stores to the acc node
3356 : fields (pubkey, lamports, size, generation, fork_id,
3357 : offset, map.next) are ordered before the CAS that publishes
3358 : the new head. On x86-64 (TSO), hardware also guarantees
3359 : this, but the compiler fence is needed to prevent the
3360 : compiler from reordering the stores. A reader that
3361 : observes the new head is guaranteed to see a fully
3362 : initialized node. A reader that has not yet seen the new
3363 : head simply traverses the previous (still valid) chain.
3364 :
3365 : (3) A concurrent acc_unlink (advance_root / purge) may CAS the
3366 : head away between our load and CAS here. The CAS retry
3367 : loop handles this. */
3368 :
3369 110445 : fd_accdb_txn_t * txn = txn_pool_acquire( accdb->txn_pool );
3370 110445 : FD_TEST( txn ); /* Sized so it always succeeds */
3371 110445 : txn->acc_map_idx = (uint)accs[ i ]._acc_map_idx;
3372 110445 : txn->acc_pool_idx = (uint)acc_idx;
3373 110445 : uint txn_idx = (uint)txn_pool_idx( accdb->txn_pool, txn );
3374 110445 : for(;;) {
3375 110445 : uint old_head = FD_VOLATILE_CONST( accdb->fork_pool[ accs[ i ]._fork_id ].shmem->txn_head );
3376 110445 : txn->fork.next = old_head;
3377 110445 : if( FD_LIKELY( FD_ATOMIC_CAS( &accdb->fork_pool[ accs[ i ]._fork_id ].shmem->txn_head, old_head, txn_idx )==old_head ) ) break;
3378 0 : FD_SPIN_PAUSE();
3379 0 : }
3380 :
3381 110445 : FD_ATOMIC_FETCH_AND_ADD( &accdb->shmem->shmetrics->accounts_total, 1UL );
3382 110445 : }
3383 113880 : }
3384 :
3385 : // STEP 3.
3386 : // Finally, we release the cache class reservations we took at the
3387 : // beginning when we acquired these cache lines. Credits return
3388 : // directly to the shared pool so other threads can use them
3389 : // immediately.
3390 :
3391 301026 : ulong refund[ FD_ACCDB_CACHE_CLASS_CNT ] = {0};
3392 602937 : for( ulong i=0UL; i<accs_cnt; i++ ) {
3393 301911 : if( FD_LIKELY( accs[ i ]._original_size_class!=ULONG_MAX ) ) {
3394 95352 : if( FD_UNLIKELY( accdb->shmem->cache_class_used[ accs[ i ]._original_size_class ].val!=ULONG_MAX ) ) {
3395 3 : refund[ accs[ i ]._original_size_class ]++;
3396 3 : }
3397 95352 : }
3398 301911 : if( FD_UNLIKELY( accs[ i ]._writable ) ) {
3399 1031103 : for( ulong j=0UL; j<FD_ACCDB_CACHE_CLASS_CNT; j++ ) {
3400 916536 : if( FD_UNLIKELY( accdb->shmem->cache_class_used[ j ].val!=ULONG_MAX ) ) {
3401 54 : refund[ j ]++;
3402 54 : }
3403 916536 : }
3404 114567 : }
3405 301911 : }
3406 2709234 : for( ulong k=0UL; k<FD_ACCDB_CACHE_CLASS_CNT; k++ ) {
3407 2408208 : if( FD_UNLIKELY( refund[ k ] ) ) FD_ATOMIC_FETCH_AND_SUB( &accdb->shmem->cache_class_used[ k ].val, refund[ k ] );
3408 2408208 : }
3409 :
3410 301026 : FD_COMPILER_MFENCE();
3411 301026 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
3412 301026 : }
3413 :
3414 : void
3415 : fd_accdb_release( fd_accdb_t * accdb,
3416 : ulong accs_cnt,
3417 300756 : fd_acc_t * accs ) {
3418 300756 : FD_TEST( accdb->acquire_state==FD_ACCDB_ACQUIRE_STATE_OPEN );
3419 300756 : release_inner( accdb, accs_cnt, accs );
3420 300756 : accdb->acquire_state = FD_ACCDB_ACQUIRE_STATE_IDLE;
3421 300756 : }
3422 :
3423 : void
3424 : fd_accdb_release_ab( fd_accdb_t * accdb,
3425 : ulong accs_cnt,
3426 : fd_acc_t * accs,
3427 : ulong execs_cnt,
3428 234 : fd_acc_t * execs ) {
3429 234 : FD_TEST( accdb->acquire_state==FD_ACCDB_ACQUIRE_STATE_OPEN );
3430 234 : release_inner( accdb, accs_cnt, accs );
3431 234 : if( FD_LIKELY( execs_cnt ) ) release_inner( accdb, execs_cnt, execs );
3432 234 : accdb->acquire_state = FD_ACCDB_ACQUIRE_STATE_IDLE;
3433 234 : }
3434 :
3435 : fd_acc_t
3436 : fd_accdb_read_one( fd_accdb_t * accdb,
3437 : fd_accdb_fork_id_t fork_id,
3438 185544 : uchar const * pubkey ) {
3439 185544 : fd_acc_t acc;
3440 185544 : fd_accdb_acquire( accdb, fork_id, 1UL, &pubkey, (int[]){0}, &acc );
3441 185544 : return acc;
3442 185544 : }
3443 :
3444 : void
3445 : fd_accdb_unread_one( fd_accdb_t * accdb,
3446 185544 : fd_acc_t * acc ) {
3447 185544 : fd_accdb_release( accdb, 1UL, acc );
3448 185544 : }
3449 :
3450 : fd_acc_t
3451 : fd_accdb_write_one( fd_accdb_t * accdb,
3452 : fd_accdb_fork_id_t fork_id,
3453 112377 : uchar const * pubkey ) {
3454 112377 : fd_acc_t acc;
3455 112377 : fd_accdb_acquire( accdb, fork_id, 1UL, &pubkey, (int[]){1}, &acc );
3456 112377 : return acc;
3457 112377 : }
3458 :
3459 : void
3460 : fd_accdb_unwrite_one( fd_accdb_t * accdb,
3461 112377 : fd_acc_t * acc ) {
3462 112377 : fd_accdb_release( accdb, 1UL, acc );
3463 112377 : }
3464 :
3465 : void
3466 : fd_accdb_read_one_nocache( fd_accdb_t * accdb,
3467 : fd_accdb_fork_id_t fork_id,
3468 : uchar const * pubkey,
3469 : ulong * out_lamports,
3470 : int * out_executable,
3471 : uchar * out_owner,
3472 : uchar * out_data,
3473 0 : ulong * out_data_len ) {
3474 : /* Publish epoch — protects against compaction freeing the partition
3475 : under us during the preadv2 path. This is the only write the
3476 : readonly joiner makes into accdb shmem (and the pointer it stores
3477 : through is mapped through a separately-mmap'd writable page that
3478 : aliases shmem->joiner_epochs[idx]). */
3479 0 : FD_COMPILER_MFENCE();
3480 0 : FD_VOLATILE( *accdb->my_epoch_slot ) = FD_VOLATILE_CONST( accdb->shmem->epoch );
3481 0 : FD_HW_MFENCE();
3482 :
3483 : /// STEP 1.
3484 : /// Walk the hash chain at acc_map[hash(pubkey)] using the same
3485 : // visibility test as fd_accdb_acquire_inner. See that function
3486 : // for the detailed safety argument under concurrent prepend.
3487 0 : uint root_generation = accdb->fork_pool[ accdb->shmem->root_fork_id.val ].shmem->generation;
3488 0 : fd_accdb_fork_t * fork = &accdb->fork_pool[ fork_id.val ];
3489 0 : ulong hash = fd_accdb_hash( pubkey, accdb->shmem->seed )&(accdb->shmem->chain_cnt-1UL);
3490 0 : uint acc_idx = FD_VOLATILE_CONST( accdb->acc_map[ hash ] );
3491 0 : fd_accdb_accmeta_t const * accmeta = NULL;
3492 0 : while( acc_idx!=UINT_MAX ) {
3493 0 : fd_accdb_accmeta_t const * candidate = &accdb->acc_pool[ acc_idx ];
3494 0 : uint next_idx = FD_VOLATILE_CONST( candidate->map.next );
3495 0 : if( FD_UNLIKELY( (candidate->key.generation>root_generation &&
3496 0 : fd_accdb_acc_fork_id(candidate)!=fork_id.val &&
3497 0 : !descends_set_test( fork->descends, fd_accdb_acc_fork_id(candidate) )) ) ||
3498 0 : memcmp( pubkey, candidate->key.pubkey, 32UL ) ) {
3499 0 : acc_idx = next_idx;
3500 0 : continue;
3501 0 : }
3502 0 : accmeta = candidate;
3503 0 : break;
3504 0 : }
3505 :
3506 0 : if( FD_UNLIKELY( !accmeta ) ) {
3507 0 : accdb->metrics->accounts_acquired_per_class[ 0 ]++;
3508 0 : *out_lamports = 0UL;
3509 0 : FD_COMPILER_MFENCE();
3510 0 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
3511 0 : return;
3512 0 : }
3513 :
3514 : /// STEP 2.
3515 : /// Snapshot acc fields. The acc element's metadata is effectively
3516 : /// immutable from the perspective of cross-fork readers (see the
3517 : /// comment block in fd_accdb.h about cross-fork reads). */
3518 0 : uint snap_es = FD_VOLATILE_CONST( accmeta->executable_size );
3519 0 : uint snap_gen = accmeta->key.generation;
3520 0 : ulong snap_lamports = accmeta->lamports;
3521 0 : uint snap_cidx = FD_VOLATILE_CONST( accmeta->cache_idx );
3522 0 : ulong data_len = (ulong)FD_ACCDB_SIZE_DATA( snap_es );
3523 0 : int executable = FD_ACCDB_SIZE_EXEC( snap_es );
3524 :
3525 0 : accdb->metrics->accounts_acquired_per_class[ fd_accdb_cache_class( data_len ) ]++;
3526 :
3527 0 : if( FD_UNLIKELY( !snap_lamports ) ) {
3528 0 : *out_lamports = 0UL;
3529 0 : FD_COMPILER_MFENCE();
3530 0 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
3531 0 : return;
3532 0 : }
3533 :
3534 : /// STEP 3.
3535 : /// Cache hit fast path with try-read-test (ABA) loop. Same
3536 : /// primitives as cache_try_pin: re-check key.generation + pubkey
3537 : /// before and after the bulk copy, and bail to the disk path if the
3538 : /// line was claimed for eviction (refcnt ==
3539 : /// FD_ACCDB_EVICT_SENTINEL). No CAS on refcnt, we never pin the
3540 : /// line.
3541 0 : if( FD_LIKELY( FD_ACCDB_SIZE_CACHE_VALID( snap_es ) && snap_cidx!=FD_ACCDB_ACC_CIDX_INVAL ) ) {
3542 0 : ulong cls = FD_ACCDB_ACC_CIDX_CLASS( snap_cidx );
3543 0 : ulong idx = FD_ACCDB_ACC_CIDX_IDX ( snap_cidx );
3544 0 : fd_accdb_cache_line_t * line = cache_line( accdb, cls, idx );
3545 :
3546 0 : for(;;) {
3547 0 : uint gen0 = FD_VOLATILE_CONST( line->key.generation );
3548 0 : uint rc0 = FD_VOLATILE_CONST( line->refcnt );
3549 0 : uint ai0 = FD_VOLATILE_CONST( line->acc_idx );
3550 0 : if( FD_UNLIKELY( rc0==FD_ACCDB_EVICT_SENTINEL ) ) goto miss;
3551 0 : if( FD_UNLIKELY( gen0!=snap_gen ) ) goto miss;
3552 0 : if( FD_UNLIKELY( memcmp( line->key.pubkey, pubkey, 32UL ) ) ) goto miss;
3553 : /* acc_idx==UINT_MAX is the "loading" sentinel set by cold_load_acc
3554 : before the preadv2 fills the line. CACHE_VALID can be observed
3555 : set while the bytes are still stale, so fall to the disk path
3556 : (which spins on offset_fork and reads from the file) rather
3557 : than copying garbage. */
3558 0 : if( FD_UNLIKELY( ai0==UINT_MAX ) ) goto miss;
3559 :
3560 0 : FD_COMPILER_MFENCE();
3561 0 : memcpy( out_owner, line->owner, 32UL );
3562 0 : memcpy( out_data, (uchar const *)(line+1UL), data_len );
3563 0 : FD_COMPILER_MFENCE();
3564 :
3565 0 : uint gen1 = FD_VOLATILE_CONST( line->key.generation );
3566 0 : uint rc1 = FD_VOLATILE_CONST( line->refcnt );
3567 0 : uint ai1 = FD_VOLATILE_CONST( line->acc_idx );
3568 0 : if( FD_UNLIKELY( rc1==FD_ACCDB_EVICT_SENTINEL ) ) goto miss;
3569 0 : if( FD_UNLIKELY( gen1!=snap_gen ) ) goto miss;
3570 0 : if( FD_UNLIKELY( memcmp( line->key.pubkey, pubkey, 32UL ) ) ) goto miss;
3571 0 : if( FD_UNLIKELY( ai1==UINT_MAX ) ) goto miss;
3572 :
3573 0 : *out_lamports = snap_lamports;
3574 0 : *out_executable = executable;
3575 0 : *out_data_len = data_len;
3576 0 : accdb->metrics->bytes_copied += data_len;
3577 0 : FD_COMPILER_MFENCE();
3578 0 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
3579 0 : return;
3580 0 : }
3581 0 : }
3582 :
3583 0 : miss:;
3584 0 : accdb->metrics->accounts_not_found_per_class[ fd_accdb_cache_class( FD_ACCDB_SIZE_DATA( snap_es ) ) ]++;
3585 :
3586 : /// STEP 4.
3587 : /// Disk path. Spin until the writer publishes a real offset
3588 : /// (matches STEP 10 of fd_accdb_acquire_inner). Compaction may
3589 : /// concurrently relocate the record, but our published epoch
3590 : /// prevents the source partition from being freed until we exit
3591 : /// our critical section, so the bytes at the snapshotted offset
3592 : /// remain stable for the duration of the read.
3593 0 : fd_racesan_hook( "accdb_nocache:pre_offset" );
3594 0 : ulong off_packed = FD_VOLATILE_CONST( accmeta->offset_fork );
3595 0 : if( FD_UNLIKELY( (off_packed & FD_ACCDB_OFF_MASK)==FD_ACCDB_OFF_INVAL ) ) {
3596 0 : accdb->metrics->accounts_waited++;
3597 0 : while( FD_UNLIKELY( ((off_packed=FD_VOLATILE_CONST( accmeta->offset_fork )) & FD_ACCDB_OFF_MASK)==FD_ACCDB_OFF_INVAL ) ) FD_SPIN_PAUSE();
3598 0 : }
3599 0 : ulong off = off_packed & FD_ACCDB_OFF_MASK;
3600 0 : fd_racesan_hook( "accdb_nocache:pre_preadv2" );
3601 :
3602 0 : struct iovec iovs[ 2 ] = {
3603 0 : { .iov_base = out_owner, .iov_len = 32UL },
3604 0 : { .iov_base = out_data, .iov_len = data_len },
3605 0 : };
3606 0 : ulong total = 32UL+data_len;
3607 0 : ulong start = off+offsetof( fd_accdb_disk_meta_t, owner );
3608 0 : ulong got = 0UL;
3609 0 : int nio = data_len ? 2 : 1;
3610 0 : while( FD_LIKELY( got<total ) ) {
3611 0 : long result = preadv2( accdb->fd, iovs, nio, (long)(start+got), 0 );
3612 0 : if( FD_UNLIKELY( -1==result && (errno==EINTR || errno==EAGAIN || errno==EWOULDBLOCK) ) ) continue;
3613 0 : else if( FD_UNLIKELY( -1==result ) ) FD_LOG_ERR(( "preadv2() failed (%d-%s)", errno, fd_io_strerror( errno ) ));
3614 0 : else if( FD_UNLIKELY( !result ) ) FD_LOG_ERR(( "accounts database is corrupt, data expected at offset %lu with size %lu exceeded file extents", start+got, total ));
3615 0 : fd_accdb_partition_read_bump( accdb, start+got, (ulong)result );
3616 0 : got += (ulong)result;
3617 0 : accdb->metrics->bytes_read += (ulong)result;
3618 0 : accdb->metrics->read_ops++;
3619 :
3620 0 : long r = result;
3621 0 : for( int v=0; v<nio; v++ ) {
3622 0 : if( (ulong)r>=iovs[ v ].iov_len ) {
3623 0 : r -= (long)iovs[ v ].iov_len;
3624 0 : iovs[ v ].iov_len = 0UL;
3625 0 : } else {
3626 0 : iovs[ v ].iov_base = (uchar *)iovs[ v ].iov_base + r;
3627 0 : iovs[ v ].iov_len -= (ulong)r;
3628 0 : break;
3629 0 : }
3630 0 : }
3631 0 : }
3632 :
3633 0 : *out_lamports = snap_lamports;
3634 0 : *out_executable = executable;
3635 0 : *out_data_len = data_len;
3636 :
3637 0 : FD_COMPILER_MFENCE();
3638 0 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
3639 0 : }
3640 :
3641 : int
3642 : fd_accdb_exists( fd_accdb_t * accdb,
3643 : fd_accdb_fork_id_t fork_id,
3644 111 : uchar const * pubkey ) {
3645 111 : FD_COMPILER_MFENCE();
3646 111 : FD_VOLATILE( *accdb->my_epoch_slot ) = FD_VOLATILE_CONST( accdb->shmem->epoch );
3647 111 : FD_HW_MFENCE();
3648 :
3649 111 : uint root_generation = accdb->fork_pool[ accdb->shmem->root_fork_id.val ].shmem->generation;
3650 111 : fd_accdb_fork_t * fork = &accdb->fork_pool[ fork_id.val ];
3651 111 : ulong hash = fd_accdb_hash( pubkey, accdb->shmem->seed )&(accdb->shmem->chain_cnt-1UL);
3652 111 : uint acc = FD_VOLATILE_CONST( accdb->acc_map[ hash ] );
3653 180 : while( acc!=UINT_MAX ) {
3654 174 : fd_accdb_accmeta_t const * candidate_acc = &accdb->acc_pool[ acc ];
3655 174 : uint next_acc = FD_VOLATILE_CONST( candidate_acc->map.next );
3656 :
3657 174 : if( FD_UNLIKELY( (candidate_acc->key.generation>root_generation && fd_accdb_acc_fork_id(candidate_acc)!=fork_id.val && !descends_set_test( fork->descends, fd_accdb_acc_fork_id(candidate_acc) )) ) || memcmp( pubkey, candidate_acc->key.pubkey, 32UL ) ) {
3658 69 : acc = next_acc;
3659 69 : continue;
3660 69 : }
3661 :
3662 105 : break;
3663 174 : }
3664 :
3665 111 : int result;
3666 111 : if( FD_UNLIKELY( acc==UINT_MAX ) ) result = 0;
3667 105 : else result = !!FD_VOLATILE_CONST( accdb->acc_pool[ acc ].lamports );
3668 :
3669 111 : FD_COMPILER_MFENCE();
3670 111 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
3671 111 : return result;
3672 111 : }
3673 :
3674 : int
3675 : fd_accdb_probe_pd_this_fork( fd_accdb_t * accdb,
3676 : fd_accdb_fork_id_t fork_id,
3677 : uchar const * pubkey,
3678 : int * out_pd_write,
3679 : ulong * out_data_len,
3680 114 : ulong * out_lamports ) {
3681 114 : FD_COMPILER_MFENCE();
3682 114 : FD_VOLATILE( *accdb->my_epoch_slot ) = FD_VOLATILE_CONST( accdb->shmem->epoch );
3683 114 : FD_HW_MFENCE();
3684 :
3685 114 : uint root_generation = accdb->fork_pool[ accdb->shmem->root_fork_id.val ].shmem->generation;
3686 114 : fd_accdb_fork_t * fork = &accdb->fork_pool[ fork_id.val ];
3687 114 : ulong hash = fd_accdb_hash( pubkey, accdb->shmem->seed )&(accdb->shmem->chain_cnt-1UL);
3688 114 : uint acc = FD_VOLATILE_CONST( accdb->acc_map[ hash ] );
3689 114 : while( acc!=UINT_MAX ) {
3690 111 : fd_accdb_accmeta_t const * candidate_acc = &accdb->acc_pool[ acc ];
3691 111 : uint next_acc = FD_VOLATILE_CONST( candidate_acc->map.next );
3692 :
3693 111 : if( FD_UNLIKELY( (candidate_acc->key.generation>root_generation && fd_accdb_acc_fork_id(candidate_acc)!=fork_id.val && !descends_set_test( fork->descends, fd_accdb_acc_fork_id(candidate_acc) )) ) || memcmp( pubkey, candidate_acc->key.pubkey, 32UL ) ) {
3694 0 : acc = next_acc;
3695 0 : continue;
3696 0 : }
3697 :
3698 111 : break;
3699 111 : }
3700 :
3701 114 : int pd = 0;
3702 114 : int gen_match = 0;
3703 114 : ulong len = 0UL;
3704 114 : ulong lamports = 0UL;
3705 114 : if( FD_LIKELY( acc!=UINT_MAX ) ) {
3706 111 : fd_accdb_accmeta_t const * m = &accdb->acc_pool[ acc ];
3707 111 : uint es = FD_VOLATILE_CONST( m->executable_size );
3708 111 : gen_match = ( m->key.generation==fork->shmem->generation );
3709 111 : pd = gen_match && FD_ACCDB_SIZE_PD_WRITE( es );
3710 111 : len = FD_ACCDB_SIZE_DATA( es );
3711 111 : lamports = FD_VOLATILE_CONST( m->lamports );
3712 111 : }
3713 :
3714 114 : FD_COMPILER_MFENCE();
3715 114 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
3716 :
3717 114 : *out_pd_write = pd;
3718 114 : if( gen_match ) {
3719 99 : *out_data_len = len;
3720 99 : *out_lamports = lamports;
3721 99 : }
3722 114 : return gen_match;
3723 114 : }
3724 :
3725 : ulong
3726 : fd_accdb_lamports( fd_accdb_t * accdb,
3727 : fd_accdb_fork_id_t fork_id,
3728 8604 : uchar const * pubkey ) {
3729 8604 : FD_COMPILER_MFENCE();
3730 8604 : FD_VOLATILE( *accdb->my_epoch_slot ) = FD_VOLATILE_CONST( accdb->shmem->epoch );
3731 8604 : FD_HW_MFENCE();
3732 :
3733 8604 : uint root_generation = accdb->fork_pool[ accdb->shmem->root_fork_id.val ].shmem->generation;
3734 8604 : fd_accdb_fork_t * fork = &accdb->fork_pool[ fork_id.val ];
3735 8604 : ulong hash = fd_accdb_hash( pubkey, accdb->shmem->seed )&(accdb->shmem->chain_cnt-1UL);
3736 8604 : uint acc = FD_VOLATILE_CONST( accdb->acc_map[ hash ] );
3737 9879 : while( acc!=UINT_MAX ) {
3738 2142 : fd_accdb_accmeta_t const * candidate_acc = &accdb->acc_pool[ acc ];
3739 2142 : uint next_acc = FD_VOLATILE_CONST( candidate_acc->map.next );
3740 :
3741 2142 : if( FD_UNLIKELY( (candidate_acc->key.generation>root_generation && fd_accdb_acc_fork_id(candidate_acc)!=fork_id.val && !descends_set_test( fork->descends, fd_accdb_acc_fork_id(candidate_acc) )) ) || memcmp( pubkey, candidate_acc->key.pubkey, 32UL ) ) {
3742 1275 : acc = next_acc;
3743 1275 : continue;
3744 1275 : }
3745 :
3746 867 : break;
3747 2142 : }
3748 :
3749 8604 : ulong result;
3750 8604 : if( FD_UNLIKELY( acc==UINT_MAX ) ) result = 0UL;
3751 867 : else result = FD_VOLATILE_CONST( accdb->acc_pool[ acc ].lamports );
3752 :
3753 8604 : FD_COMPILER_MFENCE();
3754 8604 : FD_VOLATILE( *accdb->my_epoch_slot ) = ULONG_MAX;
3755 8604 : return result;
3756 8604 : }
3757 :
3758 : /* cache_bg_evict pre-evicts cache lines in the background to keep the
3759 : per-class CAS free lists populated ahead of demand. For each class
3760 : whose immediately available capacity has dropped below low_water,
3761 : a bounded CLOCK sweep claims lines, writes dirty ones to disk, and
3762 : pushes them onto the free list until available capacity reaches
3763 : target. Immediately available capacity includes both the CAS free
3764 : list and the never-initialized tail of the class, since foreground
3765 : allocators can consume either path without evicting resident data.
3766 :
3767 : Budget: at most 256 CLOCK ticks per class per invocation to keep the
3768 : background loop responsive. The function is called every tick of
3769 : fd_accdb_background, so large refills happen across several ticks
3770 : rather than blocking. The low_water / target thresholds are static
3771 : per-class watermarks computed at initialization; pre-eviction only
3772 : converts resident lines into free-list entries and does not consume
3773 : cache-slot reservations.
3774 :
3775 : force: when non-zero, ignore the watermark and sweep every line in
3776 : every class. Always 0 in normal operation; used only by
3777 : test_accdb_racesan to deterministically exercise the writeback path
3778 : without manufacturing real cache pressure. */
3779 :
3780 : static void
3781 : background_preevict( fd_accdb_t * accdb,
3782 : int * charge_busy,
3783 3375590 : int force ) {
3784 3375590 : fd_accdb_shmem_t * shmem = accdb->shmem;
3785 :
3786 30380310 : for( ulong c=0UL; c<FD_ACCDB_CACHE_CLASS_CNT; c++ ) {
3787 27004720 : ulong target = shmem->cache_free_target[ c ];
3788 27004720 : ulong max_c = shmem->cache_class_max[ c ];
3789 27004720 : ulong init = fd_ulong_min( FD_VOLATILE_CONST( shmem->cache_class_init[ c ].val ), max_c );
3790 27004720 : ulong freec = FD_VOLATILE_CONST( shmem->cache_free_cnt[ c ].val );
3791 27004720 : ulong live = init>freec ? init-freec : 0UL;
3792 27004720 : ulong avail = max_c-live;
3793 27004720 : if( FD_LIKELY( !force && avail>=shmem->cache_free_low_water[ c ] ) ) continue;
3794 :
3795 0 : *charge_busy = 1;
3796 :
3797 0 : ulong budget = force ? init : 256UL;
3798 0 : ulong evicted = 0UL;
3799 0 : if( FD_UNLIKELY( force ) ) target = max_c; /* sweep everything */
3800 :
3801 0 : for( ulong tick=0UL; tick<budget && avail+evicted<target; tick++ ) {
3802 : /* Only sweep the lazily initialized prefix. cache_class_init
3803 : may transiently exceed max_c during the acquire_cache_line
3804 : overflow/undo path, so clamp it before using it as the wrap
3805 : bound. */
3806 0 : init = fd_ulong_min( FD_VOLATILE_CONST( shmem->cache_class_init[ c ].val ), max_c );
3807 0 : if( FD_UNLIKELY( !init ) ) break;
3808 :
3809 0 : ulong hand = FD_ATOMIC_FETCH_AND_ADD( &shmem->clock_hand[ c ].val, 1UL ) % init;
3810 :
3811 0 : fd_accdb_cache_line_t * line = cache_line( accdb, c, hand );
3812 :
3813 0 : if( FD_UNLIKELY( line->key.generation==UINT_MAX && line->acc_idx==UINT_MAX ) ) continue;
3814 :
3815 0 : uint rc = FD_VOLATILE_CONST( line->refcnt );
3816 0 : if( FD_UNLIKELY( rc ) ) continue;
3817 :
3818 0 : if( FD_UNLIKELY( line->referenced ) ) {
3819 0 : line->referenced = 0;
3820 0 : continue;
3821 0 : }
3822 :
3823 0 : if( FD_UNLIKELY( FD_ATOMIC_CAS( &line->refcnt, 0U, FD_ACCDB_EVICT_SENTINEL )!=0U ) ) continue;
3824 :
3825 0 : uint acc_idx = line->acc_idx;
3826 : #if FD_TMPL_USE_HANDHOLDING
3827 : uint line_gen FD_FN_UNUSED = line->key.generation;
3828 : #endif
3829 0 : if( FD_LIKELY( acc_idx!=UINT_MAX ) ) {
3830 0 : evict_clear_acc_cache_ref( &accdb->acc_pool[ acc_idx ], c, hand );
3831 0 : }
3832 0 : line->key.generation = UINT_MAX;
3833 0 : if( FD_UNLIKELY( !line->persisted && acc_idx!=UINT_MAX ) ) {
3834 0 : fd_accdb_accmeta_t * accmeta = &accdb->acc_pool[ acc_idx ];
3835 0 : fd_racesan_hook( "preevict:pre_synth" );
3836 : #if FD_TMPL_USE_HANDHOLDING
3837 : FD_TEST( line_gen==accmeta->key.generation &&
3838 : !memcmp( line->key.pubkey, accmeta->key.pubkey, 32UL ) );
3839 : #endif
3840 0 : ulong entry_sz = sizeof(fd_accdb_disk_meta_t)+(ulong)FD_ACCDB_SIZE_DATA( accmeta->executable_size );
3841 :
3842 : /* Atomically swap the old offset to FD_ACCDB_OFF_INVAL so that
3843 : a concurrent compaction CAS (old_offset -> dest_offset)
3844 : cannot succeed between our read and our later store of
3845 : the new file_off. Without the exchange, compaction could
3846 : relocate the record, then our plain store would overwrite
3847 : the relocated offset, leaving the compaction destination
3848 : as unreachable dead space whose bytes are never freed. */
3849 0 : ulong old_offset = fd_accdb_acc_xchg_offset( accmeta, FD_ACCDB_OFF_INVAL );
3850 0 : if( FD_LIKELY( old_offset!=FD_ACCDB_OFF_INVAL ) ) {
3851 0 : fd_accdb_shmem_bytes_freed( shmem, old_offset, entry_sz );
3852 0 : FD_ATOMIC_FETCH_AND_SUB( &shmem->shmetrics->disk_used_bytes, entry_sz );
3853 0 : }
3854 :
3855 0 : fd_accdb_disk_meta_t meta;
3856 0 : fd_memcpy( meta.pubkey, accmeta->key.pubkey, 32UL );
3857 0 : meta.size = FD_ACCDB_SIZE_DATA( accmeta->executable_size );
3858 0 : fd_memcpy( meta.owner, line->owner, 32UL );
3859 :
3860 0 : struct iovec iovs[ 2UL ] = {
3861 0 : { .iov_base = &meta, .iov_len = sizeof(fd_accdb_disk_meta_t) },
3862 0 : { .iov_base = (void *)(line+1UL), .iov_len = FD_ACCDB_SIZE_DATA( accmeta->executable_size ) }
3863 0 : };
3864 :
3865 0 : ulong file_off = allocate_next_write( accdb, entry_sz );
3866 0 : ulong written = 0UL;
3867 0 : while( written<entry_sz ) {
3868 0 : long result = pwritev2( accdb->fd, iovs, 2, (long)(file_off+written), 0 );
3869 0 : if( FD_UNLIKELY( result==-1 && errno==EINTR ) ) continue;
3870 0 : else if( FD_UNLIKELY( result<=0 ) ) FD_LOG_ERR(( "pwritev2() failed (%d-%s)", errno, fd_io_strerror( errno ) ));
3871 0 : written += (ulong)result;
3872 0 : accdb->metrics->bytes_written += (ulong)result;
3873 0 : accdb->metrics->write_ops++;
3874 :
3875 0 : for( int v=0; v<2; v++ ) {
3876 0 : if( (ulong)result>=iovs[ v ].iov_len ) {
3877 0 : result -= (long)iovs[ v ].iov_len;
3878 0 : iovs[ v ].iov_len = 0UL;
3879 0 : } else {
3880 0 : iovs[ v ].iov_base = (uchar *)iovs[ v ].iov_base + result;
3881 0 : iovs[ v ].iov_len -= (ulong)result;
3882 0 : break;
3883 0 : }
3884 0 : }
3885 0 : }
3886 :
3887 0 : FD_COMPILER_MFENCE();
3888 0 : accmeta->offset_fork = fd_accdb_acc_pack_offset_fork( file_off, fd_accdb_acc_fork_id(accmeta) );
3889 0 : FD_ATOMIC_FETCH_AND_ADD( &shmem->shmetrics->disk_used_bytes, entry_sz );
3890 :
3891 0 : accdb->metrics->accounts_preevicted++;
3892 0 : accdb->metrics->accounts_preevicted_per_class[ c ]++;
3893 0 : }
3894 :
3895 0 : line->persisted = 1;
3896 0 : line->acc_idx = UINT_MAX;
3897 0 : line->key.generation = UINT_MAX;
3898 0 : line->refcnt = 0;
3899 0 : cache_free_push( accdb, c, line );
3900 0 : evicted++;
3901 0 : }
3902 0 : }
3903 3375590 : }
3904 :
3905 : int
3906 : fd_accdb_snapshot_write_one( fd_accdb_t * accdb,
3907 : fd_accdb_fork_id_t fork_id,
3908 : uchar const * pubkey,
3909 : ulong slot,
3910 : ulong lamports,
3911 : ulong data_len,
3912 : int executable,
3913 69 : ulong * out_replaced_lamports ) {
3914 : /* Snapshot slots are stored in the 32-bit cache_idx scratch field
3915 : during loading. Reject anything that would truncate. */
3916 69 : if( FD_UNLIKELY( slot>UINT_MAX ) ) FD_LOG_ERR(( "snapshot slot %lu exceeds 2^32-1, accdb format must be widened", slot ));
3917 :
3918 69 : int incremental = fork_id.val!=USHORT_MAX;
3919 :
3920 69 : fd_accdb_fork_t * fork = NULL;
3921 69 : uint fork_gen = 0U;
3922 69 : if( FD_UNLIKELY( incremental ) ) {
3923 33 : fork = &accdb->fork_pool[ fork_id.val ];
3924 33 : fork_gen = fork->shmem->generation;
3925 33 : }
3926 :
3927 69 : ulong hash = fd_accdb_hash( pubkey, accdb->shmem->seed )&(accdb->shmem->chain_cnt-1UL);
3928 :
3929 69 : *out_replaced_lamports = 0UL;
3930 :
3931 69 : fd_accdb_accmeta_t * accmeta = NULL;
3932 69 : int cross_fork = 0; /* incremental only: existing entry from different fork */
3933 :
3934 69 : ulong next_acc = accdb->acc_map[ hash ];
3935 75 : while( next_acc!=UINT_MAX ) {
3936 12 : fd_accdb_accmeta_t * candidate_acc = &accdb->acc_pool[ next_acc ];
3937 12 : if( FD_UNLIKELY( !memcmp( pubkey, candidate_acc->key.pubkey, 32UL ) ) ) {
3938 6 : if( FD_LIKELY( (ulong)candidate_acc->cache_idx>slot ) ) {
3939 : /* Still advance the write head so snapwr and snapin stay in
3940 : sync — snapwr unconditionally writes every account to disk.
3941 : Mark the space as immediately freed since it is dead on
3942 : arrival. */
3943 0 : ulong dead_sz = sizeof(fd_accdb_disk_meta_t)+data_len;
3944 0 : ulong dead_off = allocate_next_write( accdb, dead_sz );
3945 0 : fd_accdb_shmem_bytes_freed( accdb->shmem, dead_off, dead_sz );
3946 0 : return -1;
3947 0 : }
3948 6 : if( FD_UNLIKELY( incremental ) && candidate_acc->key.generation!=fork_gen ) {
3949 : /* Cross-snapshot override: don't replace in-place; insert a
3950 : new entry alongside the old one so purge can revert. */
3951 6 : cross_fork = 1;
3952 6 : *out_replaced_lamports = candidate_acc->lamports;
3953 6 : } else {
3954 : /* Same-fork duplicate (or full-snapshot mode): replace in-place */
3955 0 : accmeta = candidate_acc;
3956 0 : }
3957 6 : break;
3958 6 : }
3959 6 : next_acc = candidate_acc->map.next;
3960 6 : }
3961 :
3962 69 : int replace = !!accmeta;
3963 :
3964 69 : if( FD_UNLIKELY( !accmeta ) ) {
3965 69 : accmeta = acc_pool_acquire_nolock( accdb->acc_pool_join );
3966 69 : if( FD_UNLIKELY( !accmeta ) ) FD_LOG_ERR(( "accounts database ran out of space during snapshot loading, increase [accounts.max_accounts], current value is %lu", acc_pool_ele_max( accdb->acc_pool_join ) ));
3967 :
3968 69 : uint acc_idx = (uint)acc_pool_idx( accdb->acc_pool_join, accmeta );
3969 :
3970 69 : fd_memcpy( accmeta->key.pubkey, pubkey, 32UL );
3971 69 : if( FD_UNLIKELY( !incremental && accdb->shmem->root_fork_id.val==USHORT_MAX ) ) {
3972 0 : FD_LOG_ERR(( "snapshot_write_one called without a root fork attached" ));
3973 0 : }
3974 69 : accmeta->key.generation = incremental ? fork_gen : accdb->fork_pool[ accdb->shmem->root_fork_id.val ].shmem->generation;
3975 69 : accmeta->map.next = accdb->acc_map[ hash ];
3976 69 : accdb->acc_map[ hash ] = acc_idx;
3977 :
3978 : /* In incremental mode, record this insert in the fork's txn list
3979 : so purge can find and unlink it on failure. */
3980 69 : if( FD_UNLIKELY( incremental ) ) {
3981 33 : fd_accdb_txn_t * txn = txn_pool_acquire( accdb->txn_pool );
3982 33 : if( FD_UNLIKELY( !txn ) ) FD_LOG_ERR(( "txn pool exhausted during incremental snapshot loading" ));
3983 33 : txn->acc_map_idx = (uint)hash;
3984 33 : txn->acc_pool_idx = acc_idx;
3985 33 : uint txn_idx = (uint)txn_pool_idx( accdb->txn_pool, txn );
3986 33 : txn->fork.next = fork->shmem->txn_head;
3987 33 : fork->shmem->txn_head = txn_idx;
3988 33 : }
3989 69 : }
3990 :
3991 69 : if( FD_UNLIKELY( replace ) ) {
3992 : /* The old version's disk space is now dead. */
3993 0 : ulong old_sz = sizeof(fd_accdb_disk_meta_t) + FD_ACCDB_SIZE_DATA( accmeta->executable_size );
3994 0 : fd_accdb_shmem_bytes_freed( accdb->shmem, fd_accdb_acc_offset( accmeta ), old_sz );
3995 0 : accdb->shmem->shmetrics->disk_used_bytes -= old_sz;
3996 0 : *out_replaced_lamports = accmeta->lamports;
3997 0 : }
3998 :
3999 69 : accmeta->cache_idx = (uint)slot;
4000 69 : accmeta->lamports = lamports;
4001 69 : accmeta->executable_size = FD_ACCDB_SIZE_PACK( (uint)data_len, executable );
4002 69 : ulong entry_sz = sizeof(fd_accdb_disk_meta_t)+data_len;
4003 69 : ulong file_off = allocate_next_write( accdb, entry_sz );
4004 69 : accmeta->offset_fork = incremental ? fd_accdb_acc_pack_offset_fork( file_off, fork_id.val ) : file_off;
4005 69 : accdb->shmem->shmetrics->disk_used_bytes += entry_sz;
4006 69 : if( !replace ) accdb->shmem->shmetrics->accounts_total++;
4007 :
4008 69 : return ( replace || cross_fork ) ? 2 : 1;
4009 69 : }
4010 :
4011 : int
4012 : fd_accdb_snapshot_write_batch( fd_accdb_t * accdb,
4013 : fd_accdb_fork_id_t fork_id,
4014 : ulong cnt,
4015 : uchar const * const pubkeys[],
4016 : ulong const slots[],
4017 : ulong const lamports[],
4018 : ulong const data_lens[],
4019 : int const executables[],
4020 : ulong * accounts_ignored,
4021 : ulong * accounts_replaced,
4022 : ulong * accounts_loaded,
4023 : ulong * out_replaced_lamports,
4024 12 : ulong * out_ignored_lamports ) {
4025 12 : int incremental = fork_id.val!=USHORT_MAX;
4026 :
4027 12 : fd_accdb_fork_t * fork = NULL;
4028 12 : uint fork_gen = 0U;
4029 12 : if( FD_UNLIKELY( incremental ) ) {
4030 3 : fork = &accdb->fork_pool[ fork_id.val ];
4031 3 : fork_gen = fork->shmem->generation;
4032 3 : }
4033 :
4034 12 : ulong seed = accdb->shmem->seed;
4035 12 : ulong chain_msk = accdb->shmem->chain_cnt - 1UL;
4036 12 : if( FD_UNLIKELY( !incremental && accdb->shmem->root_fork_id.val==USHORT_MAX ) ) {
4037 0 : FD_LOG_ERR(( "snapshot_write_batch called without a root fork attached" ));
4038 0 : }
4039 12 : uint gen = incremental ? 0U : accdb->fork_pool[ accdb->shmem->root_fork_id.val ].shmem->generation;
4040 :
4041 12 : ulong ignored = 0UL;
4042 12 : ulong replaced = 0UL;
4043 12 : ulong loaded = 0UL;
4044 12 : ulong cross_replaced = 0UL; /* cross-fork overrides (subset of replaced) */
4045 12 : ulong replaced_lamports = 0UL;
4046 12 : ulong ignored_lamports = 0UL;
4047 :
4048 : /* Snapshot slots are stored in the 32-bit cache_idx scratch field
4049 : during loading. Reject anything that would truncate. */
4050 42 : for( ulong i=0UL; i<cnt; i++ ) {
4051 30 : if( FD_UNLIKELY( slots[ i ]>UINT_MAX ) ) FD_LOG_ERR(( "snapshot slot %lu exceeds 2^32-1, accdb format must be widened", slots[ i ] ));
4052 30 : }
4053 :
4054 : /* Phase 1: compute hashes and prefetch chain heads. */
4055 :
4056 12 : ulong hashes[ 8 ];
4057 12 : fd_accdb_accmeta_t * existing[ 8 ]; /* same-fork dup or full-snapshot replace */
4058 12 : fd_accdb_accmeta_t * cross_existing[ 8 ]; /* cross-fork dup (incremental only) */
4059 12 : int skip[ 8 ];
4060 :
4061 42 : for( ulong i=0UL; i<cnt; i++ ) {
4062 30 : hashes[ i ] = fd_accdb_hash( pubkeys[ i ], seed ) & chain_msk;
4063 30 : existing[ i ] = NULL;
4064 30 : cross_existing[ i ] = NULL;
4065 30 : skip[ i ] = 0;
4066 :
4067 : /* Prefetch the chain head and first pool element on the chain */
4068 30 : __builtin_prefetch( &accdb->acc_map[ hashes[ i ] ], 1, 1 );
4069 30 : }
4070 :
4071 : /* Phase 2: walk chains looking for duplicates. By now the chain
4072 : heads prefetched above should be warm in L1/L2. If the existing
4073 : entry has a higher slot, mark skip. Otherwise, save the existing
4074 : entry pointer for in-place update (matching write_one semantics).
4075 : In incremental mode, cross-fork entries are saved separately so
4076 : they can be left in place while a new entry is inserted. */
4077 :
4078 42 : for( ulong i=0UL; i<cnt; i++ ) {
4079 30 : ulong next_acc = accdb->acc_map[ hashes[ i ] ];
4080 :
4081 30 : if( FD_LIKELY( next_acc!=UINT_MAX ) ) {
4082 9 : __builtin_prefetch( &accdb->acc_pool[ next_acc ], 0, 1 );
4083 9 : }
4084 :
4085 30 : while( next_acc!=UINT_MAX ) {
4086 9 : fd_accdb_accmeta_t * candidate = &accdb->acc_pool[ next_acc ];
4087 :
4088 9 : if( FD_LIKELY( candidate->map.next!=UINT_MAX ) ) {
4089 0 : __builtin_prefetch( &accdb->acc_pool[ candidate->map.next ], 0, 1 );
4090 0 : }
4091 :
4092 9 : if( FD_UNLIKELY( !memcmp( pubkeys[ i ], candidate->key.pubkey, 32UL ) ) ) {
4093 9 : if( FD_LIKELY( (ulong)candidate->cache_idx>slots[ i ] ) ) {
4094 3 : skip[ i ] = 1;
4095 6 : } else if( FD_UNLIKELY( incremental ) && candidate->key.generation!=fork_gen ) {
4096 3 : cross_existing[ i ] = candidate;
4097 3 : } else {
4098 3 : existing[ i ] = candidate;
4099 3 : }
4100 9 : break;
4101 9 : }
4102 0 : next_acc = candidate->map.next;
4103 0 : }
4104 30 : }
4105 :
4106 : /* Phase 2b: reject intra-batch duplicate pubkeys. Snapin always
4107 : populates a batch from a single AppendVec, so every slot in the
4108 : batch is identical and a duplicate pubkey means the same account
4109 : appears twice at the same slot — i.e. a corrupt snapshot per the
4110 : Agave spec. We have no principled way to pick a winner; return
4111 : -1 so the caller can flag the snapshot malformed. Batches are
4112 : bounded (<=8) so the O(n^2) scan is trivial. */
4113 :
4114 30 : for( ulong i=1UL; i<cnt; i++ ) {
4115 45 : for( ulong j=0UL; j<i; j++ ) {
4116 27 : if( hashes[ j ]!=hashes[ i ] ) continue;
4117 0 : if( FD_UNLIKELY( !memcmp( pubkeys[ j ], pubkeys[ i ], 32UL ) ) ) {
4118 0 : FD_LOG_WARNING(( "corrupt snapshot: duplicate pubkey within a single batch (entries %lu and %lu, slots %lu and %lu)", j, i, slots[ j ], slots[ i ] ));
4119 0 : return -1;
4120 0 : }
4121 0 : }
4122 18 : }
4123 :
4124 : /* Phase 3: commit. For each account either update the existing
4125 : entry in-place (replace), allocate and insert at the chain head
4126 : (new), or skip entirely (ignore). This matches the
4127 : insert/replace/ignore semantics of write_one. */
4128 :
4129 12 : ulong used_bytes_added = 0UL;
4130 12 : ulong used_bytes_removed = 0UL;
4131 :
4132 42 : for( ulong i=0UL; i<cnt; i++ ) {
4133 30 : if( FD_UNLIKELY( skip[ i ] ) ) {
4134 : /* Still advance the write head so snapwr and snapin stay in
4135 : sync — snapwr unconditionally writes every account to disk.
4136 : Mark the space as immediately freed since it is dead on
4137 : arrival. */
4138 3 : ulong dead_sz = sizeof(fd_accdb_disk_meta_t)+data_lens[ i ];
4139 3 : ulong dead_off = allocate_next_write( accdb, dead_sz );
4140 3 : fd_accdb_shmem_bytes_freed( accdb->shmem, dead_off, dead_sz );
4141 3 : ignored_lamports += lamports[ i ];
4142 3 : ignored++;
4143 3 : continue;
4144 3 : }
4145 :
4146 27 : fd_accdb_accmeta_t * accmeta;
4147 :
4148 27 : if( FD_UNLIKELY( existing[ i ] ) ) {
4149 3 : accmeta = existing[ i ];
4150 : /* The old version's disk space is now dead. */
4151 3 : ulong old_sz = sizeof(fd_accdb_disk_meta_t) + FD_ACCDB_SIZE_DATA( accmeta->executable_size );
4152 3 : fd_accdb_shmem_bytes_freed( accdb->shmem, fd_accdb_acc_offset( accmeta ), old_sz );
4153 3 : used_bytes_removed += old_sz;
4154 3 : replaced_lamports += accmeta->lamports;
4155 3 : replaced++;
4156 24 : } else {
4157 24 : accmeta = acc_pool_acquire_nolock( accdb->acc_pool_join );
4158 24 : if( FD_UNLIKELY( !accmeta ) ) FD_LOG_ERR(( "accounts database ran out of space during snapshot loading" ));
4159 :
4160 24 : uint acc_idx = (uint)acc_pool_idx( accdb->acc_pool_join, accmeta );
4161 :
4162 24 : fd_memcpy( accmeta->key.pubkey, pubkeys[ i ], 32UL );
4163 24 : accmeta->key.generation = incremental ? fork_gen : gen;
4164 24 : accmeta->map.next = accdb->acc_map[ hashes[ i ] ];
4165 24 : accdb->acc_map[ hashes[ i ] ] = acc_idx;
4166 :
4167 24 : if( FD_UNLIKELY( incremental ) ) {
4168 6 : fd_accdb_txn_t * txn = txn_pool_acquire( accdb->txn_pool );
4169 6 : if( FD_UNLIKELY( !txn ) ) FD_LOG_ERR(( "txn pool exhausted during incremental snapshot loading" ));
4170 6 : txn->acc_map_idx = (uint)hashes[ i ];
4171 6 : txn->acc_pool_idx = acc_idx;
4172 6 : uint txn_idx = (uint)txn_pool_idx( accdb->txn_pool, txn );
4173 6 : txn->fork.next = fork->shmem->txn_head;
4174 6 : fork->shmem->txn_head = txn_idx;
4175 6 : }
4176 :
4177 24 : if( cross_existing[ i ] ) {
4178 3 : replaced_lamports += cross_existing[ i ]->lamports;
4179 3 : replaced++;
4180 3 : cross_replaced++;
4181 21 : } else {
4182 21 : loaded++;
4183 21 : }
4184 24 : }
4185 :
4186 27 : accmeta->cache_idx = (uint)slots[ i ];
4187 27 : accmeta->lamports = lamports[ i ];
4188 27 : accmeta->executable_size = FD_ACCDB_SIZE_PACK( (uint)data_lens[ i ], executables[ i ] );
4189 27 : ulong entry_sz = sizeof(fd_accdb_disk_meta_t)+data_lens[ i ];
4190 27 : ulong file_off = allocate_next_write( accdb, entry_sz );
4191 27 : accmeta->offset_fork = incremental ? fd_accdb_acc_pack_offset_fork( file_off, fork_id.val ) : file_off;
4192 27 : used_bytes_added += entry_sz;
4193 27 : }
4194 :
4195 12 : accdb->shmem->shmetrics->disk_used_bytes += used_bytes_added;
4196 12 : accdb->shmem->shmetrics->disk_used_bytes -= used_bytes_removed;
4197 :
4198 : /* accounts_total tracks acc_pool entries: increment for every new
4199 : allocation (both genuinely new accounts and cross-fork overrides
4200 : that insert a second pool entry). The output counter
4201 : *accounts_loaded excludes cross-fork overrides to match
4202 : snapshot_write_one semantics (cross-fork returns 2 = replaced). */
4203 12 : accdb->shmem->shmetrics->accounts_total += loaded + cross_replaced;
4204 :
4205 12 : *accounts_ignored = ignored;
4206 12 : *accounts_replaced = replaced;
4207 12 : *accounts_loaded = loaded;
4208 12 : *out_replaced_lamports = replaced_lamports;
4209 12 : *out_ignored_lamports = ignored_lamports;
4210 :
4211 12 : return 0;
4212 12 : }
4213 :
4214 : static void
4215 0 : delta_reset( fd_accdb_t * accdb ) {
4216 0 : if( !accdb->shmem->delta.head ) return; /* clean */
4217 0 : uint * chains = accdb->delta.chains;
4218 0 : ulong chain_cnt = accdb->shmem->delta.chain_cnt;
4219 0 : for( ulong i=0UL; i<chain_cnt; i++ ) chains[ i ] = UINT_MAX;
4220 0 : accdb->shmem->delta.head = 0UL;
4221 0 : }
4222 :
4223 : static int
4224 0 : delta_is_valid( fd_accdb_shmem_t const * accdb ) {
4225 0 : return accdb->delta.head < accdb->delta.ele_max;
4226 0 : }
4227 :
4228 : void
4229 : fd_accdb_background( fd_accdb_t * accdb,
4230 3376115 : int * charge_busy ) {
4231 3376115 : fd_accdb_shmem_t * shmem = accdb->shmem;
4232 :
4233 3376115 : ulong * snap_sync_p = &shmem->snapshot_sync;
4234 3376115 : ulong snap_sync = fd_accdb_snapshot_sync_state( snap_sync_p );
4235 :
4236 3376115 : uint op = FD_VOLATILE_CONST( shmem->cmd_op );
4237 3376115 : if( FD_UNLIKELY( op!=FD_ACCDB_CMD_IDLE ) ) {
4238 525 : fd_accdb_fork_id_t fork_id = { .val = FD_VOLATILE_CONST( shmem->cmd_fork_id ) };
4239 :
4240 525 : switch( op ) {
4241 498 : case FD_ACCDB_CMD_ADVANCE_ROOT:
4242 498 : background_advance_root( accdb, fork_id );
4243 498 : break;
4244 21 : case FD_ACCDB_CMD_PURGE:
4245 21 : background_purge( accdb, fork_id );
4246 21 : break;
4247 3 : case FD_ACCDB_CMD_DRAIN_DEFERRED:
4248 3 : drain_deferred_frees( accdb );
4249 3 : break;
4250 3 : case FD_ACCDB_CMD_CLEAR_DEFERRED: {
4251 : /* Posted by fd_accdb_reset after it clobbers shared pools.
4252 : T2's deferred fork chain now points at recycled elements;
4253 : discard the stale pointers. Epoch slots are preserved
4254 : across reset so no re-join is needed. */
4255 3 : accdb->deferred_fork_head = NULL;
4256 3 : accdb->deferred_fork_tail = NULL;
4257 3 : accdb->deferred_fork_epoch = 0UL;
4258 3 : break;
4259 0 : }
4260 0 : default:
4261 0 : FD_LOG_ERR(( "unexpected accdb cmd_op %u", op ));
4262 525 : }
4263 :
4264 525 : FD_COMPILER_MFENCE();
4265 525 : FD_VOLATILE( shmem->cmd_op ) = FD_ACCDB_CMD_IDLE;
4266 525 : *charge_busy = 1;
4267 525 : return;
4268 525 : }
4269 :
4270 3375590 : if( FD_UNLIKELY( snap_sync!=FD_ACCDB_SNAPSHOT_SYNC_IDLE ) ) {
4271 0 : switch( snap_sync ) {
4272 0 : case FD_ACCDB_SNAPSHOT_SYNC_RUNNING:
4273 : /* while producing a snapshot, don't do compaction work */
4274 0 : background_preevict( accdb, charge_busy, 0 );
4275 0 : return;
4276 0 : case FD_ACCDB_SNAPSHOT_SYNC_DONE:
4277 0 : fd_accdb_snapshot_sync_advance( snap_sync_p, FD_ACCDB_SNAPSHOT_SYNC_IDLE );
4278 0 : break;
4279 0 : case FD_ACCDB_SNAPSHOT_SYNC_START_FULL:
4280 0 : delta_reset( accdb );
4281 0 : fd_accdb_snapshot_sync_advance( snap_sync_p, FD_ACCDB_SNAPSHOT_SYNC_RUNNING );
4282 0 : *charge_busy = 1;
4283 0 : return;
4284 0 : case FD_ACCDB_SNAPSHOT_SYNC_START_INCR:
4285 0 : if( delta_is_valid( accdb->shmem ) ) {
4286 0 : fd_accdb_snapshot_sync_advance( snap_sync_p, FD_ACCDB_SNAPSHOT_SYNC_RUNNING );
4287 0 : } else {
4288 : /* cannot produce incrementals because delta ran out of space,
4289 : therefore don't know which accounts changed */
4290 0 : fd_accdb_snapshot_sync_advance( snap_sync_p, FD_ACCDB_SNAPSHOT_SYNC_FAIL );
4291 0 : }
4292 0 : *charge_busy = 1;
4293 0 : return;
4294 0 : case FD_ACCDB_SNAPSHOT_SYNC_FAIL:
4295 : /* wait for client to acknowledge */
4296 0 : break;
4297 0 : default:
4298 0 : FD_LOG_CRIT(( "corrupt snapshot_sync state %lu", snap_sync ));
4299 0 : }
4300 0 : }
4301 :
4302 3375590 : background_preevict( accdb, charge_busy, 0 );
4303 :
4304 13502360 : for( ulong k=0UL; k<FD_ACCDB_COMPACTION_LAYER_CNT; k++ ) {
4305 10126770 : background_compact( accdb, k, charge_busy );
4306 10126770 : }
4307 3375590 : }
4308 :
4309 : fd_accdb_shmem_metrics_t const *
4310 57 : fd_accdb_shmetrics( fd_accdb_t * accdb ) {
4311 57 : return accdb->shmem->shmetrics;
4312 57 : }
4313 :
4314 : fd_accdb_metrics_t const *
4315 9 : fd_accdb_metrics( fd_accdb_t * accdb ) {
4316 9 : return accdb->metrics;
4317 9 : }
4318 :
4319 : void
4320 : fd_accdb_cache_class_occupancy( fd_accdb_t * accdb,
4321 : ulong * used,
4322 : ulong * max,
4323 12 : ulong * reserved ) {
4324 108 : for( ulong c=0UL; c<FD_ACCDB_CACHE_CLASS_CNT; c++ ) {
4325 96 : ulong cap = accdb->shmem->cache_class_max[ c ];
4326 96 : ulong init = FD_VOLATILE_CONST( accdb->shmem->cache_class_init[ c ].val );
4327 96 : ulong freec = FD_VOLATILE_CONST( accdb->shmem->cache_free_cnt [ c ].val );
4328 96 : ulong live = init>freec ? init-freec : 0UL;
4329 96 : if( live>cap ) live = cap;
4330 96 : max [ c ] = cap;
4331 96 : used [ c ] = live;
4332 96 : reserved[ c ] = FD_VOLATILE_CONST( accdb->shmem->cache_class_used[ c ].val );
4333 96 : }
4334 12 : }
4335 :
4336 : void
4337 : fd_accdb_cache_class_thresholds( fd_accdb_t * accdb,
4338 : ulong * target_used,
4339 0 : ulong * low_water_used ) {
4340 0 : for( ulong c=0UL; c<FD_ACCDB_CACHE_CLASS_CNT; c++ ) {
4341 0 : ulong max_c = accdb->shmem->cache_class_max [ c ];
4342 0 : ulong free_tgt = accdb->shmem->cache_free_target [ c ];
4343 0 : ulong free_lwm = accdb->shmem->cache_free_low_water[ c ];
4344 0 : target_used [ c ] = max_c>free_tgt ? max_c-free_tgt : 0UL;
4345 0 : low_water_used[ c ] = max_c>free_lwm ? max_c-free_lwm : 0UL;
4346 0 : }
4347 0 : }
4348 :
4349 : #if FD_HAS_RACESAN
4350 :
4351 : /* Force pre-eviction (ignore the watermark) so a deterministic
4352 : single-threaded test can exercise the writeback path without
4353 : manufacturing real cache pressure. Sweeps several times: CLOCK needs
4354 : two visits to evict a recently-touched line (clear the "referenced"
4355 : bit, then evict), and the clock hand position carries across calls, so
4356 : one or two sweeps is not enough to guarantee every eligible line is
4357 : flushed back. */
4358 : void
4359 : fd_accdb_debug_force_preevict( fd_accdb_t * accdb ) {
4360 : for( ulong iter=0UL; iter<8UL; iter++ ) {
4361 : int charge_busy = 0;
4362 : background_preevict( accdb, &charge_busy, 1 );
4363 : }
4364 : }
4365 :
4366 : /* Locate the resident cache line currently holding `pubkey` (most recent
4367 : generation if multiple). Returns 1 and fills out_class/out_idx on a
4368 : hit, 0 if no resident line matches. Test-only helper so the test can
4369 : target a specific line without seeing the opaque fd_accdb struct. */
4370 :
4371 : int
4372 : fd_accdb_debug_find_line( fd_accdb_t * accdb,
4373 : uchar const * pubkey,
4374 : ulong * out_class,
4375 : ulong * out_idx ) {
4376 : int found = 0;
4377 : uint best_gen = 0U;
4378 : for( ulong c=0UL; c<FD_ACCDB_CACHE_CLASS_CNT; c++ ) {
4379 : ulong init = FD_VOLATILE_CONST( accdb->shmem->cache_class_init[ c ].val );
4380 : ulong max_c = accdb->shmem->cache_class_max[ c ];
4381 : if( init>max_c ) init = max_c;
4382 : for( ulong idx=0UL; idx<init; idx++ ) {
4383 : fd_accdb_cache_line_t * line = cache_line( accdb, c, idx );
4384 : if( line->key.generation==UINT_MAX ) continue;
4385 : if( memcmp( line->key.pubkey, pubkey, 32UL ) ) continue;
4386 : if( !found || line->key.generation>=best_gen ) {
4387 : best_gen = line->key.generation;
4388 : *out_class = c;
4389 : *out_idx = idx;
4390 : found = 1;
4391 : }
4392 : }
4393 : }
4394 : return found;
4395 : }
4396 :
4397 : /* Deterministically evict a single specified cache line via the
4398 : foreground evictor's claim sequence (CAS refcnt 0->EVICT_SENTINEL),
4399 : then write the dirty line back exactly as fd_accdb_acquire_inner's
4400 : STEP-4 / background_ preevict do (pubkey from accmeta, owner+data
4401 : from the line). Mirrors acquire_cache_line's CLOCK-claim path
4402 : (fd_accdb.c) so a racesan test can reproduce, without a 640+-slot
4403 : cache-pressure rig, the interleaving where acc_unlink observes
4404 : EVICT_SENTINEL on the line it is unlinking.
4405 :
4406 : The fd_racesan_hook("clock_evict:post_sentinel") fires right after
4407 : the sentinel is installed (matching the production foreground path),
4408 : so the test can suspend this fiber holding the sentinel while another
4409 : fiber drives acc_unlink to its reclaim CAS. Returns the captured
4410 : evicted acc_idx (UINT_MAX if the line was clean / unbound). */
4411 :
4412 : uint
4413 : fd_accdb_debug_clock_evict_line( fd_accdb_t * accdb,
4414 : ulong size_class,
4415 : ulong line_idx ) {
4416 : fd_accdb_shmem_t * shmem = accdb->shmem;
4417 : fd_accdb_cache_line_t * line = cache_line( accdb, size_class, line_idx );
4418 :
4419 : /* Claim for eviction, same as acquire_cache_line's CLOCK path. */
4420 : if( FD_UNLIKELY( FD_ATOMIC_CAS( &line->refcnt, 0U, FD_ACCDB_EVICT_SENTINEL )!=0U ) ) return UINT_MAX;
4421 :
4422 : fd_racesan_hook( "clock_evict:post_sentinel" );
4423 :
4424 : uint acc_idx = line->acc_idx;
4425 : if( FD_LIKELY( acc_idx!=UINT_MAX ) ) {
4426 : evict_clear_acc_cache_ref( &accdb->acc_pool[ acc_idx ], size_class, line_idx );
4427 : }
4428 : uint evicted_acc_idx = line->persisted ? UINT_MAX : acc_idx;
4429 : line->key.generation = UINT_MAX;
4430 :
4431 : /* Write back the dirty line, exactly like the production writeback
4432 : sites: this is the synthesis that would emit a pubkey=NEW/owner=OLD
4433 : poison record if the accmeta slot had been recycled out from under
4434 : us. In the SENTINEL-vs-acc_unlink race this proves no poison: the
4435 : epoch the evictor holds blocks drain_deferred_frees, so the slot is
4436 : never recycled while we are here. */
4437 : if( FD_UNLIKELY( !line->persisted && acc_idx!=UINT_MAX ) ) {
4438 : fd_accdb_accmeta_t * accmeta = &accdb->acc_pool[ acc_idx ];
4439 : ulong entry_sz = sizeof(fd_accdb_disk_meta_t)+(ulong)FD_ACCDB_SIZE_DATA( accmeta->executable_size );
4440 :
4441 : ulong old_offset = fd_accdb_acc_xchg_offset( accmeta, FD_ACCDB_OFF_INVAL );
4442 : if( FD_LIKELY( old_offset!=FD_ACCDB_OFF_INVAL ) ) {
4443 : fd_accdb_shmem_bytes_freed( shmem, old_offset, entry_sz );
4444 : FD_ATOMIC_FETCH_AND_SUB( &shmem->shmetrics->disk_used_bytes, entry_sz );
4445 : }
4446 :
4447 : fd_accdb_disk_meta_t meta;
4448 : fd_memcpy( meta.pubkey, accmeta->key.pubkey, 32UL );
4449 : meta.size = FD_ACCDB_SIZE_DATA( accmeta->executable_size );
4450 : fd_memcpy( meta.owner, line->owner, 32UL );
4451 :
4452 : struct iovec iovs[ 2UL ] = {
4453 : { .iov_base = &meta, .iov_len = sizeof(fd_accdb_disk_meta_t) },
4454 : { .iov_base = (void *)(line+1UL), .iov_len = FD_ACCDB_SIZE_DATA( accmeta->executable_size ) }
4455 : };
4456 : ulong file_off = allocate_next_write( accdb, entry_sz );
4457 : ulong written = 0UL;
4458 : while( written<entry_sz ) {
4459 : long result = pwritev2( accdb->fd, iovs, 2, (long)(file_off+written), 0 );
4460 : if( FD_UNLIKELY( result==-1 && errno==EINTR ) ) continue;
4461 : else if( FD_UNLIKELY( result<=0 ) ) FD_LOG_ERR(( "pwritev2() failed (%d-%s)", errno, fd_io_strerror( errno ) ));
4462 : written += (ulong)result;
4463 : for( int v=0; v<2; v++ ) {
4464 : if( (ulong)result>=iovs[ v ].iov_len ) { result -= (long)iovs[ v ].iov_len; iovs[ v ].iov_len = 0UL; }
4465 : else { iovs[ v ].iov_base = (uchar *)iovs[ v ].iov_base + result; iovs[ v ].iov_len -= (ulong)result; break; }
4466 : }
4467 : }
4468 : FD_COMPILER_MFENCE();
4469 : accmeta->offset_fork = fd_accdb_acc_pack_offset_fork( file_off, fd_accdb_acc_fork_id(accmeta) );
4470 : FD_ATOMIC_FETCH_AND_ADD( &shmem->shmetrics->disk_used_bytes, entry_sz );
4471 : }
4472 :
4473 : line->persisted = 1;
4474 : line->acc_idx = UINT_MAX;
4475 : line->key.generation = UINT_MAX;
4476 : line->refcnt = 0;
4477 : cache_free_push( accdb, size_class, line );
4478 : return evicted_acc_idx;
4479 : }
4480 :
4481 : #endif
|