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