@forgeax/engine-ddc 0.1.6 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +213 -93
- package/dist/index.mjs.map +1 -1
- package/dist/lifecycle.d.ts +38 -3
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/publication.d.ts.map +1 -1
- package/dist/session.d.ts +4 -3
- package/dist/session.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/generation-session.integration.test.ts +132 -4
- package/src/__tests__/multiprocess-lifecycle.integration.test.ts +33 -2
- package/src/index.ts +4 -0
- package/src/lifecycle.ts +297 -92
- package/src/publication.ts +12 -10
- package/src/session.ts +40 -13
package/src/session.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { DdcEntry, StagedDdcEntry } from './entry-store.js';
|
|
2
2
|
import { DdcEntryStore } from './entry-store.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
DdcCommitResult,
|
|
5
|
+
DdcHead,
|
|
6
|
+
DdcLease,
|
|
7
|
+
DdcRestoreFence,
|
|
8
|
+
DdcRestoreResult,
|
|
9
|
+
DdcRollbackSnapshot,
|
|
10
|
+
} from './lifecycle.js';
|
|
4
11
|
import { DdcLifecycle } from './lifecycle.js';
|
|
5
12
|
|
|
6
13
|
export interface DdcSessionOptions {
|
|
@@ -11,7 +18,7 @@ export interface DdcSessionOptions {
|
|
|
11
18
|
export interface DdcGenerationCandidate {
|
|
12
19
|
readonly generation: number;
|
|
13
20
|
readonly lease: DdcLease;
|
|
14
|
-
readonly previousHead:
|
|
21
|
+
readonly previousHead: DdcRollbackSnapshot;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
export interface DdcGenerationEntryCandidate extends DdcGenerationCandidate {
|
|
@@ -47,6 +54,7 @@ export class DdcGenerationSession {
|
|
|
47
54
|
private readonly lifecycle: DdcLifecycle;
|
|
48
55
|
private readonly entries: DdcEntryStore;
|
|
49
56
|
private readonly candidates = new Map<string, MutableDdcGenerationCandidate>();
|
|
57
|
+
private readonly restoreFences = new WeakMap<DdcGenerationCandidate, DdcRestoreFence>();
|
|
50
58
|
private readonly heartbeatTimers = new Map<string, HeartbeatTimer>();
|
|
51
59
|
private readonly counters: MutableMetrics = {
|
|
52
60
|
hitCount: 0,
|
|
@@ -69,12 +77,14 @@ export class DdcGenerationSession {
|
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
public async beginCandidate(guid: string, desiredKey: string): Promise<DdcGenerationCandidate> {
|
|
72
|
-
|
|
73
|
-
const lease = await this.lifecycle.
|
|
80
|
+
this.assertOpen();
|
|
81
|
+
const { lease, previousHead } = await this.lifecycle.beginWithSnapshot(guid, desiredKey);
|
|
82
|
+
if (previousHead.state === 'current') this.counters.hitCount += 1;
|
|
83
|
+
else this.counters.missCount += 1;
|
|
74
84
|
const candidate: MutableDdcGenerationCandidate = {
|
|
75
85
|
generation: this.generation,
|
|
76
86
|
lease,
|
|
77
|
-
previousHead
|
|
87
|
+
previousHead,
|
|
78
88
|
};
|
|
79
89
|
this.candidates.set(lease.attempt, candidate);
|
|
80
90
|
this.scheduleHeartbeat(candidate);
|
|
@@ -95,10 +105,11 @@ export class DdcGenerationSession {
|
|
|
95
105
|
this.candidates.set(candidate.lease.attempt, entryCandidate);
|
|
96
106
|
return entryCandidate;
|
|
97
107
|
} catch (error) {
|
|
98
|
-
await this.lifecycle.fail(candidate.lease, {
|
|
108
|
+
const fence = await this.lifecycle.fail(candidate.lease, {
|
|
99
109
|
code: 'ddc-entry-stage-failed',
|
|
100
110
|
detail: error instanceof Error ? error.message : String(error),
|
|
101
111
|
});
|
|
112
|
+
if (fence !== undefined) this.restoreFences.set(candidate, fence);
|
|
102
113
|
this.stopHeartbeat(candidate);
|
|
103
114
|
this.candidates.delete(candidate.lease.attempt);
|
|
104
115
|
this.counters.writeFailureCount += 1;
|
|
@@ -121,6 +132,8 @@ export class DdcGenerationSession {
|
|
|
121
132
|
const registered = this.assertCandidate(candidate);
|
|
122
133
|
try {
|
|
123
134
|
const result = await this.lifecycle.commit(registered.lease, validatedKey);
|
|
135
|
+
if (result.restoreFence !== undefined)
|
|
136
|
+
this.restoreFences.set(registered, result.restoreFence);
|
|
124
137
|
if (result.result === 'invalid') this.counters.corruptCount += 1;
|
|
125
138
|
this.stopHeartbeat(registered);
|
|
126
139
|
this.candidates.delete(registered.lease.attempt);
|
|
@@ -140,15 +153,18 @@ export class DdcGenerationSession {
|
|
|
140
153
|
try {
|
|
141
154
|
await this.entries.publish(candidate.staged);
|
|
142
155
|
const result = await this.lifecycle.commit(registered.lease, validatedKey);
|
|
156
|
+
if (result.restoreFence !== undefined)
|
|
157
|
+
this.restoreFences.set(registered, result.restoreFence);
|
|
143
158
|
this.stopHeartbeat(registered);
|
|
144
159
|
this.candidates.delete(registered.lease.attempt);
|
|
145
160
|
return result;
|
|
146
161
|
} catch (error) {
|
|
147
162
|
this.stopHeartbeat(registered);
|
|
148
|
-
await this.lifecycle.fail(registered.lease, {
|
|
163
|
+
const fence = await this.lifecycle.fail(registered.lease, {
|
|
149
164
|
code: 'ddc-entry-publish-failed',
|
|
150
165
|
detail: error instanceof Error ? error.message : String(error),
|
|
151
166
|
});
|
|
167
|
+
if (fence !== undefined) this.restoreFences.set(registered, fence);
|
|
152
168
|
await this.entries.discard(candidate.staged).catch(() => {});
|
|
153
169
|
this.candidates.delete(registered.lease.attempt);
|
|
154
170
|
this.counters.writeFailureCount += 1;
|
|
@@ -173,21 +189,31 @@ export class DdcGenerationSession {
|
|
|
173
189
|
await Promise.all([this.discardCandidate(candidate), this.entries.discard(candidate.staged)]);
|
|
174
190
|
}
|
|
175
191
|
|
|
176
|
-
public async restoreEntry(candidate: DdcGenerationEntryCandidate): Promise<
|
|
192
|
+
public async restoreEntry(candidate: DdcGenerationEntryCandidate): Promise<DdcRestoreResult> {
|
|
177
193
|
const registered = this.candidates.get(candidate.lease.attempt);
|
|
194
|
+
const ownerLease = registered?.lease ?? candidate.lease;
|
|
195
|
+
const fence = this.restoreFences.get(candidate);
|
|
196
|
+
this.stopHeartbeat(ownerLease);
|
|
197
|
+
let result: DdcRestoreResult = {
|
|
198
|
+
result: 'not-owner',
|
|
199
|
+
revision: candidate.previousHead.revision ?? 0,
|
|
200
|
+
...(candidate.previousHead.generation === undefined
|
|
201
|
+
? {}
|
|
202
|
+
: { generation: candidate.previousHead.generation }),
|
|
203
|
+
};
|
|
178
204
|
let restoreError: unknown;
|
|
179
205
|
try {
|
|
180
|
-
await this.lifecycle.
|
|
206
|
+
result = await this.lifecycle.restoreIfCurrent(candidate.previousHead, ownerLease, fence);
|
|
181
207
|
} catch (error) {
|
|
182
208
|
restoreError = error;
|
|
183
209
|
} finally {
|
|
184
210
|
await this.entries.discard(candidate.staged).catch(() => {});
|
|
185
211
|
if (registered !== undefined) {
|
|
186
|
-
this.stopHeartbeat(registered);
|
|
187
212
|
this.candidates.delete(registered.lease.attempt);
|
|
188
213
|
}
|
|
189
214
|
}
|
|
190
215
|
if (restoreError !== undefined) throw restoreError;
|
|
216
|
+
return result;
|
|
191
217
|
}
|
|
192
218
|
|
|
193
219
|
public metrics(): DdcSessionMetrics {
|
|
@@ -245,10 +271,11 @@ export class DdcGenerationSession {
|
|
|
245
271
|
this.heartbeatTimers.set(attempt, timer);
|
|
246
272
|
}
|
|
247
273
|
|
|
248
|
-
private stopHeartbeat(candidate: DdcGenerationCandidate): void {
|
|
249
|
-
const
|
|
274
|
+
private stopHeartbeat(candidate: DdcGenerationCandidate | DdcLease): void {
|
|
275
|
+
const attempt = 'lease' in candidate ? candidate.lease.attempt : candidate.attempt;
|
|
276
|
+
const timer = this.heartbeatTimers.get(attempt);
|
|
250
277
|
if (timer === undefined) return;
|
|
251
278
|
clearTimeout(timer);
|
|
252
|
-
this.heartbeatTimers.delete(
|
|
279
|
+
this.heartbeatTimers.delete(attempt);
|
|
253
280
|
}
|
|
254
281
|
}
|