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