@dxos/echo-pipeline 0.1.56-main.e47dfd1 → 0.1.56-main.e79d64a
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-7U2NXI2P.mjs → chunk-GDF4DQU2.mjs} +692 -355
- 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 +14 -18
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/index.cjs +699 -362
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +696 -363
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/common/feeds.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service-host.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/snapshot-manager.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/pipeline/message-selector.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/auth.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 +2 -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/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/package.json +32 -31
- package/src/common/feeds.ts +1 -2
- package/src/db-host/data-service-host.ts +10 -4
- package/src/db-host/data-service.ts +1 -2
- package/src/db-host/snapshot-manager.ts +0 -2
- package/src/metadata/metadata-store.ts +2 -2
- package/src/pipeline/message-selector.ts +1 -2
- package/src/pipeline/pipeline-stress.test.ts +226 -0
- package/src/pipeline/pipeline.test.ts +30 -206
- package/src/pipeline/pipeline.ts +50 -24
- package/src/space/auth.ts +1 -2
- package/src/space/control-pipeline.ts +47 -22
- package/src/space/data-pipeline.ts +21 -12
- package/src/space/space-manager.ts +1 -0
- package/src/space/space-protocol.ts +3 -1
- package/src/space/space.test.ts +18 -17
- package/src/space/space.ts +7 -3
- package/src/testing/database-test-rig.ts +27 -2
- package/src/testing/test-agent-builder.ts +2 -1
- package/src/tests/database-unit.test.ts +39 -0
- package/dist/lib/browser/chunk-7U2NXI2P.mjs.map +0 -7
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import expect from 'expect';
|
|
6
|
+
import * as fc from 'fast-check';
|
|
7
|
+
import { inspect } from 'util';
|
|
8
|
+
|
|
9
|
+
import { asyncTimeout } from '@dxos/async';
|
|
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';
|
|
14
|
+
import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
15
|
+
import { describe, test } from '@dxos/test';
|
|
16
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
17
|
+
import { range } from '@dxos/util';
|
|
18
|
+
|
|
19
|
+
import { TestFeedBuilder } from '../testing';
|
|
20
|
+
import { Pipeline } from './pipeline';
|
|
21
|
+
|
|
22
|
+
const NUM_AGENTS = 2;
|
|
23
|
+
const NUM_MESSAGES = 10;
|
|
24
|
+
|
|
25
|
+
// TODO(burdon): Describe test.
|
|
26
|
+
describe('pipeline/stress test', () => {
|
|
27
|
+
test
|
|
28
|
+
.skip('stress', async () => {
|
|
29
|
+
const builder = new TestFeedBuilder();
|
|
30
|
+
|
|
31
|
+
const agentIds = range(NUM_AGENTS).map(() => PublicKey.random().toHex().slice(0, 8));
|
|
32
|
+
const anAgentId = fc.constantFrom(...agentIds);
|
|
33
|
+
|
|
34
|
+
const commands = fc.commands(
|
|
35
|
+
[
|
|
36
|
+
fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
|
|
37
|
+
fc.constant(new SyncCommand()),
|
|
38
|
+
anAgentId.map((agent) => new RestartCommand(agent)),
|
|
39
|
+
],
|
|
40
|
+
{ size: 'large' },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const model = fc.asyncProperty(commands, async (commands) => {
|
|
44
|
+
const feedStore = builder.createFeedStore();
|
|
45
|
+
|
|
46
|
+
const agents = new Map(agentIds.map((id) => [id, new Agent(builder, feedStore, id)]));
|
|
47
|
+
await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
|
|
48
|
+
await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
|
|
49
|
+
|
|
50
|
+
const setup: fc.ModelRunSetup<Model, Real> = () => ({
|
|
51
|
+
model: {},
|
|
52
|
+
real: {
|
|
53
|
+
feedStore,
|
|
54
|
+
agents,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
|
|
60
|
+
} finally {
|
|
61
|
+
await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
|
|
62
|
+
await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
|
|
67
|
+
[[new WriteCommand(agentIds[0], 10), new WriteCommand(agentIds[1], 10), new SyncCommand()]],
|
|
68
|
+
[[new WriteCommand(agentIds[0], 4), new RestartCommand(agentIds[0]), new SyncCommand()]],
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
await fc.assert(model, { examples });
|
|
72
|
+
})
|
|
73
|
+
.timeout(60_000);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
class Agent {
|
|
77
|
+
public startingTimeframe = new Timeframe();
|
|
78
|
+
public pipeline!: Pipeline;
|
|
79
|
+
public feed!: FeedWrapper<FeedMessage>;
|
|
80
|
+
public messages: FeedMessageBlock[] = [];
|
|
81
|
+
public writePromise: Promise<any> = Promise.resolve();
|
|
82
|
+
|
|
83
|
+
constructor(private readonly builder: TestFeedBuilder, public feedStore: FeedStore<FeedMessage>, public id: string) {}
|
|
84
|
+
|
|
85
|
+
async open() {
|
|
86
|
+
const key = await this.builder.keyring.createKey();
|
|
87
|
+
this.feed = await this.feedStore.openFeed(key, { writable: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async start() {
|
|
91
|
+
this.pipeline = new Pipeline();
|
|
92
|
+
await this.pipeline.setCursor(this.startingTimeframe);
|
|
93
|
+
await this.pipeline.start();
|
|
94
|
+
|
|
95
|
+
// NOTE: not awaiting here breaks the test.
|
|
96
|
+
await Promise.all(this.feedStore.feeds.map((feed) => this.pipeline.addFeed(feed)));
|
|
97
|
+
this.pipeline.setWriteFeed(this.feed);
|
|
98
|
+
|
|
99
|
+
// consume in async task.
|
|
100
|
+
void this.consume();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async stop() {
|
|
104
|
+
await this.pipeline.stop();
|
|
105
|
+
this.startingTimeframe = this.pipeline.state.timeframe;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async close() {
|
|
109
|
+
await this.feed.close();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
write(message: FeedMessage.Payload) {
|
|
113
|
+
const prev = this.writePromise;
|
|
114
|
+
const promise = this.pipeline.writer!.write(message);
|
|
115
|
+
this.writePromise = Promise.all([prev, promise]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async consume() {
|
|
119
|
+
for await (const msg of this.pipeline.consume()) {
|
|
120
|
+
this.messages.push(msg);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
log('stopped consuming');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type Model = {};
|
|
128
|
+
type Real = {
|
|
129
|
+
feedStore: FeedStore<FeedMessage>;
|
|
130
|
+
agents: Map<string, Agent>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
class WriteCommand implements fc.AsyncCommand<Model, Real> {
|
|
134
|
+
constructor(public agent: string, public count: number) {}
|
|
135
|
+
|
|
136
|
+
check = () => true;
|
|
137
|
+
|
|
138
|
+
async run(model: Model, real: Real) {
|
|
139
|
+
// console.log(`WriteCommand(${this.agent}, ${this.count})`);
|
|
140
|
+
const agent = real.agents.get(this.agent)!;
|
|
141
|
+
const toWrite = Math.min(this.count, NUM_MESSAGES - agent.feed.length);
|
|
142
|
+
if (toWrite > 0) {
|
|
143
|
+
for (const _ of range(toWrite)) {
|
|
144
|
+
agent.write({}); // Content is not important.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
toString = () => `WriteCommand(${this.agent}, ${this.count})`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class SyncCommand implements fc.AsyncCommand<Model, Real> {
|
|
153
|
+
check = () => true;
|
|
154
|
+
|
|
155
|
+
async run(model: Model, real: Real) {
|
|
156
|
+
// console.log('SyncCommand()');
|
|
157
|
+
const targets: any = {};
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
for (const agent of real.agents.values()) {
|
|
161
|
+
await agent.writePromise;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const agent of real.agents.values()) {
|
|
165
|
+
targets[agent.id] = agent.pipeline.state.endTimeframe;
|
|
166
|
+
if (agent.pipeline.state.endTimeframe.isEmpty()) {
|
|
167
|
+
log('empty endtimeframe', {
|
|
168
|
+
id: agent.id,
|
|
169
|
+
endTimeframe: agent.pipeline.state.endTimeframe,
|
|
170
|
+
feeds: agent.pipeline.getFeeds().map((feed) => [feed.key.toString(), feed.length]),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
await asyncTimeout(agent.pipeline.state.waitUntilTimeframe(agent.pipeline.state.endTimeframe), 1000);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const tf: Timeframe = real.agents.values().next().value!.pipeline.state.timeframe;
|
|
177
|
+
for (const agent of real.agents.values()) {
|
|
178
|
+
expect(agent.pipeline.state.timeframe.equals(tf)).toEqual(true);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const totalMessages = real.feedStore.feeds.reduce((acc, feed) => acc + feed.length, 0);
|
|
182
|
+
for (const agent of real.agents.values()) {
|
|
183
|
+
expect(agent.messages.length).toEqual(totalMessages);
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
log(
|
|
187
|
+
inspect(
|
|
188
|
+
{
|
|
189
|
+
agents: Array.from(real.agents.values()).map((agent) => ({
|
|
190
|
+
id: agent.id,
|
|
191
|
+
messages: agent.messages.length,
|
|
192
|
+
feeds: agent.pipeline.getFeeds().map((feed) => [feed.key, feed.length]),
|
|
193
|
+
timeframe: agent.pipeline.state.timeframe,
|
|
194
|
+
endTimeframe: agent.pipeline.state.endTimeframe,
|
|
195
|
+
})),
|
|
196
|
+
feeds: real.feedStore.feeds.map((feed) => [feed.key, feed.length]),
|
|
197
|
+
targets,
|
|
198
|
+
},
|
|
199
|
+
false,
|
|
200
|
+
null,
|
|
201
|
+
true,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
toString = () => 'SyncCommand()';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class RestartCommand implements fc.AsyncCommand<Model, Real> {
|
|
212
|
+
constructor(public agent: string) {}
|
|
213
|
+
|
|
214
|
+
check = () => true;
|
|
215
|
+
|
|
216
|
+
async run(model: Model, real: Real) {
|
|
217
|
+
log(`RestartCommand(${this.agent})`);
|
|
218
|
+
|
|
219
|
+
const agent = real.agents.get(this.agent)!;
|
|
220
|
+
await agent.writePromise;
|
|
221
|
+
await agent.stop();
|
|
222
|
+
await agent.start();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
toString = () => `RestartCommand(${this.agent})`;
|
|
226
|
+
}
|
|
@@ -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 {
|
|
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]]));
|
|
@@ -109,209 +103,39 @@ describe('pipeline/Pipeline', () => {
|
|
|
109
103
|
expect(processedSequenceNumbers).toEqual(expectedSequenceNumbers);
|
|
110
104
|
});
|
|
111
105
|
|
|
112
|
-
test
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const NUM_AGENTS = 2;
|
|
117
|
-
const NUM_MESSAGES = 100;
|
|
118
|
-
|
|
119
|
-
class Agent {
|
|
120
|
-
public startingTimeframe = new Timeframe();
|
|
121
|
-
public pipeline!: Pipeline;
|
|
122
|
-
public feed!: FeedWrapper<FeedMessage>;
|
|
123
|
-
public messages: FeedMessageBlock[] = [];
|
|
124
|
-
public writePromise: Promise<any> = Promise.resolve();
|
|
125
|
-
|
|
126
|
-
constructor(public id: string, public feedStore: FeedStore<FeedMessage>) {}
|
|
127
|
-
|
|
128
|
-
async open() {
|
|
129
|
-
const key = await builder.keyring.createKey();
|
|
130
|
-
this.feed = await this.feedStore.openFeed(key, { writable: true });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async start() {
|
|
134
|
-
this.pipeline = new Pipeline();
|
|
135
|
-
await this.pipeline.setCursor(this.startingTimeframe);
|
|
136
|
-
|
|
137
|
-
await this.pipeline.start();
|
|
138
|
-
|
|
139
|
-
// NOTE: not awaiting here breaks the test.
|
|
140
|
-
await Promise.all(this.feedStore.feeds.map((feed) => this.pipeline.addFeed(feed)));
|
|
141
|
-
this.pipeline.setWriteFeed(this.feed);
|
|
142
|
-
|
|
143
|
-
// consume in async task.
|
|
144
|
-
void this.consume();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async stop() {
|
|
148
|
-
await this.pipeline.stop();
|
|
149
|
-
this.startingTimeframe = this.pipeline.state.timeframe;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async close() {
|
|
153
|
-
await this.feed.close();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
write(message: FeedMessage.Payload) {
|
|
157
|
-
const prev = this.writePromise;
|
|
158
|
-
const promise = this.pipeline.writer!.write(message);
|
|
159
|
-
this.writePromise = Promise.all([prev, promise]);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async consume() {
|
|
163
|
-
for await (const msg of this.pipeline.consume()) {
|
|
164
|
-
this.messages.push(msg);
|
|
165
|
-
}
|
|
166
|
-
log('stopped consuming');
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
type Model = {};
|
|
171
|
-
type Real = {
|
|
172
|
-
feedStore: FeedStore<FeedMessage>;
|
|
173
|
-
agents: Map<string, Agent>;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
class WriteCommand implements fc.AsyncCommand<Model, Real> {
|
|
177
|
-
constructor(public agent: string, public count: number) {}
|
|
178
|
-
|
|
179
|
-
check = () => true;
|
|
180
|
-
|
|
181
|
-
async run(model: Model, real: Real) {
|
|
182
|
-
console.log(`WriteCommand(${this.agent}, ${this.count})`);
|
|
183
|
-
|
|
184
|
-
const agent = real.agents.get(this.agent)!;
|
|
185
|
-
|
|
186
|
-
const toWrite = Math.min(this.count, NUM_MESSAGES - agent.feed.length);
|
|
187
|
-
if (toWrite > 0) {
|
|
188
|
-
for (const _ of range(toWrite)) {
|
|
189
|
-
agent.write({}); // Content is not important.
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
toString = () => `WriteCommand(${this.agent}, ${this.count})`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
class SyncCommand implements fc.AsyncCommand<Model, Real> {
|
|
198
|
-
check = () => true;
|
|
199
|
-
|
|
200
|
-
async run(model: Model, real: Real) {
|
|
201
|
-
console.log('SyncCommand()');
|
|
202
|
-
const targets: any = {};
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
for (const agent of real.agents.values()) {
|
|
206
|
-
await agent.writePromise;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
for (const agent of real.agents.values()) {
|
|
210
|
-
targets[agent.id] = agent.pipeline.state.endTimeframe;
|
|
211
|
-
if (agent.pipeline.state.endTimeframe.isEmpty()) {
|
|
212
|
-
console.log('empty endtimeframe', {
|
|
213
|
-
id: agent.id,
|
|
214
|
-
endTimeframe: agent.pipeline.state.endTimeframe,
|
|
215
|
-
feeds: agent.pipeline.getFeeds().map((feed) => [feed.key.toString(), feed.length]),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
await asyncTimeout(agent.pipeline.state.waitUntilTimeframe(agent.pipeline.state.endTimeframe), 1000);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const tf: Timeframe = real.agents.values().next().value!.pipeline.state.timeframe;
|
|
222
|
-
for (const agent of real.agents.values()) {
|
|
223
|
-
expect(agent.pipeline.state.timeframe.equals(tf)).toEqual(true);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const totalMessages = real.feedStore.feeds.reduce((acc, feed) => acc + feed.length, 0);
|
|
227
|
-
for (const agent of real.agents.values()) {
|
|
228
|
-
expect(agent.messages.length).toEqual(totalMessages);
|
|
229
|
-
}
|
|
230
|
-
} catch (err) {
|
|
231
|
-
console.log(
|
|
232
|
-
inspect(
|
|
233
|
-
{
|
|
234
|
-
agents: Array.from(real.agents.values()).map((agent) => ({
|
|
235
|
-
id: agent.id,
|
|
236
|
-
messages: agent.messages.length,
|
|
237
|
-
feeds: agent.pipeline.getFeeds().map((feed) => [feed.key, feed.length]),
|
|
238
|
-
timeframe: agent.pipeline.state.timeframe,
|
|
239
|
-
endTimeframe: agent.pipeline.state.endTimeframe,
|
|
240
|
-
})),
|
|
241
|
-
feeds: real.feedStore.feeds.map((feed) => [feed.key, feed.length]),
|
|
242
|
-
targets,
|
|
243
|
-
},
|
|
244
|
-
false,
|
|
245
|
-
null,
|
|
246
|
-
true,
|
|
247
|
-
),
|
|
248
|
-
);
|
|
249
|
-
throw err;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
toString = () => 'SyncCommand()';
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
class RestartCommand implements fc.AsyncCommand<Model, Real> {
|
|
257
|
-
constructor(public agent: string) {}
|
|
258
|
-
|
|
259
|
-
check = () => true;
|
|
260
|
-
|
|
261
|
-
async run(model: Model, real: Real) {
|
|
262
|
-
console.log(`RestartCommand(${this.agent})`);
|
|
106
|
+
test('cursor change while polling', async () => {
|
|
107
|
+
const pipeline = new Pipeline();
|
|
108
|
+
afterTest(() => pipeline.stop());
|
|
263
109
|
|
|
264
|
-
|
|
110
|
+
const builder = new TestFeedBuilder();
|
|
111
|
+
const feedStore = builder.createFeedStore();
|
|
112
|
+
const key = await builder.keyring.createKey();
|
|
113
|
+
const feed = await feedStore.openFeed(key, { writable: true });
|
|
114
|
+
await pipeline.addFeed(feed);
|
|
265
115
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
116
|
+
for (const _ of range(20)) {
|
|
117
|
+
await feed.appendWithReceipt(TEST_MESSAGE);
|
|
118
|
+
}
|
|
119
|
+
await feed.clear(0, 10);
|
|
270
120
|
|
|
271
|
-
|
|
121
|
+
const processedSequenceNumbers: number[] = [];
|
|
122
|
+
const expectedSequenceNumbers = range(20).slice(10);
|
|
123
|
+
const processedEvent = new Event();
|
|
124
|
+
setTimeout(async () => {
|
|
125
|
+
for await (const block of pipeline.consume()) {
|
|
126
|
+
processedSequenceNumbers.push(block.seq);
|
|
127
|
+
processedEvent.emit();
|
|
272
128
|
}
|
|
129
|
+
});
|
|
273
130
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const commands = fc.commands(
|
|
278
|
-
[
|
|
279
|
-
fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
|
|
280
|
-
fc.constant(new SyncCommand()),
|
|
281
|
-
anAgentId.map((agent) => new RestartCommand(agent)),
|
|
282
|
-
],
|
|
283
|
-
{ size: 'large' },
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
const model = fc.asyncProperty(commands, async (commands) => {
|
|
287
|
-
const feedStore = builder.createFeedStore();
|
|
288
|
-
|
|
289
|
-
const agents = new Map(agentIds.map((id) => [id, new Agent(id, feedStore)]));
|
|
290
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
|
|
291
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
|
|
292
|
-
|
|
293
|
-
const setup: fc.ModelRunSetup<Model, Real> = () => ({
|
|
294
|
-
model: {},
|
|
295
|
-
real: {
|
|
296
|
-
feedStore,
|
|
297
|
-
agents,
|
|
298
|
-
},
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
try {
|
|
302
|
-
await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
|
|
303
|
-
} finally {
|
|
304
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
|
|
305
|
-
await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
|
|
306
|
-
}
|
|
307
|
-
});
|
|
131
|
+
await pipeline.start();
|
|
132
|
+
await sleep(1000);
|
|
308
133
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
];
|
|
134
|
+
await pipeline.pause();
|
|
135
|
+
await pipeline.setCursor(new Timeframe([[feed.key, 9]]));
|
|
136
|
+
await pipeline.unpause();
|
|
313
137
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
138
|
+
await processedEvent.waitForCondition(() => processedSequenceNumbers.length === 10);
|
|
139
|
+
expect(processedSequenceNumbers).toEqual(expectedSequenceNumbers);
|
|
140
|
+
});
|
|
317
141
|
});
|
package/src/pipeline/pipeline.ts
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import invariant from 'tiny-invariant';
|
|
6
|
-
|
|
7
5
|
import { Event, sleep, synchronized, Trigger } from '@dxos/async';
|
|
8
6
|
import { Context, rejectOnDispose } from '@dxos/context';
|
|
9
7
|
import { failUndefined } from '@dxos/debug';
|
|
10
8
|
import { FeedSetIterator, FeedWrapper, FeedWriter } from '@dxos/feed-store';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
11
10
|
import { PublicKey } from '@dxos/keys';
|
|
12
11
|
import { log } from '@dxos/log';
|
|
13
12
|
import { FeedMessageBlock } from '@dxos/protocols';
|
|
@@ -205,9 +204,6 @@ export class Pipeline implements PipelineAccessor {
|
|
|
205
204
|
private readonly _timeframeClock = new TimeframeClock(new Timeframe());
|
|
206
205
|
private readonly _feeds = new ComplexMap<PublicKey, FeedWrapper<FeedMessage>>(PublicKey.hash);
|
|
207
206
|
|
|
208
|
-
// Inbound feed stream.
|
|
209
|
-
private _feedSetIterator?: FeedSetIterator<FeedMessage>;
|
|
210
|
-
|
|
211
207
|
// External state accessor.
|
|
212
208
|
private readonly _state: PipelineState = new PipelineState(this._feeds, this._timeframeClock);
|
|
213
209
|
|
|
@@ -215,14 +211,20 @@ export class Pipeline implements PipelineAccessor {
|
|
|
215
211
|
private readonly _processingTrigger = new Trigger().wake();
|
|
216
212
|
private readonly _pauseTrigger = new Trigger().wake();
|
|
217
213
|
|
|
218
|
-
|
|
219
|
-
private
|
|
220
|
-
|
|
221
|
-
|
|
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>;
|
|
222
219
|
|
|
223
220
|
// Outbound feed writer.
|
|
224
221
|
private _writer: FeedWriter<FeedMessage.Payload> | undefined;
|
|
225
222
|
|
|
223
|
+
private _isStopping = false;
|
|
224
|
+
private _isStarted = false;
|
|
225
|
+
private _isBeingConsumed = false;
|
|
226
|
+
private _isPaused = false;
|
|
227
|
+
|
|
226
228
|
get state() {
|
|
227
229
|
return this._state;
|
|
228
230
|
}
|
|
@@ -244,10 +246,14 @@ export class Pipeline implements PipelineAccessor {
|
|
|
244
246
|
// which might be opening feeds during the mutation processing, which w
|
|
245
247
|
async addFeed(feed: FeedWrapper<FeedMessage>) {
|
|
246
248
|
this._feeds.set(feed.key, feed);
|
|
249
|
+
|
|
247
250
|
if (this._feedSetIterator) {
|
|
248
251
|
await this._feedSetIterator.addFeed(feed);
|
|
249
252
|
}
|
|
250
|
-
|
|
253
|
+
|
|
254
|
+
if (this._isStarted && !this._isPaused) {
|
|
255
|
+
this._setFeedDownloadState(feed);
|
|
256
|
+
}
|
|
251
257
|
}
|
|
252
258
|
|
|
253
259
|
setWriteFeed(feed: FeedWrapper<FeedMessage>) {
|
|
@@ -265,17 +271,28 @@ export class Pipeline implements PipelineAccessor {
|
|
|
265
271
|
|
|
266
272
|
@synchronized
|
|
267
273
|
async start() {
|
|
274
|
+
invariant(!this._isStarted, 'Pipeline is already started.');
|
|
268
275
|
log('starting...');
|
|
269
276
|
await this._initIterator();
|
|
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;
|
|
@@ -354,6 +369,7 @@ export class Pipeline implements PipelineAccessor {
|
|
|
354
369
|
iterable = lastFeedSetIterator[Symbol.asyncIterator]();
|
|
355
370
|
}
|
|
356
371
|
|
|
372
|
+
// Will be canceled when the iterator gets closed.
|
|
357
373
|
const { done, value } = await iterable.next();
|
|
358
374
|
if (!done) {
|
|
359
375
|
const block = value ?? failUndefined();
|
|
@@ -366,17 +382,27 @@ export class Pipeline implements PipelineAccessor {
|
|
|
366
382
|
}
|
|
367
383
|
|
|
368
384
|
// TODO(burdon): Test re-entrant?
|
|
369
|
-
this.
|
|
385
|
+
this._isBeingConsumed = false;
|
|
370
386
|
}
|
|
371
387
|
|
|
372
388
|
private _setFeedDownloadState(feed: FeedWrapper<FeedMessage>) {
|
|
373
|
-
|
|
374
|
-
|
|
389
|
+
let handle = this._downloads.get(feed); // TODO(burdon): Always undefined.
|
|
390
|
+
if (handle) {
|
|
391
|
+
feed.undownload(handle);
|
|
392
|
+
}
|
|
375
393
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
}
|
|
379
403
|
});
|
|
404
|
+
|
|
405
|
+
this._downloads.set(feed, handle);
|
|
380
406
|
}
|
|
381
407
|
|
|
382
408
|
private async _initIterator() {
|
package/src/space/auth.ts
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
// Copyright 2019 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import invariant from 'tiny-invariant';
|
|
6
|
-
|
|
7
5
|
import { runInContext, scheduleTask } from '@dxos/async';
|
|
8
6
|
import { Context } from '@dxos/context';
|
|
9
7
|
import { randomBytes } from '@dxos/crypto';
|
|
8
|
+
import { invariant } from '@dxos/invariant';
|
|
10
9
|
import { log } from '@dxos/log';
|
|
11
10
|
import { schema } from '@dxos/protocols';
|
|
12
11
|
import { AuthService } from '@dxos/protocols/proto/dxos/mesh/teleport/auth';
|