File size: 7,761 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// src/lifecycle-status.ts
//
// Structured per-open lifecycle telemetry: the state machine one open() walks
// through and the wall-clock cost of every lifecycle phase, so operators and
// benches can tell a published-generation attach apart from a legacy full
// recovery WITHOUT inferring it from the cumulative stats counters. One
// tracker instance lives exactly one open (a MiniDb instance opens once);
// lifecycle.ts, generation-loader.ts, recovery.ts (through a sink) and the
// deferred text-build task feed it, and the public read model is
// MiniDb.lifecycleStatus().

/** The lifecycle states one open() walks through. */
export type MiniDbLifecycleState =
  /** Nothing has been loaded: the initial state, and the state of an open
   *  that found no published generation to even attempt (a fresh/legacy
   *  database, or generations disabled). */
  | 'no-generation'
  /** Loading a published generation's images (store + derived indexes). */
  | 'generation-load'
  /** Replaying the WAL delta past the loaded generation's checkpoint. */
  | 'wal-catch-up'
  /** The legacy full recovery + open-time index rebuild is running (no usable
   *  generation, or every candidate failed validation). */
  | 'full-rebuild'
  /** open() completed and every index is servable. */
  | 'ready'
  /** open() completed but at least one text index's base is still being built
   *  by the deferred background task — searches on it raise
   *  TextIndexBuildingError until the build commits. */
  | 'degraded';

/** Wall-clock milliseconds per lifecycle phase of the LAST open (0 for a
 *  phase the open never entered). Phases are NOT strictly nested:
 *  generationCandidateLoadMs contains the image-load / postings-check /
 *  wal-catch-up phases of the generation path, fullRecoveryMs contains the
 *  full-recovery path's walScan/walApply (and its staged text commit also
 *  lands in textRebuildMs), and a deferred text build's textRebuildMs is
 *  recorded after open() returned, while the instance sits in 'degraded'. */
export interface OpenPhaseTimings {
  /** The whole open() call. */
  openMs: number;
  /** tryLoadGeneration across every candidate (manifest validation, image
   *  loads, WAL-delta replay, failed-candidate resets). */
  generationCandidateLoadMs: number;
  /** Store image read + decode + bulk load (generation path). */
  storeImageLoadMs: number;
  /** dt / secondary / compound image loads, including the per-index rebuild
   *  of a corrupt or definition-mismatched image (generation path). */
  nonTextImageLoadMs: number;
  /** Text dictionary/docs image reads + base attach (generation path). */
  textImageLoadMs: number;
  /** Whole-file postings crc32 verification (generation path). */
  postingsIntegrityCheckMs: number;
  /** Frame scanning: the generation path's WAL-delta scan, or the full
   *  recovery's snapshot + WAL scans (the async scanner's time only). */
  walScanMs: number;
  /** Applying scanned frames to the store and the derived indexes. */
  walApplyMs: number;
  /** The legacy full recovery + open-time index rebuild block (recover() +
   *  rebuildAllIndexes()); 0 when a generation served the open. */
  fullRecoveryMs: number;
  /** Corpus tokenization work: per-index rebuilds during a generation load
   *  (bounded worker/inline or staged), the full-rebuild walk's staged text
   *  commit, and — after open() returned, under 'degraded' — the deferred
   *  background base builds. 0 on a healthy generation attach. */
  textRebuildMs: number;
}

/** How one text index's base was served by the last open. */
export type OpenTextIndexSource =
  /** Attached from the generation's images — no tokenization at all. */
  | 'image'
  /** Rebuilt by the bounded engine hosted in a worker thread. */
  | 'worker'
  /** Rebuilt by the bounded engine hosted inline on this thread. */
  | 'inline'
  /** Rebuilt by the staged in-thread build (the unbounded small-corpus path). */
  | 'staged'
  /** Still building in the deferred background task when open() returned. */
  | 'deferred';

/** The public read model (MiniDb.lifecycleStatus()). */
export interface MiniDbLifecycleStatus {
  state: MiniDbLifecycleState;
  /** The states the last open walked through, in order (deduped transitions;
   *  always starts at 'no-generation' — nothing is loaded before the first
   *  attempt). */
  path: MiniDbLifecycleState[];
  /** Epoch ms when open() completed; null while an open is still in flight
   *  (or after a failed open — a failed open never reaches ready/degraded). */
  openedAt: number | null;
  phases: OpenPhaseTimings;
  /** Per text index: how its base was served (absent = the index did not
   *  exist at open). A 'deferred' entry flips to its hosting mode when the
   *  background build commits. */
  textIndexes: Record<string, OpenTextIndexSource>;
  /** Text index names whose base build was still pending when open()
   *  returned — the reason for 'degraded'. */
  pendingTextIndexes: string[];
}

/** The mutable tracker behind MiniDb.lifecycleStatus(); driven by the
 *  lifecycle facets through the LifecycleHost / GenerationLoaderDeps views
 *  (same shared-by-reference discipline as the stats object). */
export class LifecycleTracker {
  private current: MiniDbLifecycleState = 'no-generation';
  private readonly transitions: MiniDbLifecycleState[] = ['no-generation'];
  private completedAt: number | null = null;
  private readonly pending = new Set<string>();
  private readonly textSources = new Map<string, OpenTextIndexSource>();
  readonly phases: OpenPhaseTimings = {
    openMs: 0,
    generationCandidateLoadMs: 0,
    storeImageLoadMs: 0,
    nonTextImageLoadMs: 0,
    textImageLoadMs: 0,
    postingsIntegrityCheckMs: 0,
    walScanMs: 0,
    walApplyMs: 0,
    fullRecoveryMs: 0,
    textRebuildMs: 0,
  };

  get state(): MiniDbLifecycleState {
    return this.current;
  }

  transition(next: MiniDbLifecycleState): void {
    if (next === this.current) return;
    this.current = next;
    this.transitions.push(next);
  }

  /** Add `ms` to one phase (phases accumulate across candidates/passes). */
  time(phase: keyof OpenPhaseTimings, ms: number): void {
    this.phases[phase] += ms;
  }

  noteTextIndexSource(name: string, source: OpenTextIndexSource): void {
    this.textSources.set(name, source);
  }

  /** A text index's base build was deferred to the background task: pending
   *  from here until the build commits (finishOpen maps this to 'degraded'). */
  markTextIndexPending(name: string): void {
    this.pending.add(name);
    this.textSources.set(name, 'deferred');
  }

  /** The deferred build for one index committed (source = its hosting mode);
   *  the last pending clear flips a completed open back to 'ready'. A finally
   *  failed build stays pending — the index keeps raising
   *  TextIndexBuildingError, which IS the degraded state. */
  clearTextIndexPending(name: string, source: Exclude<OpenTextIndexSource, 'image' | 'deferred'>): void {
    this.pending.delete(name);
    this.textSources.set(name, source);
    if (this.pending.size === 0 && this.completedAt !== null && this.current === 'degraded') {
      this.transition('ready');
    }
  }

  /** open() is about to return: servable ('ready') unless a deferred text
   *  build is still pending ('degraded'). */
  finishOpen(): void {
    this.completedAt = Date.now();
    this.transition(this.pending.size > 0 ? 'degraded' : 'ready');
  }

  snapshot(): MiniDbLifecycleStatus {
    return {
      state: this.current,
      path: [...this.transitions],
      openedAt: this.completedAt,
      phases: { ...this.phases },
      textIndexes: Object.fromEntries(this.textSources),
      pendingTextIndexes: [...this.pending],
    };
  }
}