Skip to content

Commit b92ff14

Browse files
twoethswemeetagain
andauthored
feat: implement new state caches (#6176)
* feat: implement LRUBlockStateCache * feat: implement PersistentCheckpointStateCache * feat: implement findSeedStateToReload * fix: add missing catch() * fix: import path in state-transition * fix: model CacheItem and type in PersistentCheckpointStateCache * refactor: use for loop in PersistentCheckpointStateCache.processState * chore: move test code to beforeAll() in persistentCheckpointsCache.test.ts * feat: do not prune persisted state when reload * fix: fifo instead of lru BlockStateCache * fix: do not prune the last added item in FIFOBlockStateCache * fix: sync epochIndex and cache in PersistentCheckpointStateCache * chore: cleanup persistent checkpoint cache types * chore: tweak comments * chore: tweak more comments * chore: reword persistent apis * chore: add type to cpStateCache size metrics * fix: metrics labels after rebasing from unstable --------- Co-authored-by: Cayman <[email protected]>
1 parent 9262064 commit b92ff14

File tree

25 files changed

+2402
-15
lines changed

25 files changed

+2402
-15
lines changed

packages/beacon-node/src/chain/shufflingCache.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,23 @@ export class ShufflingCache {
167167
}
168168
}
169169

170+
/**
171+
* Same to get() function but synchronous.
172+
*/
173+
getSync(shufflingEpoch: Epoch, decisionRootHex: RootHex): EpochShuffling | null {
174+
const cacheItem = this.itemsByDecisionRootByEpoch.getOrDefault(shufflingEpoch).get(decisionRootHex);
175+
if (cacheItem === undefined) {
176+
return null;
177+
}
178+
179+
if (isShufflingCacheItem(cacheItem)) {
180+
return cacheItem.shuffling;
181+
}
182+
183+
// ignore promise
184+
return null;
185+
}
186+
170187
private add(shufflingEpoch: Epoch, decisionBlock: RootHex, cacheItem: CacheItem): void {
171188
this.itemsByDecisionRootByEpoch.getOrDefault(shufflingEpoch).set(decisionBlock, cacheItem);
172189
pruneSetToMax(this.itemsByDecisionRootByEpoch, this.maxEpochs);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
2+
import {phase0, ssz} from "@lodestar/types";
3+
import {IBeaconDb} from "../../../db/interface.js";
4+
import {CPStateDatastore, DatastoreKey} from "./types.js";
5+
6+
/**
7+
* Implementation of CPStateDatastore using db.
8+
*/
9+
export class DbCPStateDatastore implements CPStateDatastore {
10+
constructor(private readonly db: IBeaconDb) {}
11+
12+
async write(cpKey: phase0.Checkpoint, state: CachedBeaconStateAllForks): Promise<DatastoreKey> {
13+
const serializedCheckpoint = checkpointToDatastoreKey(cpKey);
14+
const stateBytes = state.serialize();
15+
await this.db.checkpointState.putBinary(serializedCheckpoint, stateBytes);
16+
return serializedCheckpoint;
17+
}
18+
19+
async remove(serializedCheckpoint: DatastoreKey): Promise<void> {
20+
await this.db.checkpointState.delete(serializedCheckpoint);
21+
}
22+
23+
async read(serializedCheckpoint: DatastoreKey): Promise<Uint8Array | null> {
24+
return this.db.checkpointState.getBinary(serializedCheckpoint);
25+
}
26+
27+
async readKeys(): Promise<DatastoreKey[]> {
28+
return this.db.checkpointState.keys();
29+
}
30+
}
31+
32+
export function datastoreKeyToCheckpoint(key: DatastoreKey): phase0.Checkpoint {
33+
return ssz.phase0.Checkpoint.deserialize(key);
34+
}
35+
36+
export function checkpointToDatastoreKey(cp: phase0.Checkpoint): DatastoreKey {
37+
return ssz.phase0.Checkpoint.serialize(cp);
38+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./types.js";
2+
export * from "./db.js";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
2+
import {phase0} from "@lodestar/types";
3+
4+
// With db implementation, persistedKey is serialized data of a checkpoint
5+
export type DatastoreKey = Uint8Array;
6+
7+
// Make this generic to support testing
8+
export interface CPStateDatastore {
9+
write: (cpKey: phase0.Checkpoint, state: CachedBeaconStateAllForks) => Promise<DatastoreKey>;
10+
remove: (key: DatastoreKey) => Promise<void>;
11+
read: (key: DatastoreKey) => Promise<Uint8Array | null>;
12+
readKeys: () => Promise<DatastoreKey[]>;
13+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import {toHexString} from "@chainsafe/ssz";
2+
import {RootHex} from "@lodestar/types";
3+
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
4+
import {routes} from "@lodestar/api";
5+
import {Metrics} from "../../metrics/index.js";
6+
import {LinkedList} from "../../util/array.js";
7+
import {MapTracker} from "./mapMetrics.js";
8+
import {BlockStateCache} from "./types.js";
9+
10+
export type FIFOBlockStateCacheOpts = {
11+
maxBlockStates?: number;
12+
};
13+
14+
/**
15+
* Regen state if there's a reorg distance > 32 slots.
16+
*/
17+
export const DEFAULT_MAX_BLOCK_STATES = 32;
18+
19+
/**
20+
* New implementation of BlockStateCache that keeps the most recent n states consistently
21+
* - Maintain a linked list (FIFO) with special handling for head state, which is always the first item in the list
22+
* - Prune per add() instead of per checkpoint so it only keeps n historical states consistently, prune from tail
23+
* - No need to prune per finalized checkpoint
24+
*
25+
* Given this block tree with Block 11 as head:
26+
* ```
27+
Block 10
28+
|
29+
+-----+-----+
30+
| |
31+
Block 11 Block 12
32+
^ |
33+
| |
34+
head Block 13
35+
* ```
36+
* The maintained key order would be: 11 -> 13 -> 12 -> 10, and state 10 will be pruned first.
37+
*/
38+
export class FIFOBlockStateCache implements BlockStateCache {
39+
/**
40+
* Max number of states allowed in the cache
41+
*/
42+
readonly maxStates: number;
43+
44+
private readonly cache: MapTracker<string, CachedBeaconStateAllForks>;
45+
/**
46+
* Key order to implement FIFO cache
47+
*/
48+
private readonly keyOrder: LinkedList<string>;
49+
private readonly metrics: Metrics["stateCache"] | null | undefined;
50+
51+
constructor(opts: FIFOBlockStateCacheOpts, {metrics}: {metrics?: Metrics | null}) {
52+
this.maxStates = opts.maxBlockStates ?? DEFAULT_MAX_BLOCK_STATES;
53+
this.cache = new MapTracker(metrics?.stateCache);
54+
if (metrics) {
55+
this.metrics = metrics.stateCache;
56+
metrics.stateCache.size.addCollect(() => metrics.stateCache.size.set(this.cache.size));
57+
}
58+
this.keyOrder = new LinkedList();
59+
}
60+
61+
/**
62+
* Set a state as head, happens when importing a block and head block is changed.
63+
*/
64+
setHeadState(item: CachedBeaconStateAllForks | null): void {
65+
if (item !== null) {
66+
this.add(item, true);
67+
}
68+
}
69+
70+
/**
71+
* Get a state from this cache given a state root hex.
72+
*/
73+
get(rootHex: RootHex): CachedBeaconStateAllForks | null {
74+
this.metrics?.lookups.inc();
75+
const item = this.cache.get(rootHex);
76+
if (!item) {
77+
return null;
78+
}
79+
80+
this.metrics?.hits.inc();
81+
this.metrics?.stateClonedCount.observe(item.clonedCount);
82+
83+
return item;
84+
}
85+
86+
/**
87+
* Add a state to this cache.
88+
* @param isHead if true, move it to the head of the list. Otherwise add to the 2nd position.
89+
* In importBlock() steps, normally it'll call add() with isHead = false first. Then call setHeadState() to set the head.
90+
*/
91+
add(item: CachedBeaconStateAllForks, isHead = false): void {
92+
const key = toHexString(item.hashTreeRoot());
93+
if (this.cache.get(key) != null) {
94+
if (!this.keyOrder.has(key)) {
95+
throw Error(`State exists but key not found in keyOrder: ${key}`);
96+
}
97+
if (isHead) {
98+
this.keyOrder.moveToHead(key);
99+
} else {
100+
this.keyOrder.moveToSecond(key);
101+
}
102+
// same size, no prune
103+
return;
104+
}
105+
106+
// new state
107+
this.metrics?.adds.inc();
108+
this.cache.set(key, item);
109+
if (isHead) {
110+
this.keyOrder.unshift(key);
111+
} else {
112+
// insert after head
113+
const head = this.keyOrder.first();
114+
if (head == null) {
115+
// should not happen, however handle just in case
116+
this.keyOrder.unshift(key);
117+
} else {
118+
this.keyOrder.insertAfter(head, key);
119+
}
120+
}
121+
this.prune(key);
122+
}
123+
124+
get size(): number {
125+
return this.cache.size;
126+
}
127+
128+
/**
129+
* Prune the cache from tail to keep the most recent n states consistently.
130+
* The tail of the list is the oldest state, in case regen adds back the same state,
131+
* it should stay next to head so that it won't be pruned right away.
132+
* The FIFO cache helps with this.
133+
*/
134+
prune(lastAddedKey: string): void {
135+
while (this.keyOrder.length > this.maxStates) {
136+
const key = this.keyOrder.last();
137+
// it does not make sense to prune the last added state
138+
// this only happens when max state is 1 in a short period of time
139+
if (key === lastAddedKey) {
140+
break;
141+
}
142+
if (!key) {
143+
// should not happen
144+
throw new Error("No key");
145+
}
146+
this.keyOrder.pop();
147+
this.cache.delete(key);
148+
}
149+
}
150+
151+
/**
152+
* No need for this implementation
153+
* This is only to conform to the old api
154+
*/
155+
deleteAllBeforeEpoch(): void {}
156+
157+
/**
158+
* ONLY FOR DEBUGGING PURPOSES. For lodestar debug API.
159+
*/
160+
clear(): void {
161+
this.cache.clear();
162+
}
163+
164+
/** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */
165+
dumpSummary(): routes.lodestar.StateCacheItem[] {
166+
return Array.from(this.cache.entries()).map(([key, state]) => ({
167+
slot: state.slot,
168+
root: toHexString(state.hashTreeRoot()),
169+
reads: this.cache.readCount.get(key) ?? 0,
170+
lastRead: this.cache.lastRead.get(key) ?? 0,
171+
checkpointState: false,
172+
}));
173+
}
174+
175+
/**
176+
* For unit test only.
177+
*/
178+
dumpKeyOrder(): string[] {
179+
return this.keyOrder.toArray();
180+
}
181+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./stateContextCache.js";
22
export * from "./stateContextCheckpointsCache.js";
3+
export * from "./fifoBlockStateCache.js";

0 commit comments

Comments
 (0)