@dxos/echo-pipeline 0.1.56-main.c19b7cd → 0.1.56-main.c277f68
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/dist/lib/browser/{chunk-EJ35NHZV.mjs → chunk-GDF4DQU2.mjs} +296 -272
- package/dist/lib/browser/chunk-GDF4DQU2.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +12 -17
- package/dist/lib/browser/testing/index.mjs.map +1 -1
- package/dist/lib/node/index.cjs +300 -276
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +313 -294
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/db-host/data-service-host.d.ts.map +1 -1
- package/dist/types/src/pipeline/pipeline-stress.test.d.ts +2 -0
- package/dist/types/src/pipeline/pipeline-stress.test.d.ts.map +1 -0
- package/dist/types/src/pipeline/pipeline.d.ts +4 -3
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/space/control-pipeline.d.ts +4 -0
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/data-pipeline.d.ts +2 -0
- package/dist/types/src/space/data-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/database-test-rig.d.ts +4 -1
- package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
- package/package.json +32 -32
- package/src/db-host/data-service-host.ts +9 -2
- package/src/metadata/metadata-store.ts +1 -1
- package/src/pipeline/pipeline-stress.test.ts +226 -0
- package/src/pipeline/pipeline.test.ts +2 -214
- package/src/pipeline/pipeline.ts +47 -22
- package/src/space/control-pipeline.ts +47 -22
- package/src/space/data-pipeline.ts +20 -10
- package/src/space/space-manager.ts +1 -0
- package/src/space/space-protocol.ts +3 -1
- package/src/space/space.ts +1 -0
- package/src/testing/database-test-rig.ts +27 -2
- package/src/tests/database-unit.test.ts +39 -0
- package/dist/lib/browser/chunk-EJ35NHZV.mjs.map +0 -7
|
@@ -3,14 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import expect from 'expect';
|
|
6
|
-
import * as fc from 'fast-check';
|
|
7
|
-
import { inspect } from 'util';
|
|
8
6
|
|
|
9
|
-
import { Event,
|
|
10
|
-
import { FeedStore, FeedWrapper } from '@dxos/feed-store';
|
|
11
|
-
import { PublicKey } from '@dxos/keys';
|
|
12
|
-
import { log } from '@dxos/log';
|
|
13
|
-
import { FeedMessageBlock } from '@dxos/protocols';
|
|
7
|
+
import { Event, sleep } from '@dxos/async';
|
|
14
8
|
import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
15
9
|
import { describe, test, afterTest } from '@dxos/test';
|
|
16
10
|
import { Timeframe } from '@dxos/timeframe';
|
|
@@ -85,7 +79,7 @@ describe('pipeline/Pipeline', () => {
|
|
|
85
79
|
|
|
86
80
|
const processedSequenceNumbers: number[] = [];
|
|
87
81
|
|
|
88
|
-
//
|
|
82
|
+
// Skip first 10, process the rest, and the repeat the last 10.
|
|
89
83
|
const expectedSequenceNumbers = [...sequenceNumbers.slice(10, 30), ...sequenceNumbers.slice(20, 30)];
|
|
90
84
|
|
|
91
85
|
await pipeline.setCursor(new Timeframe([[feed.key, 9]]));
|
|
@@ -144,210 +138,4 @@ describe('pipeline/Pipeline', () => {
|
|
|
144
138
|
await processedEvent.waitForCondition(() => processedSequenceNumbers.length === 10);
|
|
145
139
|
expect(processedSequenceNumbers).toEqual(expectedSequenceNumbers);
|
|
146
140
|
});
|
|
147
|
-
|
|
148
|
-
test
|
|
149
|
-
.skip('stress', async () => {
|
|
150
|
-
const builder = new TestFeedBuilder();
|
|
151
|
-
|
|
152
|
-
const NUM_AGENTS = 2;
|
|
153
|
-
const NUM_MESSAGES = 100;
|
|
154
|
-
|
|
155
|
-
class Agent {
|
|
156
|
-
public startingTimeframe = new Timeframe();
|
|
157
|
-
public pipeline!: Pipeline;
|
|
158
|
-
public feed!: FeedWrapper<FeedMessage>;
|
|
159
|
-
public messages: FeedMessageBlock[] = [];
|
|
160
|
-
public writePromise: Promise<any> = Promise.resolve();
|
|
161
|
-
|
|
162
|
-
constructor(public id: string, public feedStore: FeedStore<FeedMessage>) {}
|
|
163
|
-
|
|
164
|
-
async open() {
|
|
165
|
-
const key = await builder.keyring.createKey();
|
|
166
|
-
this.feed = await this.feedStore.openFeed(key, { writable: true });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async start() {
|
|
170
|
-
this.pipeline = new Pipeline();
|
|
171
|
-
await this.pipeline.setCursor(this.startingTimeframe);
|
|
172
|
-
|
|
173
|
-
await this.pipeline.start();
|
|
174
|
-
|
|
175
|
-
// NOTE: not awaiting here breaks the test.
|
|
176
|
-
await Promise.all(this.feedStore.feeds.map((feed) => this.pipeline.addFeed(feed)));
|
|
177
|
-
this.pipeline.setWriteFeed(this.feed);
|
|
178
|
-
|
|
179
|
-
// consume in async task.
|
|
180
|
-
void this.consume();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async stop() {
|
|
184
|
-
await this.pipeline.stop();
|
|
185
|
-
this.startingTimeframe = this.pipeline.state.timeframe;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async close() {
|
|
189
|
-
await this.feed.close();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
write(message: FeedMessage.Payload) {
|
|
193
|
-
const prev = this.writePromise;
|
|
194
|
-
const promise = this.pipeline.writer!.write(message);
|
|
195
|
-
this.writePromise = Promise.all([prev, promise]);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async consume() {
|
|
199
|
-
for await (const msg of this.pipeline.consume()) {
|
|
200
|
-
this.messages.push(msg);
|
|
201
|
-
}
|
|
202
|
-
log('stopped consuming');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
type Model = {};
|
|
207
|
-
type Real = {
|
|
208
|
-
feedStore: FeedStore<FeedMessage>;
|
|
209
|
-
agents: Map<string, Agent>;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
class WriteCommand implements fc.AsyncCommand<Model, Real> {
|
|
213
|
-
constructor(public agent: string, public count: number) {}
|
|
214
|
-
|
|
215
|
-
check = () => true;
|
|
216
|
-
|
|
217
|
-
async run(model: Model, real: Real) {
|
|
218
|
-
console.log(`WriteCommand(${this.agent}, ${this.count})`);
|
|
219
|
-
|
|
220
|
-
const agent = real.agents.get(this.agent)!;
|
|
221
|
-
|
|
222
|
-
const toWrite = Math.min(this.count, NUM_MESSAGES - agent.feed.length);
|
|
223
|
-
if (toWrite > 0) {
|
|
224
|
-
for (const _ of range(toWrite)) {
|
|
225
|
-
agent.write({}); // Content is not important.
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
toString = () => `WriteCommand(${this.agent}, ${this.count})`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
class SyncCommand implements fc.AsyncCommand<Model, Real> {
|
|
234
|
-
check = () => true;
|
|
235
|
-
|
|
236
|
-
async run(model: Model, real: Real) {
|
|
237
|
-
console.log('SyncCommand()');
|
|
238
|
-
const targets: any = {};
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
for (const agent of real.agents.values()) {
|
|
242
|
-
await agent.writePromise;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
for (const agent of real.agents.values()) {
|
|
246
|
-
targets[agent.id] = agent.pipeline.state.endTimeframe;
|
|
247
|
-
if (agent.pipeline.state.endTimeframe.isEmpty()) {
|
|
248
|
-
console.log('empty endtimeframe', {
|
|
249
|
-
id: agent.id,
|
|
250
|
-
endTimeframe: agent.pipeline.state.endTimeframe,
|
|
251
|
-
feeds: agent.pipeline.getFeeds().map((feed) => [feed.key.toString(), feed.length]),
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
await asyncTimeout(agent.pipeline.state.waitUntilTimeframe(agent.pipeline.state.endTimeframe), 1000);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const tf: Timeframe = real.agents.values().next().value!.pipeline.state.timeframe;
|
|
258
|
-
for (const agent of real.agents.values()) {
|
|
259
|
-
expect(agent.pipeline.state.timeframe.equals(tf)).toEqual(true);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const totalMessages = real.feedStore.feeds.reduce((acc, feed) => acc + feed.length, 0);
|
|
263
|
-
for (const agent of real.agents.values()) {
|
|
264
|
-
expect(agent.messages.length).toEqual(totalMessages);
|
|
265
|
-
}
|
|
266
|
-
} catch (err) {
|
|
267
|
-
console.log(
|
|
268
|
-
inspect(
|
|
269
|
-
{
|
|
270
|
-
agents: Array.from(real.agents.values()).map((agent) => ({
|
|
271
|
-
id: agent.id,
|
|
272
|
-
messages: agent.messages.length,
|
|
273
|
-
feeds: agent.pipeline.getFeeds().map((feed) => [feed.key, feed.length]),
|
|
274
|
-
timeframe: agent.pipeline.state.timeframe,
|
|
275
|
-
endTimeframe: agent.pipeline.state.endTimeframe,
|
|
276
|
-
})),
|
|
277
|
-
feeds: real.feedStore.feeds.map((feed) => [feed.key, feed.length]),
|
|
278
|
-
targets,
|
|
279
|
-
},
|
|
280
|
-
false,
|
|
281
|
-
null,
|
|
282
|
-
true,
|
|
283
|
-
),
|
|
284
|
-
);
|
|
285
|
-
throw err;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
toString = () => 'SyncCommand()';
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
class RestartCommand implements fc.AsyncCommand<Model, Real> {
|
|
293
|
-
constructor(public agent: string) {}
|
|
294
|
-
|
|
295
|
-
check = () => true;
|
|
296
|
-
|
|
297
|
-
async run(model: Model, real: Real) {
|
|
298
|
-
console.log(`RestartCommand(${this.agent})`);
|
|
299
|
-
|
|
300
|
-
const agent = real.agents.get(this.agent)!;
|
|
301
|
-
|
|
302
|
-
await agent.writePromise;
|
|
303
|
-
await agent.stop();
|
|
304
|
-
await agent.start();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
toString = () => `RestartCommand(${this.agent})`;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const agentIds = range(NUM_AGENTS).map(() => PublicKey.random().toHex().slice(0, 8));
|
|
311
|
-
const anAgentId = fc.constantFrom(...agentIds);
|
|
312
|
-
|
|
313
|
-
const commands = fc.commands(
|
|
314
|
-
[
|
|
315
|
-
fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
|
|
316
|
-
fc.constant(new SyncCommand()),
|
|
317
|
-
anAgentId.map((agent) => new RestartCommand(agent)),
|
|
318
|
-
],
|
|
319
|
-
{ size: 'large' },
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
const model = fc.asyncProperty(commands, async (commands) => {
|
|
323
|
-
const feedStore = builder.createFeedStore();
|
|
324
|
-
|
|
325
|
-
const agents = new Map(agentIds.map((id) => [id, new Agent(id, feedStore)]));
|
|
326
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
|
|
327
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
|
|
328
|
-
|
|
329
|
-
const setup: fc.ModelRunSetup<Model, Real> = () => ({
|
|
330
|
-
model: {},
|
|
331
|
-
real: {
|
|
332
|
-
feedStore,
|
|
333
|
-
agents,
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
|
|
339
|
-
} finally {
|
|
340
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
|
|
341
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
|
|
346
|
-
[[new WriteCommand(agentIds[0], 10), new WriteCommand(agentIds[1], 10), new SyncCommand()]],
|
|
347
|
-
[[new WriteCommand(agentIds[0], 4), new RestartCommand(agentIds[0]), new SyncCommand()]],
|
|
348
|
-
];
|
|
349
|
-
|
|
350
|
-
await fc.assert(model, { examples });
|
|
351
|
-
})
|
|
352
|
-
.timeout(60_000);
|
|
353
141
|
});
|
package/src/pipeline/pipeline.ts
CHANGED
|
@@ -204,9 +204,6 @@ export class Pipeline implements PipelineAccessor {
|
|
|
204
204
|
private readonly _timeframeClock = new TimeframeClock(new Timeframe());
|
|
205
205
|
private readonly _feeds = new ComplexMap<PublicKey, FeedWrapper<FeedMessage>>(PublicKey.hash);
|
|
206
206
|
|
|
207
|
-
// Inbound feed stream.
|
|
208
|
-
private _feedSetIterator?: FeedSetIterator<FeedMessage>;
|
|
209
|
-
|
|
210
207
|
// External state accessor.
|
|
211
208
|
private readonly _state: PipelineState = new PipelineState(this._feeds, this._timeframeClock);
|
|
212
209
|
|
|
@@ -214,14 +211,20 @@ export class Pipeline implements PipelineAccessor {
|
|
|
214
211
|
private readonly _processingTrigger = new Trigger().wake();
|
|
215
212
|
private readonly _pauseTrigger = new Trigger().wake();
|
|
216
213
|
|
|
217
|
-
|
|
218
|
-
private
|
|
219
|
-
|
|
220
|
-
|
|
214
|
+
// Pending downloads.
|
|
215
|
+
private readonly _downloads = new ComplexMap<FeedWrapper<FeedMessage>, any>((value) => PublicKey.hash(value.key));
|
|
216
|
+
|
|
217
|
+
// Inbound feed stream.
|
|
218
|
+
private _feedSetIterator?: FeedSetIterator<FeedMessage>;
|
|
221
219
|
|
|
222
220
|
// Outbound feed writer.
|
|
223
221
|
private _writer: FeedWriter<FeedMessage.Payload> | undefined;
|
|
224
222
|
|
|
223
|
+
private _isStopping = false;
|
|
224
|
+
private _isStarted = false;
|
|
225
|
+
private _isBeingConsumed = false;
|
|
226
|
+
private _isPaused = false;
|
|
227
|
+
|
|
225
228
|
get state() {
|
|
226
229
|
return this._state;
|
|
227
230
|
}
|
|
@@ -243,10 +246,14 @@ export class Pipeline implements PipelineAccessor {
|
|
|
243
246
|
// which might be opening feeds during the mutation processing, which w
|
|
244
247
|
async addFeed(feed: FeedWrapper<FeedMessage>) {
|
|
245
248
|
this._feeds.set(feed.key, feed);
|
|
249
|
+
|
|
246
250
|
if (this._feedSetIterator) {
|
|
247
251
|
await this._feedSetIterator.addFeed(feed);
|
|
248
252
|
}
|
|
249
|
-
|
|
253
|
+
|
|
254
|
+
if (this._isStarted && !this._isPaused) {
|
|
255
|
+
this._setFeedDownloadState(feed);
|
|
256
|
+
}
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
setWriteFeed(feed: FeedWrapper<FeedMessage>) {
|
|
@@ -270,12 +277,22 @@ export class Pipeline implements PipelineAccessor {
|
|
|
270
277
|
await this._feedSetIterator!.open();
|
|
271
278
|
this._isStarted = true;
|
|
272
279
|
log('started');
|
|
280
|
+
|
|
281
|
+
if (!this._isPaused) {
|
|
282
|
+
for (const feed of this._feeds.values()) {
|
|
283
|
+
this._setFeedDownloadState(feed);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
273
286
|
}
|
|
274
287
|
|
|
275
288
|
@synchronized
|
|
276
289
|
async stop() {
|
|
277
290
|
log('stopping...');
|
|
278
291
|
this._isStopping = true;
|
|
292
|
+
for (const [feed, handle] of this._downloads.entries()) {
|
|
293
|
+
feed.undownload(handle);
|
|
294
|
+
}
|
|
295
|
+
this._downloads.clear();
|
|
279
296
|
await this._feedSetIterator?.close();
|
|
280
297
|
await this._processingTrigger.wait(); // Wait for the in-flight message to be processed.
|
|
281
298
|
await this._state._ctx.dispose();
|
|
@@ -297,10 +314,6 @@ export class Pipeline implements PipelineAccessor {
|
|
|
297
314
|
this._timeframeClock.setTimeframe(timeframe);
|
|
298
315
|
|
|
299
316
|
// Cancel downloads of mutations before the cursor.
|
|
300
|
-
for (const feed of this._feeds.values()) {
|
|
301
|
-
this._setFeedDownloadState(feed);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
317
|
if (this._feedSetIterator) {
|
|
305
318
|
await this._feedSetIterator.close();
|
|
306
319
|
await this._initIterator();
|
|
@@ -313,7 +326,6 @@ export class Pipeline implements PipelineAccessor {
|
|
|
313
326
|
*/
|
|
314
327
|
@synchronized
|
|
315
328
|
async pause() {
|
|
316
|
-
invariant(this._isStarted, 'Pipeline is not open.');
|
|
317
329
|
if (this._isPaused) {
|
|
318
330
|
return;
|
|
319
331
|
}
|
|
@@ -325,11 +337,14 @@ export class Pipeline implements PipelineAccessor {
|
|
|
325
337
|
|
|
326
338
|
@synchronized
|
|
327
339
|
async unpause() {
|
|
328
|
-
invariant(this._isStarted, 'Pipeline is not open.');
|
|
329
340
|
invariant(this._isPaused, 'Pipeline is not paused.');
|
|
330
341
|
|
|
331
342
|
this._pauseTrigger.wake();
|
|
332
343
|
this._isPaused = false;
|
|
344
|
+
|
|
345
|
+
for (const feed of this._feeds.values()) {
|
|
346
|
+
this._setFeedDownloadState(feed);
|
|
347
|
+
}
|
|
333
348
|
}
|
|
334
349
|
|
|
335
350
|
/**
|
|
@@ -337,8 +352,8 @@ export class Pipeline implements PipelineAccessor {
|
|
|
337
352
|
* Updates the timeframe clock after the message has bee processed.
|
|
338
353
|
*/
|
|
339
354
|
async *consume(): AsyncIterable<FeedMessageBlock> {
|
|
340
|
-
invariant(!this.
|
|
341
|
-
this.
|
|
355
|
+
invariant(!this._isBeingConsumed, 'Pipeline is already being consumed.');
|
|
356
|
+
this._isBeingConsumed = true;
|
|
342
357
|
|
|
343
358
|
invariant(this._feedSetIterator, 'Iterator not initialized.');
|
|
344
359
|
let lastFeedSetIterator = this._feedSetIterator;
|
|
@@ -367,17 +382,27 @@ export class Pipeline implements PipelineAccessor {
|
|
|
367
382
|
}
|
|
368
383
|
|
|
369
384
|
// TODO(burdon): Test re-entrant?
|
|
370
|
-
this.
|
|
385
|
+
this._isBeingConsumed = false;
|
|
371
386
|
}
|
|
372
387
|
|
|
373
388
|
private _setFeedDownloadState(feed: FeedWrapper<FeedMessage>) {
|
|
374
|
-
|
|
375
|
-
|
|
389
|
+
let handle = this._downloads.get(feed); // TODO(burdon): Always undefined.
|
|
390
|
+
if (handle) {
|
|
391
|
+
feed.undownload(handle);
|
|
392
|
+
}
|
|
376
393
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
394
|
+
const timeframe = this._state._startTimeframe;
|
|
395
|
+
const seq = timeframe.get(feed.key) ?? -1;
|
|
396
|
+
log.info('download', { feed: feed.key.truncate(), seq, length: feed.length });
|
|
397
|
+
handle = feed.download({ start: seq + 1, linear: true }, (err: any, data: any) => {
|
|
398
|
+
if (err) {
|
|
399
|
+
// log.error(err); // TODO(burdon): Feed is closed.
|
|
400
|
+
} else {
|
|
401
|
+
log.info('data', data); // TODO(burdon): Never called.
|
|
402
|
+
}
|
|
380
403
|
});
|
|
404
|
+
|
|
405
|
+
this._downloads.set(feed, handle);
|
|
381
406
|
}
|
|
382
407
|
|
|
383
408
|
private async _initIterator() {
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Context } from '@dxos/context';
|
|
5
6
|
import { SpaceStateMachine, SpaceState, MemberInfo, FeedInfo } from '@dxos/credentials';
|
|
6
7
|
import { FeedWrapper } from '@dxos/feed-store';
|
|
7
8
|
import { PublicKey } from '@dxos/keys';
|
|
8
9
|
import { log } from '@dxos/log';
|
|
10
|
+
import { FeedMessageBlock } from '@dxos/protocols';
|
|
9
11
|
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
10
12
|
import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
11
13
|
import { Timeframe } from '@dxos/timeframe';
|
|
14
|
+
import { TimeSeriesCounter, TimeUsageCounter, trace } from '@dxos/tracing';
|
|
12
15
|
import { AsyncCallback, Callback, tracer } from '@dxos/util';
|
|
13
16
|
|
|
14
17
|
import { MetadataStore } from '../metadata';
|
|
@@ -26,6 +29,7 @@ const TIMEFRAME_SAVE_DEBOUNCE_INTERVAL = 500;
|
|
|
26
29
|
/**
|
|
27
30
|
* Processes HALO credentials, which include genesis and invitations.
|
|
28
31
|
*/
|
|
32
|
+
@trace.resource()
|
|
29
33
|
export class ControlPipeline {
|
|
30
34
|
private readonly _pipeline: Pipeline;
|
|
31
35
|
private readonly _spaceStateMachine: SpaceStateMachine;
|
|
@@ -39,6 +43,12 @@ export class ControlPipeline {
|
|
|
39
43
|
public readonly onMemberAdmitted: Callback<AsyncCallback<MemberInfo>>;
|
|
40
44
|
public readonly onCredentialProcessed: Callback<AsyncCallback<Credential>>;
|
|
41
45
|
|
|
46
|
+
@trace.metricsCounter()
|
|
47
|
+
private _usage = new TimeUsageCounter();
|
|
48
|
+
|
|
49
|
+
@trace.metricsCounter()
|
|
50
|
+
private _mutations = new TimeSeriesCounter();
|
|
51
|
+
|
|
42
52
|
constructor({ spaceKey, genesisFeed, feedProvider, metadataStore }: ControlPipelineParams) {
|
|
43
53
|
this._spaceKey = spaceKey;
|
|
44
54
|
this._metadata = metadataStore;
|
|
@@ -82,34 +92,49 @@ export class ControlPipeline {
|
|
|
82
92
|
async start() {
|
|
83
93
|
log('starting...');
|
|
84
94
|
setTimeout(async () => {
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
// log('processing', { msg });
|
|
88
|
-
log('processing', { key: msg.feedKey, seq: msg.seq });
|
|
89
|
-
if (msg.data.payload.credential) {
|
|
90
|
-
const timer = tracer.mark('dxos.echo.pipeline.control');
|
|
91
|
-
const result = await this._spaceStateMachine.process(
|
|
92
|
-
msg.data.payload.credential.credential,
|
|
93
|
-
PublicKey.from(msg.feedKey),
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
timer.end();
|
|
97
|
-
if (!result) {
|
|
98
|
-
log.warn('processing failed', { msg });
|
|
99
|
-
} else {
|
|
100
|
-
await this._noteTargetStateIfNeeded(this._pipeline.state.pendingTimeframe);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
} catch (err: any) {
|
|
104
|
-
log.catch(err);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
95
|
+
void this._consumePipeline(new Context());
|
|
107
96
|
});
|
|
108
97
|
|
|
109
98
|
await this._pipeline.start();
|
|
110
99
|
log('started');
|
|
111
100
|
}
|
|
112
101
|
|
|
102
|
+
@trace.span()
|
|
103
|
+
private async _consumePipeline(ctx: Context) {
|
|
104
|
+
for await (const msg of this._pipeline.consume()) {
|
|
105
|
+
const span = this._usage.beginRecording();
|
|
106
|
+
this._mutations.inc();
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await this._processMessage(ctx, msg);
|
|
110
|
+
} catch (err: any) {
|
|
111
|
+
log.catch(err);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
span.end();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@trace.span()
|
|
119
|
+
private async _processMessage(ctx: Context, msg: FeedMessageBlock) {
|
|
120
|
+
// log('processing', { msg });
|
|
121
|
+
log('processing', { key: msg.feedKey, seq: msg.seq });
|
|
122
|
+
if (msg.data.payload.credential) {
|
|
123
|
+
const timer = tracer.mark('dxos.echo.pipeline.control');
|
|
124
|
+
const result = await this._spaceStateMachine.process(
|
|
125
|
+
msg.data.payload.credential.credential,
|
|
126
|
+
PublicKey.from(msg.feedKey),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
timer.end();
|
|
130
|
+
if (!result) {
|
|
131
|
+
log.warn('processing failed', { msg });
|
|
132
|
+
} else {
|
|
133
|
+
await this._noteTargetStateIfNeeded(this._pipeline.state.pendingTimeframe);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
113
138
|
private async _noteTargetStateIfNeeded(timeframe: Timeframe) {
|
|
114
139
|
// TODO(dmaretskyi): Replace this with a proper debounce/throttle.
|
|
115
140
|
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
import { Event, scheduleTask, synchronized, trackLeaks } from '@dxos/async';
|
|
6
6
|
import { Context } from '@dxos/context';
|
|
7
7
|
import { CredentialProcessor, FeedInfo, SpecificCredential, checkCredentialType } from '@dxos/credentials';
|
|
8
|
-
import { getStateMachineFromItem, ItemManager } from '@dxos/echo-db';
|
|
8
|
+
import { getStateMachineFromItem, ItemManager, TYPE_PROPERTIES } from '@dxos/echo-db';
|
|
9
9
|
import { CancelledError } from '@dxos/errors';
|
|
10
10
|
import { FeedWriter } from '@dxos/feed-store';
|
|
11
11
|
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { PublicKey } from '@dxos/keys';
|
|
13
|
-
import { log } from '@dxos/log';
|
|
13
|
+
import { log, omit } from '@dxos/log';
|
|
14
14
|
import { ModelFactory } from '@dxos/model-factory';
|
|
15
15
|
import { DataPipelineProcessed } from '@dxos/protocols';
|
|
16
16
|
import { DataMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
@@ -19,6 +19,7 @@ import { ObjectSnapshot } from '@dxos/protocols/proto/dxos/echo/model/document';
|
|
|
19
19
|
import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
20
20
|
import { Credential, Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
21
21
|
import { Timeframe } from '@dxos/timeframe';
|
|
22
|
+
import { TimeSeriesCounter, TimeUsageCounter, trace } from '@dxos/tracing';
|
|
22
23
|
import { tracer } from '@dxos/util';
|
|
23
24
|
|
|
24
25
|
import { DatabaseHost, SnapshotManager } from '../db-host';
|
|
@@ -65,6 +66,7 @@ const TIMEFRAME_SAVE_DEBOUNCE_INTERVAL = 500;
|
|
|
65
66
|
* Reacts to new epochs to restart the pipeline.
|
|
66
67
|
*/
|
|
67
68
|
@trackLeaks('open', 'close')
|
|
69
|
+
@trace.resource()
|
|
68
70
|
export class DataPipeline implements CredentialProcessor {
|
|
69
71
|
private _ctx = new Context();
|
|
70
72
|
private _pipeline?: Pipeline = undefined;
|
|
@@ -78,6 +80,12 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
78
80
|
private _lastProcessedEpoch = -1;
|
|
79
81
|
private _epochCtx?: Context;
|
|
80
82
|
|
|
83
|
+
@trace.metricsCounter()
|
|
84
|
+
private _usage = new TimeUsageCounter();
|
|
85
|
+
|
|
86
|
+
@trace.metricsCounter()
|
|
87
|
+
private _mutations = new TimeSeriesCounter();
|
|
88
|
+
|
|
81
89
|
constructor(private readonly _params: DataPipelineParams) {}
|
|
82
90
|
|
|
83
91
|
public itemManager!: ItemManager;
|
|
@@ -132,8 +140,9 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
132
140
|
|
|
133
141
|
this._pipeline = new Pipeline();
|
|
134
142
|
await this._params.onPipelineCreated(this._pipeline);
|
|
135
|
-
|
|
143
|
+
|
|
136
144
|
await this._pipeline.pause(); // Start paused until we have the first epoch.
|
|
145
|
+
await this._pipeline.start();
|
|
137
146
|
|
|
138
147
|
if (this._targetTimeframe) {
|
|
139
148
|
this._pipeline.state.setTargetTimeframe(this._targetTimeframe);
|
|
@@ -205,6 +214,9 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
205
214
|
|
|
206
215
|
invariant(this._pipeline, 'Pipeline is not initialized.');
|
|
207
216
|
for await (const msg of this._pipeline.consume()) {
|
|
217
|
+
const span = this._usage.beginRecording();
|
|
218
|
+
this._mutations.inc();
|
|
219
|
+
|
|
208
220
|
const { feedKey, seq, data } = msg;
|
|
209
221
|
log('processing message', { feedKey, seq });
|
|
210
222
|
|
|
@@ -241,6 +253,8 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
241
253
|
} catch (err: any) {
|
|
242
254
|
log.catch(err);
|
|
243
255
|
}
|
|
256
|
+
|
|
257
|
+
span.end();
|
|
244
258
|
}
|
|
245
259
|
}
|
|
246
260
|
|
|
@@ -268,7 +282,7 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
268
282
|
(item) =>
|
|
269
283
|
item.modelMeta?.type === 'dxos:model/document' &&
|
|
270
284
|
// TODO(burdon): Document?
|
|
271
|
-
(getStateMachineFromItem(item)?.snapshot() as ObjectSnapshot).type ===
|
|
285
|
+
(getStateMachineFromItem(item)?.snapshot() as ObjectSnapshot).type === TYPE_PROPERTIES,
|
|
272
286
|
);
|
|
273
287
|
if (propertiesItem) {
|
|
274
288
|
cache.properties = getStateMachineFromItem(propertiesItem)?.snapshot() as ObjectSnapshot;
|
|
@@ -318,7 +332,6 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
318
332
|
// Space closed before we got to process the epoch.
|
|
319
333
|
return;
|
|
320
334
|
}
|
|
321
|
-
log('process epoch', { epoch });
|
|
322
335
|
await this._processEpoch(ctx, epoch.subject.assertion);
|
|
323
336
|
|
|
324
337
|
this.appliedEpoch = epoch;
|
|
@@ -332,16 +345,13 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
332
345
|
invariant(this._pipeline);
|
|
333
346
|
this._lastProcessedEpoch = epoch.number;
|
|
334
347
|
|
|
335
|
-
log('
|
|
348
|
+
log('processing', { epoch: omit(epoch, 'proof') });
|
|
336
349
|
if (epoch.snapshotCid) {
|
|
337
350
|
const snapshot = await this._params.snapshotManager.load(ctx, epoch.snapshotCid);
|
|
338
|
-
|
|
339
|
-
// TODO(dmaretskyi): Clearing old items + events.
|
|
340
351
|
this.databaseHost!._itemDemuxer.restoreFromSnapshot(snapshot.database);
|
|
341
352
|
}
|
|
342
353
|
|
|
343
|
-
log('restarting pipeline
|
|
344
|
-
|
|
354
|
+
log('restarting pipeline from epoch');
|
|
345
355
|
await this._pipeline.pause();
|
|
346
356
|
await this._pipeline.setCursor(epoch.timeframe);
|
|
347
357
|
await this._pipeline.unpause();
|
|
@@ -87,6 +87,7 @@ export class SpaceProtocol {
|
|
|
87
87
|
this._onAuthFailure = onAuthFailure;
|
|
88
88
|
this.blobSync = new BlobSync({ blobStore });
|
|
89
89
|
|
|
90
|
+
// TODO(burdon): Async race condition? Move to start?
|
|
90
91
|
this._topic = subtleCrypto.digest('SHA-256', topic.asBuffer()).then(discoveryKey).then(PublicKey.from);
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -100,6 +101,7 @@ export class SpaceProtocol {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
// TODO(burdon): Rename open? Common open/close interfaces for all services?
|
|
103
105
|
async start() {
|
|
104
106
|
if (this._connection) {
|
|
105
107
|
return;
|
|
@@ -124,7 +126,7 @@ export class SpaceProtocol {
|
|
|
124
126
|
peerId: this._swarmIdentity.peerKey,
|
|
125
127
|
topic,
|
|
126
128
|
topology: new MMSTTopology(topologyConfig),
|
|
127
|
-
label: `
|
|
129
|
+
label: `space swarm ${topic.truncate()}`,
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
log('started');
|