@fedify/cli 2.3.0-dev.1358 → 2.3.0-dev.1361
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/bench/action.js +29 -189
- package/dist/bench/command.js +13 -43
- package/dist/bench/load/clock.js +2 -20
- package/dist/bench/load/generator.js +9 -42
- package/dist/bench/metrics/stats-client.js +3 -65
- package/dist/bench/mod.js +2 -9
- package/dist/bench/render/markdown.js +0 -1
- package/dist/bench/render/text.js +0 -1
- package/dist/bench/result/build.js +10 -133
- package/dist/bench/result/expect/evaluate.js +1 -1
- package/dist/bench/result/schema.js +3 -353
- package/dist/bench/safety/gate.js +2 -4
- package/dist/bench/scenario/normalize.js +2 -1
- package/dist/bench/scenario/schema.js +9 -50
- package/dist/bench/scenario/validate.js +2 -2
- package/dist/bench/scenarios/inbox.js +12 -4
- package/dist/bench/scenarios/registry.js +1 -19
- package/dist/bench/scenarios/runner.js +1 -21
- package/dist/bench/scenarios/webfinger.js +1 -1
- package/dist/cache.js +1 -1
- package/dist/config.js +1 -1
- package/dist/deno.js +1 -1
- package/dist/docloader.js +1 -1
- package/dist/generate-vocab/action.js +1 -1
- package/dist/generate-vocab/command.js +3 -5
- package/dist/generate-vocab/mod.js +4 -0
- package/dist/imagerenderer.js +2 -2
- package/dist/inbox/command.js +4 -6
- package/dist/inbox.js +4 -4
- package/dist/init/mod.js +3 -0
- package/dist/log.js +2 -2
- package/dist/lookup.js +123 -12
- package/dist/mod.js +23 -2
- package/dist/nodeinfo.js +9 -11
- package/dist/options.js +1 -1
- package/dist/relay/command.js +4 -6
- package/dist/relay.js +2 -2
- package/dist/runner.js +46 -69
- package/dist/tempserver.js +1 -1
- package/dist/tunnel.js +4 -6
- package/dist/utils.js +4 -5
- package/dist/webfinger/action.js +1 -1
- package/dist/webfinger/command.js +4 -6
- package/dist/webfinger/lib.js +1 -1
- package/dist/webfinger/mod.js +4 -0
- package/package.json +12 -13
- package/dist/bench/compare/schema.js +0 -16
- package/dist/bench/compare.js +0 -667
- package/dist/bench/scenarios/actor.js +0 -38
- package/dist/bench/scenarios/failure.js +0 -363
- package/dist/bench/scenarios/fanout.js +0 -261
- package/dist/bench/scenarios/mixed.js +0 -244
- package/dist/bench/scenarios/object-discovery.js +0 -211
- package/dist/bench/scenarios/object.js +0 -54
- package/dist/bench/scenarios/read.js +0 -108
- package/dist/commands.js +0 -110
- package/dist/lookup/command.js +0 -121
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import "@js-temporal/polyfill";
|
|
2
|
-
import { discoverInbox, selectInbox } from "../discovery/discover.js";
|
|
3
|
-
import { systemClock } from "../load/clock.js";
|
|
4
|
-
import { runLoad } from "../load/generator.js";
|
|
5
|
-
import { aggregateSamples } from "../metrics/aggregate.js";
|
|
6
|
-
import { diffSnapshots, fetchServerSnapshot, queueTaskRemaining } from "../metrics/stats-client.js";
|
|
7
|
-
import { loadPlanOf, measuredWindowMs, sendRequest, validateInboxSelector } from "./runner.js";
|
|
8
|
-
import { createActivityIdMinter } from "../signing/activity-id.js";
|
|
9
|
-
import { signInboxDelivery } from "../signing/signer.js";
|
|
10
|
-
import { assertSinkRecipientsAllowed, resolveSinkBase, spawnSinkServer } from "./fanout.js";
|
|
11
|
-
import { Create, Note } from "@fedify/vocab";
|
|
12
|
-
//#region src/bench/scenarios/failure.ts
|
|
13
|
-
/**
|
|
14
|
-
* The `failure` scenario runner.
|
|
15
|
-
* @since 2.3.0
|
|
16
|
-
* @module
|
|
17
|
-
*/
|
|
18
|
-
const SUPPORTED_FAULTS = [
|
|
19
|
-
"invalid-signature",
|
|
20
|
-
"missing-actor",
|
|
21
|
-
"remote-404",
|
|
22
|
-
"remote-410",
|
|
23
|
-
"slow-inbox",
|
|
24
|
-
"network-error"
|
|
25
|
-
];
|
|
26
|
-
const DEFAULT_DRAIN_TIMEOUT_MS = 6e4;
|
|
27
|
-
const DRAIN_POLL_MS = 25;
|
|
28
|
-
/** The `failure` scenario runner. */
|
|
29
|
-
const failureRunner = {
|
|
30
|
-
validate(scenario) {
|
|
31
|
-
const faults = scenario.faults.length < 1 ? ["remote-404"] : scenario.faults;
|
|
32
|
-
const remoteFaults = [...new Set(faults.filter(isRemoteFault))];
|
|
33
|
-
for (const fault of faults) if (!isSupportedFault(fault)) throw new Error(`Scenario "${scenario.name}": unsupported failure fault ${JSON.stringify(fault)}; supported faults: ${SUPPORTED_FAULTS.join(", ")}.`);
|
|
34
|
-
if (faults.some(isInboundFault)) validateInboxSelector(scenario.name, scenario.inbox);
|
|
35
|
-
if (faults.some(isInboundFault) && scenario.recipients.length < 1) throw new Error(`Scenario "${scenario.name}": invalid-signature and missing-actor faults require a recipient.`);
|
|
36
|
-
if (faults.some(isRemoteFault) && scenario.sender == null) throw new Error(`Scenario "${scenario.name}": remote failure faults require a sender.`);
|
|
37
|
-
if (faults.some(isRemoteFault)) resolveSinkBase(scenario.name, scenario.raw.sinkBase);
|
|
38
|
-
if (scenario.raw.sinkBase != null && remoteFaults.includes("network-error") && remoteFaults.some((fault) => fault !== "network-error")) throw new Error(`Scenario "${scenario.name}": sinkBase cannot combine network-error with other remote failure faults because the same port cannot be both open and unreachable.`);
|
|
39
|
-
},
|
|
40
|
-
async run(context) {
|
|
41
|
-
this.validate?.(context.scenario);
|
|
42
|
-
const clock = context.clock ?? systemClock();
|
|
43
|
-
const faults = faultsOf(context);
|
|
44
|
-
const deliveryTarget = await resolveFailureDeliveryTarget(context, faults);
|
|
45
|
-
const remoteTargets = await resolveRemoteFailureTargets(context, faults);
|
|
46
|
-
const remoteActivityIds = createActivityIdMinter(context.target);
|
|
47
|
-
try {
|
|
48
|
-
await assertSinkRecipientsAllowed([...remoteTargets.values()].map((target) => target.recipient), context);
|
|
49
|
-
let index = 0;
|
|
50
|
-
const sendOne = () => sendForFault(context, faults[index++ % faults.length], deliveryTarget, remoteTargets, remoteActivityIds);
|
|
51
|
-
let send = sendOne;
|
|
52
|
-
if (faults.some(isRemoteFault)) {
|
|
53
|
-
let previous = Promise.resolve();
|
|
54
|
-
send = () => {
|
|
55
|
-
const current = previous.then(sendOne);
|
|
56
|
-
previous = current.then(() => {}, () => {});
|
|
57
|
-
return current;
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
return aggregateSamples((await runLoad(loadPlanOf(context.scenario, context.rng), send, clock, context.signal)).samples, {
|
|
61
|
-
measuredWindowMs: measuredWindowMs(context.scenario),
|
|
62
|
-
includeHistogram: true
|
|
63
|
-
});
|
|
64
|
-
} finally {
|
|
65
|
-
await Promise.all([...remoteTargets.values()].map((target) => target.close()));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
function faultsOf(context) {
|
|
70
|
-
return (context.scenario.faults.length < 1 ? ["remote-404"] : context.scenario.faults).map((fault) => {
|
|
71
|
-
if (isSupportedFault(fault)) return fault;
|
|
72
|
-
throw new Error(`Scenario "${context.scenario.name}": unsupported failure fault ${JSON.stringify(fault)}.`);
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
function isSupportedFault(fault) {
|
|
76
|
-
return SUPPORTED_FAULTS.includes(fault);
|
|
77
|
-
}
|
|
78
|
-
async function resolveFailureDeliveryTarget(context, faults) {
|
|
79
|
-
if (!faults.some(isInboundFault)) return null;
|
|
80
|
-
const { scenario } = context;
|
|
81
|
-
const discovered = await discoverInbox(scenario.recipients[0], {
|
|
82
|
-
documentLoader: context.documentLoader,
|
|
83
|
-
contextLoader: context.contextLoader,
|
|
84
|
-
allowPrivateAddress: context.allowPrivateAddress
|
|
85
|
-
});
|
|
86
|
-
const inbox = selectInbox(discovered, scenario.inbox);
|
|
87
|
-
if (faults.every((fault) => fault === "missing-actor")) await context.assertActorlessDestinationAllowed?.(inbox);
|
|
88
|
-
else await context.assertDestinationAllowed?.(inbox);
|
|
89
|
-
return {
|
|
90
|
-
inbox,
|
|
91
|
-
actorUri: discovered.actorUri
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function isInboundFault(fault) {
|
|
95
|
-
return fault === "invalid-signature" || fault === "missing-actor";
|
|
96
|
-
}
|
|
97
|
-
function isRemoteFault(fault) {
|
|
98
|
-
return fault === "remote-404" || fault === "remote-410" || fault === "slow-inbox" || fault === "network-error";
|
|
99
|
-
}
|
|
100
|
-
async function resolveRemoteFailureTargets(context, faults) {
|
|
101
|
-
const targets = /* @__PURE__ */ new Map();
|
|
102
|
-
try {
|
|
103
|
-
const remoteFaults = [...new Set(faults.filter(isRemoteFault))];
|
|
104
|
-
const liveFaults = remoteFaults.filter((fault) => fault !== "network-error");
|
|
105
|
-
if (liveFaults.length > 0) {
|
|
106
|
-
const sink = await spawnSinkServer({
|
|
107
|
-
followers: liveFaults.length,
|
|
108
|
-
rawBehavior: null,
|
|
109
|
-
rawBehaviors: liveFaults.map(remoteSinkBehavior),
|
|
110
|
-
advertiseHost: context.advertiseHost,
|
|
111
|
-
sinkBase: context.scenario.raw.sinkBase
|
|
112
|
-
});
|
|
113
|
-
const close = once(sink.close);
|
|
114
|
-
for (const [index, fault] of liveFaults.entries()) targets.set(fault, {
|
|
115
|
-
recipient: sink.recipients[index],
|
|
116
|
-
close
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
if (remoteFaults.includes("network-error")) {
|
|
120
|
-
const sink = await spawnSinkServer({
|
|
121
|
-
followers: 1,
|
|
122
|
-
rawBehavior: remoteSinkBehavior("network-error"),
|
|
123
|
-
advertiseHost: context.advertiseHost,
|
|
124
|
-
sinkBase: context.scenario.raw.sinkBase
|
|
125
|
-
});
|
|
126
|
-
const recipient = sink.recipients[0];
|
|
127
|
-
try {
|
|
128
|
-
await sink.close();
|
|
129
|
-
targets.set("network-error", {
|
|
130
|
-
recipient,
|
|
131
|
-
close: () => Promise.resolve()
|
|
132
|
-
});
|
|
133
|
-
} catch (error) {
|
|
134
|
-
await sink.close().catch(() => {});
|
|
135
|
-
throw error;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return targets;
|
|
139
|
-
} catch (error) {
|
|
140
|
-
await Promise.all([...targets.values()].map((target) => target.close()));
|
|
141
|
-
throw error;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
function once(close) {
|
|
145
|
-
let closed = null;
|
|
146
|
-
return () => {
|
|
147
|
-
closed ??= close();
|
|
148
|
-
return closed;
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function remoteSinkBehavior(fault) {
|
|
152
|
-
switch (fault) {
|
|
153
|
-
case "remote-404": return { status: 404 };
|
|
154
|
-
case "remote-410": return { status: 410 };
|
|
155
|
-
case "slow-inbox": return {
|
|
156
|
-
status: 202,
|
|
157
|
-
latency: "25ms"
|
|
158
|
-
};
|
|
159
|
-
case "network-error": return { status: 202 };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
async function sendForFault(context, fault, deliveryTarget, remoteTargets, remoteActivityIds) {
|
|
163
|
-
switch (fault) {
|
|
164
|
-
case "invalid-signature": return await sendInvalidSignature(context, requiredTarget(deliveryTarget));
|
|
165
|
-
case "missing-actor": return await sendMissingActor(context, requiredTarget(deliveryTarget));
|
|
166
|
-
case "remote-404":
|
|
167
|
-
case "remote-410":
|
|
168
|
-
case "slow-inbox":
|
|
169
|
-
case "network-error": return await sendRemoteFailure(context, fault, requiredRemoteTarget(fault, remoteTargets), remoteActivityIds);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
async function sendRemoteFailure(context, fault, target, remoteActivityIds) {
|
|
173
|
-
const fetchImpl = context.fetch ?? fetch;
|
|
174
|
-
const baseline = await fetchServerSnapshot(context.target, fetchImpl);
|
|
175
|
-
const response = await fetchImpl(new URL("/.well-known/fedify/bench/trigger", context.target), {
|
|
176
|
-
method: "POST",
|
|
177
|
-
headers: { "content-type": "application/json" },
|
|
178
|
-
redirect: "manual",
|
|
179
|
-
body: JSON.stringify({
|
|
180
|
-
sender: { identifier: requiredSender(context) },
|
|
181
|
-
recipients: [target.recipient],
|
|
182
|
-
activity: buildRemoteFailureActivity(context, remoteActivityIds.next())
|
|
183
|
-
})
|
|
184
|
-
});
|
|
185
|
-
await response.arrayBuffer().catch(() => {});
|
|
186
|
-
if (!response.ok) return {
|
|
187
|
-
ok: false,
|
|
188
|
-
status: response.status,
|
|
189
|
-
reason: `status_${response.status}`
|
|
190
|
-
};
|
|
191
|
-
const observation = await waitForRemoteFault({
|
|
192
|
-
target: context.target,
|
|
193
|
-
fetch: fetchImpl,
|
|
194
|
-
baseline,
|
|
195
|
-
fault,
|
|
196
|
-
clock: context.clock ?? systemClock(),
|
|
197
|
-
signal: context.signal,
|
|
198
|
-
timeoutMs: context.scenario.queueDrainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS
|
|
199
|
-
});
|
|
200
|
-
if (observation == null) return {
|
|
201
|
-
ok: false,
|
|
202
|
-
errorKind: "server",
|
|
203
|
-
reason: "stats_unavailable"
|
|
204
|
-
};
|
|
205
|
-
if (observation.timedOut) return {
|
|
206
|
-
ok: false,
|
|
207
|
-
errorKind: "server",
|
|
208
|
-
reason: "expected_remote_failure_not_observed"
|
|
209
|
-
};
|
|
210
|
-
return expectedRemoteFailure(fault);
|
|
211
|
-
}
|
|
212
|
-
function buildRemoteFailureActivity(context, id) {
|
|
213
|
-
const objectId = new URL(`/objects/${crypto.randomUUID()}`, context.target);
|
|
214
|
-
return {
|
|
215
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
216
|
-
type: "Create",
|
|
217
|
-
id: id.href,
|
|
218
|
-
actor: new URL(`/users/${requiredSender(context)}`, context.target).href,
|
|
219
|
-
object: {
|
|
220
|
-
type: "Note",
|
|
221
|
-
id: objectId.href,
|
|
222
|
-
content: "Benchmark failure activity."
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
async function waitForRemoteFault(options) {
|
|
227
|
-
if (options.baseline == null) return null;
|
|
228
|
-
const baselineRemaining = queueTaskRemaining(options.baseline) ?? 0;
|
|
229
|
-
const deadline = options.clock.now() + options.timeoutMs;
|
|
230
|
-
do {
|
|
231
|
-
throwIfAborted(options.signal);
|
|
232
|
-
const snapshot = await fetchServerSnapshot(options.target, options.fetch);
|
|
233
|
-
throwIfAborted(options.signal);
|
|
234
|
-
if (snapshot != null) {
|
|
235
|
-
const diff = diffSnapshots(options.baseline, snapshot);
|
|
236
|
-
const queueTasks = diff.queueTasks;
|
|
237
|
-
if (options.fault === "remote-404" || options.fault === "remote-410") {
|
|
238
|
-
if ((diff.deliveryPermanentFailures ?? 0) > 0) return { timedOut: false };
|
|
239
|
-
} else if (queueTasks != null) {
|
|
240
|
-
const remaining = queueTaskRemaining(diff, baselineRemaining);
|
|
241
|
-
if (remaining != null) {
|
|
242
|
-
if (options.fault === "slow-inbox") {
|
|
243
|
-
if (queueTasks.completed > 0 && remaining === 0) return { timedOut: false };
|
|
244
|
-
} else if (options.fault === "network-error") {
|
|
245
|
-
if (queueTasks.failed > 0 || queueTasks.completed > 0 && remaining > 0) return { timedOut: false };
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
const now = options.clock.now();
|
|
251
|
-
if (now >= deadline) break;
|
|
252
|
-
await options.clock.sleepUntil(Math.min(deadline, now + DRAIN_POLL_MS), options.signal);
|
|
253
|
-
} while (options.clock.now() < deadline);
|
|
254
|
-
return { timedOut: true };
|
|
255
|
-
}
|
|
256
|
-
function throwIfAborted(signal) {
|
|
257
|
-
if (signal?.aborted) throw abortReason(signal);
|
|
258
|
-
}
|
|
259
|
-
function abortReason(signal) {
|
|
260
|
-
return signal.reason ?? /* @__PURE__ */ new Error("Operation aborted.");
|
|
261
|
-
}
|
|
262
|
-
function expectedRemoteFailure(fault) {
|
|
263
|
-
switch (fault) {
|
|
264
|
-
case "remote-404": return {
|
|
265
|
-
ok: true,
|
|
266
|
-
status: 404
|
|
267
|
-
};
|
|
268
|
-
case "remote-410": return {
|
|
269
|
-
ok: true,
|
|
270
|
-
status: 410
|
|
271
|
-
};
|
|
272
|
-
case "slow-inbox": return {
|
|
273
|
-
ok: true,
|
|
274
|
-
status: 202
|
|
275
|
-
};
|
|
276
|
-
case "network-error": return {
|
|
277
|
-
ok: true,
|
|
278
|
-
errorKind: "network",
|
|
279
|
-
reason: "expected_network_error"
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
async function sendInvalidSignature(context, deliveryTarget) {
|
|
284
|
-
const request = await signedFailureRequest(context, "invalid-signature", deliveryTarget);
|
|
285
|
-
const body = await request.arrayBuffer();
|
|
286
|
-
const headers = new Headers(request.headers);
|
|
287
|
-
invalidateSignedDate(headers);
|
|
288
|
-
return expectedFailure(await sendRequest(new Request(request.url, {
|
|
289
|
-
method: request.method,
|
|
290
|
-
headers,
|
|
291
|
-
body,
|
|
292
|
-
redirect: "manual"
|
|
293
|
-
}), context.fetch ?? fetch), 401);
|
|
294
|
-
}
|
|
295
|
-
function invalidateSignedDate(headers) {
|
|
296
|
-
const timestamp = Date.parse(headers.get("date") ?? "");
|
|
297
|
-
const shifted = Number.isNaN(timestamp) ? /* @__PURE__ */ new Date() : new Date(timestamp + 1e3);
|
|
298
|
-
headers.set("date", shifted.toUTCString());
|
|
299
|
-
}
|
|
300
|
-
async function sendMissingActor(context, deliveryTarget) {
|
|
301
|
-
return expectedFailure(await sendRequest(await signedFailureRequest(context, "missing-actor", deliveryTarget), context.fetch ?? fetch), 401);
|
|
302
|
-
}
|
|
303
|
-
async function signedFailureRequest(context, fault, deliveryTarget) {
|
|
304
|
-
const { fleet, scenario } = context;
|
|
305
|
-
if (fleet == null || fleet.actors.length < 1) throw new Error("The failure scenario requires the synthetic actor server.");
|
|
306
|
-
if (scenario.recipients.length < 1) throw new Error("The invalid-signature and missing-actor faults require a recipient.");
|
|
307
|
-
const actor = fault === "missing-actor" ? missingActor(fleet.actors[0], context.target) : fleet.actors[0];
|
|
308
|
-
const id = createActivityIdMinter(fleet.url).next();
|
|
309
|
-
const note = new Note({
|
|
310
|
-
id: new URL(`/objects/${crypto.randomUUID()}`, fleet.url),
|
|
311
|
-
attribution: actor.id,
|
|
312
|
-
content: "Benchmark failure activity.",
|
|
313
|
-
to: deliveryTarget.actorUri
|
|
314
|
-
});
|
|
315
|
-
const activity = new Create({
|
|
316
|
-
id,
|
|
317
|
-
actor: actor.id,
|
|
318
|
-
object: note,
|
|
319
|
-
to: deliveryTarget.actorUri
|
|
320
|
-
});
|
|
321
|
-
return await signInboxDelivery({
|
|
322
|
-
actor,
|
|
323
|
-
inbox: deliveryTarget.inbox,
|
|
324
|
-
activity,
|
|
325
|
-
contextLoader: context.contextLoader
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
function requiredTarget(target) {
|
|
329
|
-
if (target == null) throw new Error("The invalid-signature and missing-actor faults require discovery.");
|
|
330
|
-
return target;
|
|
331
|
-
}
|
|
332
|
-
function requiredRemoteTarget(fault, targets) {
|
|
333
|
-
const target = targets.get(fault);
|
|
334
|
-
if (target == null) throw new Error(`The ${fault} fault requires a benchmark sink.`);
|
|
335
|
-
return target;
|
|
336
|
-
}
|
|
337
|
-
function requiredSender(context) {
|
|
338
|
-
const sender = context.scenario.sender;
|
|
339
|
-
if (sender == null) throw new Error("Remote failure faults require a sender.");
|
|
340
|
-
return sender;
|
|
341
|
-
}
|
|
342
|
-
function missingActor(actor, target) {
|
|
343
|
-
const id = new URL(`/__fedify_bench/missing/${crypto.randomUUID()}`, target);
|
|
344
|
-
return {
|
|
345
|
-
...actor,
|
|
346
|
-
id,
|
|
347
|
-
rsaKeyId: actor.rsaKeyId == null ? void 0 : new URL("#main-key", id),
|
|
348
|
-
ed25519KeyId: actor.ed25519KeyId == null ? void 0 : new URL("#ed25519-key", id)
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
function expectedFailure(outcome, expectedStatus) {
|
|
352
|
-
if (outcome.status === expectedStatus) return {
|
|
353
|
-
ok: true,
|
|
354
|
-
status: outcome.status
|
|
355
|
-
};
|
|
356
|
-
return {
|
|
357
|
-
...outcome,
|
|
358
|
-
ok: false,
|
|
359
|
-
reason: outcome.reason ?? "expected_failure_not_observed"
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
//#endregion
|
|
363
|
-
export { failureRunner };
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import "@js-temporal/polyfill";
|
|
2
|
-
import { parseDuration } from "../scenario/units.js";
|
|
3
|
-
import { systemClock } from "../load/clock.js";
|
|
4
|
-
import { runLoad } from "../load/generator.js";
|
|
5
|
-
import { LogLinearHistogram } from "../metrics/histogram.js";
|
|
6
|
-
import { aggregateSamples } from "../metrics/aggregate.js";
|
|
7
|
-
import { diffSnapshots, fetchServerSnapshot, queueTaskRemaining } from "../metrics/stats-client.js";
|
|
8
|
-
import { loadPlanOf, measuredWindowMs } from "./runner.js";
|
|
9
|
-
import { createActivityIdMinter } from "../signing/activity-id.js";
|
|
10
|
-
import { resolveAdvertiseHost } from "../server/synthetic.js";
|
|
11
|
-
import { serve } from "srvx";
|
|
12
|
-
//#region src/bench/scenarios/fanout.ts
|
|
13
|
-
/**
|
|
14
|
-
* The `fanout` scenario runner.
|
|
15
|
-
* @since 2.3.0
|
|
16
|
-
* @module
|
|
17
|
-
*/
|
|
18
|
-
const DEFAULT_FOLLOWERS = 5;
|
|
19
|
-
const DEFAULT_DRAIN_TIMEOUT_MS = 6e4;
|
|
20
|
-
const DRAIN_POLL_MS = 25;
|
|
21
|
-
/** The `fanout` scenario runner. */
|
|
22
|
-
const fanoutRunner = {
|
|
23
|
-
validate(scenario) {
|
|
24
|
-
if (triggerKind(scenario.raw.trigger) !== "benchmark-hook") throw new Error(`Scenario "${scenario.name}": fanout currently supports only trigger.kind: "benchmark-hook".`);
|
|
25
|
-
resolveSinkBase(scenario.name, scenario.raw.sinkBase);
|
|
26
|
-
},
|
|
27
|
-
async run(context) {
|
|
28
|
-
if (context.scenario.sender == null) throw new Error("The fanout scenario requires a sender.");
|
|
29
|
-
this.validate?.(context.scenario);
|
|
30
|
-
const clock = context.clock ?? systemClock();
|
|
31
|
-
const fetchImpl = context.fetch ?? fetch;
|
|
32
|
-
const sink = await spawnSinkServer({
|
|
33
|
-
followers: context.scenario.followers ?? DEFAULT_FOLLOWERS,
|
|
34
|
-
rawBehavior: context.scenario.raw.sinkBehavior,
|
|
35
|
-
advertiseHost: context.advertiseHost,
|
|
36
|
-
sinkBase: context.scenario.raw.sinkBase
|
|
37
|
-
});
|
|
38
|
-
const minter = createActivityIdMinter(context.target);
|
|
39
|
-
const drainHistogram = new LogLinearHistogram();
|
|
40
|
-
let delivered = 0;
|
|
41
|
-
try {
|
|
42
|
-
await assertSinkRecipientsAllowed(sink.recipients, context);
|
|
43
|
-
const sendOne = async (scheduledAtMs) => {
|
|
44
|
-
const baseline = await fetchServerSnapshot(context.target, fetchImpl);
|
|
45
|
-
const started = Date.now();
|
|
46
|
-
const response = await fetchImpl(new URL("/.well-known/fedify/bench/trigger", context.target), {
|
|
47
|
-
method: "POST",
|
|
48
|
-
headers: { "content-type": "application/json" },
|
|
49
|
-
redirect: "manual",
|
|
50
|
-
body: JSON.stringify({
|
|
51
|
-
sender: { identifier: context.scenario.sender },
|
|
52
|
-
recipients: sink.recipients,
|
|
53
|
-
activity: buildActivity(context, minter.next())
|
|
54
|
-
})
|
|
55
|
-
});
|
|
56
|
-
await response.arrayBuffer().catch(() => {});
|
|
57
|
-
if (!response.ok) return {
|
|
58
|
-
ok: false,
|
|
59
|
-
status: response.status,
|
|
60
|
-
reason: `status_${response.status}`
|
|
61
|
-
};
|
|
62
|
-
const drain = await waitForDrain({
|
|
63
|
-
target: context.target,
|
|
64
|
-
fetch: fetchImpl,
|
|
65
|
-
baseline,
|
|
66
|
-
clock,
|
|
67
|
-
signal: context.signal,
|
|
68
|
-
timeoutMs: context.scenario.queueDrainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS
|
|
69
|
-
});
|
|
70
|
-
if (drain == null) return {
|
|
71
|
-
ok: false,
|
|
72
|
-
errorKind: "server",
|
|
73
|
-
reason: "stats_unavailable"
|
|
74
|
-
};
|
|
75
|
-
if (drain.timedOut) return {
|
|
76
|
-
ok: false,
|
|
77
|
-
errorKind: "server",
|
|
78
|
-
reason: "queue_drain_timeout"
|
|
79
|
-
};
|
|
80
|
-
if (drain.failed > 0) return {
|
|
81
|
-
ok: false,
|
|
82
|
-
errorKind: "server",
|
|
83
|
-
reason: "queue_delivery_failed"
|
|
84
|
-
};
|
|
85
|
-
if (scheduledAtMs >= context.scenario.warmupMs) {
|
|
86
|
-
drainHistogram.record(Date.now() - started);
|
|
87
|
-
delivered += sink.recipients.length;
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
ok: true,
|
|
91
|
-
status: response.status
|
|
92
|
-
};
|
|
93
|
-
};
|
|
94
|
-
let previous = Promise.resolve();
|
|
95
|
-
const send = (scheduledAtMs) => {
|
|
96
|
-
const current = previous.then(() => sendOne(scheduledAtMs));
|
|
97
|
-
previous = current.then(() => {}, () => {});
|
|
98
|
-
return current;
|
|
99
|
-
};
|
|
100
|
-
const measurement = aggregateSamples((await runLoad(loadPlanOf(context.scenario, context.rng), send, clock, context.signal)).samples, {
|
|
101
|
-
measuredWindowMs: measuredWindowMs(context.scenario),
|
|
102
|
-
includeHistogram: true
|
|
103
|
-
});
|
|
104
|
-
const server = addQueueDrain(measurement.server, drainHistogram);
|
|
105
|
-
const deliveryThroughputPerSec = delivered / (Math.max(measuredWindowMs(context.scenario), 1) / 1e3);
|
|
106
|
-
return {
|
|
107
|
-
...measurement,
|
|
108
|
-
deliveryThroughputPerSec,
|
|
109
|
-
server
|
|
110
|
-
};
|
|
111
|
-
} finally {
|
|
112
|
-
await sink.close();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
function triggerKind(trigger) {
|
|
117
|
-
if (trigger == null) return "benchmark-hook";
|
|
118
|
-
if (typeof trigger !== "object" || Array.isArray(trigger)) return "";
|
|
119
|
-
const kind = trigger.kind;
|
|
120
|
-
return typeof kind === "string" ? kind : "benchmark-hook";
|
|
121
|
-
}
|
|
122
|
-
function buildActivity(context, id) {
|
|
123
|
-
const objectId = new URL(`/objects/${crypto.randomUUID()}`, context.target);
|
|
124
|
-
return {
|
|
125
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
126
|
-
type: "Create",
|
|
127
|
-
id: id.href,
|
|
128
|
-
actor: new URL(`/users/${context.scenario.sender}`, context.target).href,
|
|
129
|
-
object: {
|
|
130
|
-
type: "Note",
|
|
131
|
-
id: objectId.href,
|
|
132
|
-
content: "Benchmark fanout activity."
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
async function assertSinkRecipientsAllowed(recipients, context) {
|
|
137
|
-
for (const recipient of recipients) {
|
|
138
|
-
if (typeof recipient.inbox !== "string") continue;
|
|
139
|
-
await context.assertActorlessDestinationAllowed?.(new URL(recipient.inbox), context.scenario);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
async function spawnSinkServer(options) {
|
|
143
|
-
const sinkBase = resolveSinkBase("benchmark sink", options.sinkBase);
|
|
144
|
-
const advertised = options.advertiseHost == null ? null : resolveAdvertiseHost(options.advertiseHost);
|
|
145
|
-
const behaviors = Array.from({ length: options.followers }, (_, i) => parseSinkBehavior(options.rawBehaviors?.[i] ?? options.rawBehavior));
|
|
146
|
-
const server = serve({
|
|
147
|
-
port: sinkBase?.port ?? 0,
|
|
148
|
-
hostname: sinkBase?.bindHost ?? advertised?.bindHost ?? "127.0.0.1",
|
|
149
|
-
silent: true,
|
|
150
|
-
async fetch(request) {
|
|
151
|
-
const match = /^\/inbox\/(\d+)(?:\/|$)/.exec(new URL(request.url).pathname);
|
|
152
|
-
if (match != null) {
|
|
153
|
-
const behavior = behaviors[Number(match[1])] ?? parseSinkBehavior(options.rawBehavior);
|
|
154
|
-
await request.arrayBuffer().catch(() => {});
|
|
155
|
-
if (behavior.latencyMs > 0) await new Promise((resolve) => setTimeout(resolve, behavior.latencyMs));
|
|
156
|
-
return new Response("accepted", { status: behavior.status });
|
|
157
|
-
}
|
|
158
|
-
return new Response("Not found", { status: 404 });
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
await server.ready();
|
|
162
|
-
const bound = new URL(server.url);
|
|
163
|
-
const base = sinkBase?.base ?? (advertised == null ? bound : new URL(`http://${advertised.urlHost}:${bound.port}/`));
|
|
164
|
-
return {
|
|
165
|
-
recipients: Array.from({ length: options.followers }, (_, i) => ({
|
|
166
|
-
"@context": "https://www.w3.org/ns/activitystreams",
|
|
167
|
-
type: "Service",
|
|
168
|
-
id: new URL(`/actors/${i}`, base).href,
|
|
169
|
-
inbox: new URL(`/inbox/${i}`, base).href
|
|
170
|
-
})),
|
|
171
|
-
close: () => server.close(true)
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
function resolveSinkBase(scenarioName, value) {
|
|
175
|
-
if (value == null) return null;
|
|
176
|
-
let url;
|
|
177
|
-
try {
|
|
178
|
-
url = new URL(value);
|
|
179
|
-
} catch {
|
|
180
|
-
throw new Error(`Scenario "${scenarioName}": sinkBase must be an http URL with an explicit port; got ${JSON.stringify(value)}.`);
|
|
181
|
-
}
|
|
182
|
-
const port = Number(url.port);
|
|
183
|
-
if (url.protocol !== "http:" || url.hostname === "" || url.username !== "" || url.password !== "" || url.port === "" || !Number.isInteger(port) || port < 1 || url.pathname !== "/" || url.search !== "" || url.hash !== "") throw new Error(`Scenario "${scenarioName}": sinkBase must be an http URL with a host, an explicit non-zero port, no credentials, and no path, query, or fragment; got ${JSON.stringify(value)}.`);
|
|
184
|
-
const advertised = resolveAdvertiseHost(url.hostname);
|
|
185
|
-
return {
|
|
186
|
-
base: new URL(`http://${advertised.urlHost}:${url.port}/`),
|
|
187
|
-
bindHost: advertised.bindHost,
|
|
188
|
-
port
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
function parseSinkBehavior(raw) {
|
|
192
|
-
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return {
|
|
193
|
-
latencyMs: 0,
|
|
194
|
-
status: 202
|
|
195
|
-
};
|
|
196
|
-
const record = raw;
|
|
197
|
-
const latency = record.latency;
|
|
198
|
-
const status = record.status;
|
|
199
|
-
let latencyMs = 0;
|
|
200
|
-
if (typeof latency === "string") try {
|
|
201
|
-
latencyMs = parseDuration(latency);
|
|
202
|
-
} catch {
|
|
203
|
-
latencyMs = 0;
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
latencyMs,
|
|
207
|
-
status: typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 ? status : 202
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
async function waitForDrain(options) {
|
|
211
|
-
if (options.baseline == null) return null;
|
|
212
|
-
const baselineRemaining = queueTaskRemaining(options.baseline) ?? 0;
|
|
213
|
-
const deadline = options.clock.now() + options.timeoutMs;
|
|
214
|
-
do {
|
|
215
|
-
throwIfAborted(options.signal);
|
|
216
|
-
const snapshot = await fetchServerSnapshot(options.target, options.fetch);
|
|
217
|
-
throwIfAborted(options.signal);
|
|
218
|
-
if (snapshot != null) {
|
|
219
|
-
const diff = diffSnapshots(options.baseline, snapshot);
|
|
220
|
-
const queueTasks = diff.queueTasks;
|
|
221
|
-
const remaining = queueTaskRemaining(diff, baselineRemaining);
|
|
222
|
-
if (queueTasks != null && queueTasks.enqueued > 0 && remaining != null && remaining === 0) return {
|
|
223
|
-
timedOut: false,
|
|
224
|
-
failed: queueTasks.failed
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
const now = options.clock.now();
|
|
228
|
-
if (now >= deadline) break;
|
|
229
|
-
await options.clock.sleepUntil(Math.min(deadline, now + DRAIN_POLL_MS), options.signal);
|
|
230
|
-
} while (options.clock.now() < deadline);
|
|
231
|
-
return {
|
|
232
|
-
timedOut: true,
|
|
233
|
-
failed: 0
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
function throwIfAborted(signal) {
|
|
237
|
-
if (signal?.aborted) throw abortReason(signal);
|
|
238
|
-
}
|
|
239
|
-
function abortReason(signal) {
|
|
240
|
-
return signal.reason ?? /* @__PURE__ */ new Error("Operation aborted.");
|
|
241
|
-
}
|
|
242
|
-
function addQueueDrain(server, histogram) {
|
|
243
|
-
if (histogram.count < 1) return server;
|
|
244
|
-
const queue = {
|
|
245
|
-
...server?.queue ?? {},
|
|
246
|
-
drainMs: partialFromHistogram(histogram)
|
|
247
|
-
};
|
|
248
|
-
return {
|
|
249
|
-
...server ?? {},
|
|
250
|
-
queue
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
function partialFromHistogram(histogram) {
|
|
254
|
-
return {
|
|
255
|
-
p50: histogram.percentile(50),
|
|
256
|
-
p95: histogram.percentile(95),
|
|
257
|
-
p99: histogram.percentile(99)
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
//#endregion
|
|
261
|
-
export { assertSinkRecipientsAllowed, fanoutRunner, resolveSinkBase, spawnSinkServer };
|