@fedify/fedify 2.4.0-dev.1567 → 2.4.0-dev.1570
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 +4 -0
- package/dist/{assert-OguE97r2.mjs → assert-DvVHoIZ0.mjs} +1 -1
- package/dist/{assert_instance_of-DBC5X09g.mjs → assert_instance_of-CxoC8bVq.mjs} +1 -1
- package/dist/{assert_not_equals-DkVK8oqV.mjs → assert_not_equals-C8vcTTA3.mjs} +1 -1
- package/dist/{assert_rejects-DN60FHPX.mjs → assert_rejects-DAdaaIWM.mjs} +2 -2
- package/dist/{assert_strict_equals-XEgZAlrj.mjs → assert_strict_equals-BzqyKuYL.mjs} +1 -1
- package/dist/{assert_throws-BOkhLGYc.mjs → assert_throws-zN79lgIC.mjs} +1 -1
- package/dist/{builder-DGWCUaNQ.mjs → builder-CJnWbgbj.mjs} +23 -3
- package/dist/codec-nfBU4Up4.mjs +124 -0
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +3 -3
- package/dist/compat/public-audience.test.mjs +3 -3
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/{context-DVoTs_wM.mjs → context-BBRTgkrs.mjs} +8 -2
- package/dist/{context-BBVLF7lx.d.cts → context-BLe0NPtZ.d.cts} +530 -259
- package/dist/{context-BU6jSQdo.d.ts → context-GfQA-CqB.d.ts} +529 -259
- package/dist/{deno-Dho0TW0X.mjs → deno-klNxGW7W.mjs} +1 -1
- package/dist/{docloader-DCQSgDOg.mjs → docloader-CuFH35j1.mjs} +3 -3
- package/dist/federation/builder.test.mjs +5 -5
- package/dist/federation/circuit-breaker.test.mjs +5 -5
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +13 -23
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +5 -5
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +4 -4
- package/dist/federation/middleware.test.mjs +204 -19
- package/dist/federation/mod.cjs +3 -259
- package/dist/federation/mod.d.cts +4 -4
- package/dist/federation/mod.d.ts +4 -4
- package/dist/federation/mod.js +1 -257
- package/dist/federation/mq.test.mjs +103 -261
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/retry.test.mjs +3 -3
- package/dist/federation/router.test.mjs +5 -5
- package/dist/federation/send.test.mjs +10 -10
- package/dist/federation/tasks/codec.test.d.mts +2 -0
- package/dist/federation/tasks/codec.test.mjs +302 -0
- package/dist/federation/tasks/enqueue.test.d.mts +2 -0
- package/dist/federation/tasks/enqueue.test.mjs +1057 -0
- package/dist/federation/tasks/tasks.test.d.mts +2 -0
- package/dist/federation/tasks/tasks.test.mjs +1228 -0
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +6 -6
- package/dist/{http-sngIMb9S.js → http-D0jH0kco.js} +16 -80
- package/dist/{http-C7Lot0it.cjs → http-DeuPW1Oh.cjs} +14 -78
- package/dist/{http-Bf8YhWOi.mjs → http-JdRPUYku.mjs} +4 -4
- package/dist/{key-3rYgSl9f.mjs → key-o0_ipvUR.mjs} +10 -76
- package/dist/{kv-cache-CO2IaDJu.cjs → kv-cache-C3rqzQvu.cjs} +1 -1
- package/dist/{kv-cache-P3scUO6t.mjs → kv-cache-CM7w20U5.mjs} +1 -1
- package/dist/{kv-cache-B0siRaoZ.js → kv-cache-DSWq2JHU.js} +1 -1
- package/dist/{ld-CCOPy-Co.mjs → ld-C__CEgQH.mjs} +3 -3
- package/dist/{metrics-CR4UD_gF.mjs → metrics-CLSPm4dZ.mjs} +8 -6
- package/dist/{mq-D-nlpY04.d.ts → metrics-CoUJLQhZ.d.cts} +69 -1
- package/dist/{mq-D8uSFzxe.d.cts → metrics-Dc4jR-IC.d.ts} +69 -1
- package/dist/{middleware-DUelXs9h.cjs → middleware-B502nwxx.cjs} +874 -134
- package/dist/{middleware-CIteB8IS.mjs → middleware-CdMiavoh.mjs} +1 -1
- package/dist/{middleware-DfBPuuvp.mjs → middleware-CjHqy-1i.mjs} +469 -150
- package/dist/{middleware-BoNiH9CQ.js → middleware-zuLSkPIk.js} +863 -135
- package/dist/{mod-C0F6kvgS.d.cts → mod-B8Efb9qp.d.cts} +1 -1
- package/dist/{mod-yvIXFAEi.d.cts → mod-BtEaIEcD.d.cts} +1 -31
- package/dist/{mod-vPYVoa5n.d.ts → mod-Cmr39iWq.d.ts} +1 -1
- package/dist/{mod-DFvNJcNb.d.ts → mod-UMmveiO0.d.ts} +1 -31
- package/dist/mod.cjs +6 -6
- package/dist/mod.d.cts +6 -6
- package/dist/mod.d.ts +6 -6
- package/dist/mod.js +5 -5
- package/dist/mq-03vS-C9P.mjs +276 -0
- package/dist/nodeinfo/client.test.mjs +3 -3
- package/dist/nodeinfo/handler.test.mjs +5 -5
- package/dist/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/{outgoing-jsonld-L_DbOaFe.mjs → outgoing-jsonld-BNL8AC14.mjs} +1 -1
- package/dist/{owner-TUz_H-M9.mjs → owner-gz4TUfKG.mjs} +2 -2
- package/dist/{proof-DzKUmE7R.cjs → proof-CiDzRtr0.cjs} +1 -1
- package/dist/{proof-CrI3UpuA.mjs → proof-CuQpZ_AT.mjs} +5 -5
- package/dist/{proof-C5i7MhbD.js → proof-DQWTf6oB.js} +1 -1
- package/dist/{send-BBs_erdP.mjs → send-B60teucv.mjs} +3 -3
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/http.test.mjs +8 -8
- package/dist/sig/key.test.mjs +7 -191
- package/dist/sig/ld.test.mjs +7 -7
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +6 -6
- package/dist/sig/proof.test.mjs +9 -69
- package/dist/{std__assert-BBjXFNOb.mjs → std__assert-Dh6uLf_q.mjs} +4 -4
- package/dist/{temporal-bBIy2G-F.mjs → temporal-CvVMrvJJ.mjs} +1 -1
- package/dist/testing/mod.d.mts +1037 -8
- package/dist/testing/mod.mjs +3 -2
- package/dist/testing-Dy-WhqpN.mjs +77 -0
- package/dist/utils/docloader.test.mjs +6 -6
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +8 -6
- /package/dist/{accept-CPkZzmGN.mjs → accept-CgDcxvjV.mjs} +0 -0
- /package/dist/{activity-listener-tztVvlNb.mjs → activity-listener-BeTGV3wc.mjs} +0 -0
- /package/dist/{assert_equals-C-ZRDbaf.mjs → assert_equals-PBVKNMJd.mjs} +0 -0
- /package/dist/{circuit-breaker-CSWsyoef.mjs → circuit-breaker-hJBB6jwA.mjs} +0 -0
- /package/dist/{client-ByXmQhYD.mjs → client-B_A6mfn3.mjs} +0 -0
- /package/dist/{collection-Cc3DVAhE.mjs → collection-Dp5ky45w.mjs} +0 -0
- /package/dist/{keycache-BeU0LCII.mjs → keycache-CaOR6NYg.mjs} +0 -0
- /package/dist/{keys-DGu1NFwu.mjs → keys-CSYsOMFG.mjs} +0 -0
- /package/dist/{kv-rV3vodCc.mjs → kv-CIvR3Non.mjs} +0 -0
- /package/dist/{negotiation-DDstyBvc.mjs → negotiation-m_L2nyS3.mjs} +0 -0
- /package/dist/{public-audience-Cvbr2Gzt.mjs → public-audience-c9zmYKgA.mjs} +0 -0
- /package/dist/{retry-CXg_MBI-.mjs → retry-DI4O-zYl.mjs} +0 -0
- /package/dist/{types-J53Kw7so.mjs → types-BFowWFTT.mjs} +0 -0
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as version, t as name } from "./deno-
|
|
5
|
-
import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-
|
|
6
|
-
import { t as formatAcceptSignature } from "./accept-
|
|
7
|
-
import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-
|
|
8
|
-
import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-
|
|
9
|
-
import { t as getAuthenticatedDocumentLoader } from "./docloader-
|
|
10
|
-
import { n as kvCache } from "./kv-cache-
|
|
11
|
-
import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-
|
|
12
|
-
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-
|
|
13
|
-
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-
|
|
14
|
-
import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-
|
|
15
|
-
import { t as getNodeInfo } from "./client-
|
|
16
|
-
import { t as nodeInfoToJson } from "./types-
|
|
17
|
-
import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-
|
|
18
|
-
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-
|
|
19
|
-
import { t as CircuitBreaker } from "./circuit-breaker-
|
|
20
|
-
import { t as buildCollectionSynchronizationHeader } from "./collection-
|
|
21
|
-
import { t as KvKeyCache } from "./keycache-
|
|
22
|
-
import { t as acceptsJsonLd } from "./negotiation-
|
|
23
|
-
import { t as hasMalformedKnownTemporalLiteral } from "./temporal-
|
|
24
|
-
import { t as createExponentialBackoffPolicy } from "./retry-
|
|
4
|
+
import { n as version, t as name } from "./deno-klNxGW7W.mjs";
|
|
5
|
+
import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-CLSPm4dZ.mjs";
|
|
6
|
+
import { t as formatAcceptSignature } from "./accept-CgDcxvjV.mjs";
|
|
7
|
+
import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-o0_ipvUR.mjs";
|
|
8
|
+
import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-JdRPUYku.mjs";
|
|
9
|
+
import { t as getAuthenticatedDocumentLoader } from "./docloader-CuFH35j1.mjs";
|
|
10
|
+
import { n as kvCache } from "./kv-cache-CM7w20U5.mjs";
|
|
11
|
+
import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-C__CEgQH.mjs";
|
|
12
|
+
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-gz4TUfKG.mjs";
|
|
13
|
+
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-BNL8AC14.mjs";
|
|
14
|
+
import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-CuQpZ_AT.mjs";
|
|
15
|
+
import { t as getNodeInfo } from "./client-B_A6mfn3.mjs";
|
|
16
|
+
import { t as nodeInfoToJson } from "./types-BFowWFTT.mjs";
|
|
17
|
+
import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-B60teucv.mjs";
|
|
18
|
+
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-CJnWbgbj.mjs";
|
|
19
|
+
import { t as CircuitBreaker } from "./circuit-breaker-hJBB6jwA.mjs";
|
|
20
|
+
import { t as buildCollectionSynchronizationHeader } from "./collection-Dp5ky45w.mjs";
|
|
21
|
+
import { t as KvKeyCache } from "./keycache-CaOR6NYg.mjs";
|
|
22
|
+
import { t as acceptsJsonLd } from "./negotiation-m_L2nyS3.mjs";
|
|
23
|
+
import { t as hasMalformedKnownTemporalLiteral } from "./temporal-CvVMrvJJ.mjs";
|
|
24
|
+
import { t as createExponentialBackoffPolicy } from "./retry-DI4O-zYl.mjs";
|
|
25
|
+
import { t as TaskCodec } from "./codec-nfBU4Up4.mjs";
|
|
26
|
+
import { n as ParallelMessageQueue } from "./mq-03vS-C9P.mjs";
|
|
25
27
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
26
28
|
import { RouterError } from "@fedify/uri-template";
|
|
27
29
|
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
28
30
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
29
31
|
import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
32
|
+
import { uniq } from "es-toolkit";
|
|
30
33
|
import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
31
34
|
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
32
|
-
import { uniq } from "es-toolkit";
|
|
33
35
|
import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
34
36
|
import { domainToASCII } from "node:url";
|
|
35
37
|
//#region src/compat/transformers.ts
|
|
36
|
-
const logger$
|
|
38
|
+
const logger$2 = getLogger([
|
|
37
39
|
"fedify",
|
|
38
40
|
"compat",
|
|
39
41
|
"transformers"
|
|
@@ -59,7 +61,7 @@ const logger$1 = getLogger([
|
|
|
59
61
|
function autoIdAssigner(activity, context) {
|
|
60
62
|
if (activity.id != null) return activity;
|
|
61
63
|
const id = new URL(`/#${activity.constructor.name}/${crypto.randomUUID()}`, context.origin);
|
|
62
|
-
logger$
|
|
64
|
+
logger$2.warn("As the activity to send does not have an id, a new id {id} has been generated for it. However, it is recommended to explicitly set the id for the activity.", { id: id.href });
|
|
63
65
|
return activity.clone({ id });
|
|
64
66
|
}
|
|
65
67
|
/**
|
|
@@ -2093,6 +2095,199 @@ const MIN_COMPONENTS = [
|
|
|
2093
2095
|
"@authority"
|
|
2094
2096
|
];
|
|
2095
2097
|
//#endregion
|
|
2098
|
+
//#region src/federation/tasks/enqueue.ts
|
|
2099
|
+
/**
|
|
2100
|
+
* The enqueue pipeline for custom background tasks. `ContextImpl.enqueueTask`
|
|
2101
|
+
* and `ContextImpl.enqueueTaskMany` delegate to {@link enqueueTasks} so the
|
|
2102
|
+
* handle validation, deduplication planning, payload encoding, and queue
|
|
2103
|
+
* dispatch live in one cohesive place instead of one oversized method.
|
|
2104
|
+
*
|
|
2105
|
+
* @module
|
|
2106
|
+
*/
|
|
2107
|
+
/**
|
|
2108
|
+
* Validates the task handle, plans deduplication, encodes every payload, then
|
|
2109
|
+
* dispatches the resulting messages to the queue. A single item flows through
|
|
2110
|
+
* the same pipeline as a batch, so {@link Context.enqueueTask} and
|
|
2111
|
+
* {@link Context.enqueueTaskMany} share one implementation.
|
|
2112
|
+
* @template TContextData The context data to pass to the {@link Context}.
|
|
2113
|
+
* @template TData The type of the task payload, inferred from the task's schema.
|
|
2114
|
+
* @param ctx The enqueueing dependencies assembled by `ContextImpl`.
|
|
2115
|
+
* @param task The handle returned by `defineTask()`.
|
|
2116
|
+
* @param items The payloads to enqueue, in order.
|
|
2117
|
+
* @param options The enqueue options governing delay, ordering, and dedup.
|
|
2118
|
+
* @internal
|
|
2119
|
+
*/
|
|
2120
|
+
const enqueueTasks = (ctx) => async function(task, items, options) {
|
|
2121
|
+
const def = ctx.federation.taskDefinitions.get(task.name);
|
|
2122
|
+
if (def == null || def.handle !== task) throw new TypeError(`Task ${JSON.stringify(task.name)} is not defined on this federation; pass a handle returned by its defineTask().`);
|
|
2123
|
+
const queue = ctx.federation.resolveTaskQueue(task.name);
|
|
2124
|
+
if (queue == null) throw new TypeError("No message queue is configured for tasks; pass `queue` to createFederation() or to defineTask().");
|
|
2125
|
+
if (items.length < 1) return;
|
|
2126
|
+
const plan = planDeduplication(ctx, queue, task.name, options, items.length);
|
|
2127
|
+
const messages = await Promise.all(items.map(encodeTaskMessage(ctx.codec, ctx.origin, task, options)));
|
|
2128
|
+
const claim = await claimDeduplication(ctx, plan, task.name);
|
|
2129
|
+
if (!claim.proceed) return;
|
|
2130
|
+
if (!ctx.federation.manuallyStartQueue) ctx.federation._startQueueInternal(ctx.data);
|
|
2131
|
+
try {
|
|
2132
|
+
await dispatch(queue, messages, {
|
|
2133
|
+
delay: getDurationIfDefined(options.delay),
|
|
2134
|
+
orderingKey: options.orderingKey,
|
|
2135
|
+
deduplicationKey: claim.forwardedDeduplicationKey
|
|
2136
|
+
}, {
|
|
2137
|
+
meterProvider: ctx.federation.meterProvider,
|
|
2138
|
+
taskName: task.name
|
|
2139
|
+
});
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
if (claim.rollback != null) try {
|
|
2142
|
+
await claim.rollback();
|
|
2143
|
+
} catch (rollbackError) {
|
|
2144
|
+
logger$1.warn("Failed to roll back the deduplication marker for task {taskName} after a failed enqueue; it will expire by TTL. {rollbackError}", {
|
|
2145
|
+
taskName: task.name,
|
|
2146
|
+
rollbackError
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
throw error;
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
const getDurationIfDefined = (item) => item == null ? void 0 : Temporal.Duration.from(item);
|
|
2153
|
+
/**
|
|
2154
|
+
* Decides how a `deduplicationKey` (if any) is honored: forwarded to a native
|
|
2155
|
+
* queue, claimed via `cas`, or—when neither is available—dropped or rejected
|
|
2156
|
+
* per the federation's `taskDeduplicationFallback`. Throws the fail-fast
|
|
2157
|
+
* `TypeError`s so they precede the encode.
|
|
2158
|
+
*/
|
|
2159
|
+
function planDeduplication(ctx, queue, taskName, options, itemCount) {
|
|
2160
|
+
if (options.deduplicationKey == null) return { kind: "none" };
|
|
2161
|
+
const key = options.deduplicationKey;
|
|
2162
|
+
const native = queue.nativeDeduplication === true;
|
|
2163
|
+
const canCas = ctx.federation.kv.cas != null;
|
|
2164
|
+
const canBatchAtomically = queue.enqueueMany != null && !(queue instanceof ParallelMessageQueue && queue.queue.enqueueMany == null);
|
|
2165
|
+
if (itemCount > 1 && !canBatchAtomically && (native || canCas)) throw new TypeError(`Task ${JSON.stringify(taskName)} was enqueued as a batch with a deduplicationKey, but its message queue does not implement enqueueMany; a multi-item batch cannot be deduplicated atomically without it. Implement enqueueMany on the queue, or enqueue the tasks individually with enqueueTask().`);
|
|
2166
|
+
if (native) return {
|
|
2167
|
+
kind: "native",
|
|
2168
|
+
key
|
|
2169
|
+
};
|
|
2170
|
+
if (canCas) return {
|
|
2171
|
+
kind: "cas",
|
|
2172
|
+
key
|
|
2173
|
+
};
|
|
2174
|
+
if (ctx.federation.taskDeduplicationFallback === "closed") throw new TypeError("deduplicationKey was set but the message queue does not declare nativeDeduplication and the key-value store exposes no conditional write (cas); set taskDeduplicationFallback to \"open\" to proceed without deduplication, or use a backend that supports it.");
|
|
2175
|
+
return {
|
|
2176
|
+
kind: "open",
|
|
2177
|
+
key
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Executes the planned deduplication once the payloads are encoded. A native
|
|
2182
|
+
* plan forwards its key to the queue; a `cas` plan claims the marker and stops
|
|
2183
|
+
* the enqueue when it loses the race; an `open` plan logs and proceeds.
|
|
2184
|
+
* @returns Whether to proceed, and the key (if any) to forward to the queue.
|
|
2185
|
+
*/
|
|
2186
|
+
async function claimDeduplication(ctx, plan, taskName) {
|
|
2187
|
+
switch (plan.kind) {
|
|
2188
|
+
case "native": return {
|
|
2189
|
+
proceed: true,
|
|
2190
|
+
forwardedDeduplicationKey: plan.key
|
|
2191
|
+
};
|
|
2192
|
+
case "cas": {
|
|
2193
|
+
const cacheKey = [...ctx.federation.kvPrefixes.taskDeduplication, plan.key];
|
|
2194
|
+
const token = crypto.randomUUID();
|
|
2195
|
+
if (!await ctx.federation.kv.cas(cacheKey, void 0, token, { ttl: ctx.federation.taskDeduplicationTtl })) return { proceed: false };
|
|
2196
|
+
return {
|
|
2197
|
+
proceed: true,
|
|
2198
|
+
rollback: async () => {
|
|
2199
|
+
await ctx.federation.kv.cas(cacheKey, token, void 0);
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
case "open": logger$1.debug("deduplicationKey {deduplicationKey} for task {taskName} ignored: the message queue declares no nativeDeduplication and the key-value store has no cas; proceeding (taskDeduplicationFallback is \"open\").", {
|
|
2204
|
+
deduplicationKey: plan.key,
|
|
2205
|
+
taskName
|
|
2206
|
+
});
|
|
2207
|
+
case "none": return { proceed: true };
|
|
2208
|
+
default: throw new TypeError(`Unknown deduplication plan: ${JSON.stringify(plan)}`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Sends the encoded messages to the queue, picking the bulk path when the
|
|
2213
|
+
* queue implements `enqueueMany` and otherwise fanning out parallel single
|
|
2214
|
+
* enqueues. The fan-out drops `deduplicationKey`, which is only ever set for a
|
|
2215
|
+
* native plan that the bulk paths already cover.
|
|
2216
|
+
*/
|
|
2217
|
+
async function dispatch(queue, messages, options, { meterProvider, taskName }) {
|
|
2218
|
+
const metrics = getFederationMetrics(meterProvider);
|
|
2219
|
+
if (messages.length === 1) await queue.enqueue(messages[0], options);
|
|
2220
|
+
else if (queue.enqueueMany != null) await queue.enqueueMany(messages, options);
|
|
2221
|
+
else {
|
|
2222
|
+
const fanoutOptions = {
|
|
2223
|
+
delay: options.delay,
|
|
2224
|
+
orderingKey: options.orderingKey
|
|
2225
|
+
};
|
|
2226
|
+
const rejected = (await Promise.allSettled(messages.map(async (message) => {
|
|
2227
|
+
await queue.enqueue(message, fanoutOptions);
|
|
2228
|
+
metrics.recordQueueTaskEnqueued({
|
|
2229
|
+
role: "task",
|
|
2230
|
+
queue,
|
|
2231
|
+
taskName
|
|
2232
|
+
}, message.attempt);
|
|
2233
|
+
}))).find((result) => result.status === "rejected");
|
|
2234
|
+
if (rejected != null) throw rejected.reason;
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
metrics.recordQueueTaskEnqueued({
|
|
2238
|
+
role: "task",
|
|
2239
|
+
queue,
|
|
2240
|
+
taskName
|
|
2241
|
+
}, messages[0].attempt, messages.length);
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Builds the per-payload encoder: validates and serializes the payload, then
|
|
2245
|
+
* stamps the message envelope with a fresh id, the context's origin, and the
|
|
2246
|
+
* active trace context. Curried so the batch encode reuses one bound encoder.
|
|
2247
|
+
*/
|
|
2248
|
+
const encodeTaskMessage = (codec, origin, task, options) => async (data) => {
|
|
2249
|
+
const encoded = await codec.encode(task.schema, data);
|
|
2250
|
+
const carrier = {};
|
|
2251
|
+
propagation.inject(context.active(), carrier);
|
|
2252
|
+
return {
|
|
2253
|
+
type: "task",
|
|
2254
|
+
id: crypto.randomUUID(),
|
|
2255
|
+
baseUrl: origin,
|
|
2256
|
+
taskName: task.name,
|
|
2257
|
+
data: encoded,
|
|
2258
|
+
started: Temporal.Now.instant().toString(),
|
|
2259
|
+
attempt: 0,
|
|
2260
|
+
orderingKey: options.orderingKey,
|
|
2261
|
+
traceContext: carrier
|
|
2262
|
+
};
|
|
2263
|
+
};
|
|
2264
|
+
const logger$1 = getLogger([
|
|
2265
|
+
"fedify",
|
|
2266
|
+
"federation",
|
|
2267
|
+
"task"
|
|
2268
|
+
]);
|
|
2269
|
+
//#endregion
|
|
2270
|
+
//#region src/federation/tasks/error.ts
|
|
2271
|
+
var TaskRetryEnqueueError = class extends Error {
|
|
2272
|
+
constructor(cause) {
|
|
2273
|
+
super("Failed to re-enqueue a custom task for a retry.", { cause });
|
|
2274
|
+
this.name = "TaskRetryEnqueueError";
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
const classifyAbortableError = (error) => isAbortError(error) ? { outcome: "aborted" } : {
|
|
2278
|
+
outcome: "failed",
|
|
2279
|
+
error
|
|
2280
|
+
};
|
|
2281
|
+
const classifyTaskError = (error) => isAbortError(error) ? { outcome: "aborted" } : error instanceof TaskRetryEnqueueError ? {
|
|
2282
|
+
outcome: "failed",
|
|
2283
|
+
failureReason: "retry_enqueue",
|
|
2284
|
+
error: error.cause ?? error
|
|
2285
|
+
} : {
|
|
2286
|
+
outcome: "failed",
|
|
2287
|
+
failureReason: "handler",
|
|
2288
|
+
error
|
|
2289
|
+
};
|
|
2290
|
+
//#endregion
|
|
2096
2291
|
//#region src/federation/webfinger.ts
|
|
2097
2292
|
const logger = getLogger([
|
|
2098
2293
|
"fedify",
|
|
@@ -2418,9 +2613,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2418
2613
|
inboxQueue;
|
|
2419
2614
|
outboxQueue;
|
|
2420
2615
|
fanoutQueue;
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
fanoutQueueStarted;
|
|
2616
|
+
taskQueue;
|
|
2617
|
+
startedQueues;
|
|
2424
2618
|
manuallyStartQueue;
|
|
2425
2619
|
origin;
|
|
2426
2620
|
documentLoaderFactory;
|
|
@@ -2434,6 +2628,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2434
2628
|
skipSignatureVerification;
|
|
2435
2629
|
outboxRetryPolicy;
|
|
2436
2630
|
inboxRetryPolicy;
|
|
2631
|
+
taskRetryPolicy;
|
|
2632
|
+
taskQueueResolution;
|
|
2633
|
+
taskDeduplicationTtl;
|
|
2634
|
+
taskDeduplicationFallback;
|
|
2437
2635
|
circuitBreaker;
|
|
2438
2636
|
activityTransformers;
|
|
2439
2637
|
_tracerProvider;
|
|
@@ -2473,20 +2671,24 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2473
2671
|
httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
|
|
2474
2672
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
2475
2673
|
circuitBreaker: ["_fedify", "circuit"],
|
|
2674
|
+
taskDeduplication: ["_fedify", "taskDeduplication"],
|
|
2476
2675
|
...options.kvPrefixes ?? {}
|
|
2477
2676
|
};
|
|
2478
2677
|
if (options.queue == null) {
|
|
2479
2678
|
this.inboxQueue = void 0;
|
|
2480
2679
|
this.outboxQueue = void 0;
|
|
2481
2680
|
this.fanoutQueue = void 0;
|
|
2681
|
+
this.taskQueue = void 0;
|
|
2482
2682
|
} else if ("enqueue" in options.queue && "listen" in options.queue) {
|
|
2483
2683
|
this.inboxQueue = options.queue;
|
|
2484
2684
|
this.outboxQueue = options.queue;
|
|
2485
2685
|
this.fanoutQueue = options.queue;
|
|
2686
|
+
this.taskQueue = void 0;
|
|
2486
2687
|
} else {
|
|
2487
2688
|
this.inboxQueue = options.queue.inbox;
|
|
2488
2689
|
this.outboxQueue = options.queue.outbox;
|
|
2489
2690
|
this.fanoutQueue = options.queue.fanout;
|
|
2691
|
+
this.taskQueue = options.queue.task;
|
|
2490
2692
|
}
|
|
2491
2693
|
if (options.circuitBreaker !== false && this.outboxQueue != null) {
|
|
2492
2694
|
this.circuitBreaker = new CircuitBreaker({
|
|
@@ -2507,9 +2709,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2507
2709
|
]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
|
|
2508
2710
|
}
|
|
2509
2711
|
}
|
|
2510
|
-
this.
|
|
2511
|
-
this.outboxQueueStarted = false;
|
|
2512
|
-
this.fanoutQueueStarted = false;
|
|
2712
|
+
this.startedQueues = /* @__PURE__ */ new Set();
|
|
2513
2713
|
this.manuallyStartQueue = options.manuallyStartQueue ?? false;
|
|
2514
2714
|
if (options.origin != null) if (typeof options.origin === "string") {
|
|
2515
2715
|
if (!URL.canParse(options.origin) || !options.origin.match(/^https?:\/\//)) throw new TypeError(`Invalid origin: ${JSON.stringify(options.origin)}`);
|
|
@@ -2591,6 +2791,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2591
2791
|
this.inboxChallengePolicy = options.inboxChallengePolicy;
|
|
2592
2792
|
this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
2593
2793
|
this.inboxRetryPolicy = options.inboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
2794
|
+
this.taskRetryPolicy = options.taskRetryPolicy ?? createExponentialBackoffPolicy();
|
|
2795
|
+
this.taskQueueResolution = options.taskQueueResolution ?? "fallback";
|
|
2796
|
+
this.taskDeduplicationTtl = Temporal.Duration.from(options.taskDeduplicationTtl ?? { hours: 1 });
|
|
2797
|
+
this.taskDeduplicationFallback = options.taskDeduplicationFallback ?? "open";
|
|
2594
2798
|
this.activityTransformers = options.activityTransformers ?? getDefaultActivityTransformers();
|
|
2595
2799
|
this._tracerProvider = options.tracerProvider;
|
|
2596
2800
|
if (benchmarkMode) {
|
|
@@ -2623,6 +2827,9 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2623
2827
|
this.#registerQueueDepthGauge(meterProvider);
|
|
2624
2828
|
return meterProvider;
|
|
2625
2829
|
}
|
|
2830
|
+
get metrics() {
|
|
2831
|
+
return getFederationMetrics(this.meterProvider);
|
|
2832
|
+
}
|
|
2626
2833
|
#registerQueueDepthGauge(meterProvider) {
|
|
2627
2834
|
if (meterProvider === this.#queueDepthGaugeMeterProvider) return;
|
|
2628
2835
|
registerQueueDepthGauge(meterProvider, this.#queueDepthGaugeEntries, { sourceId: this.#queueDepthGaugeSourceId });
|
|
@@ -2639,35 +2846,38 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2639
2846
|
_getTracer() {
|
|
2640
2847
|
return this.tracerProvider.getTracer(name, version);
|
|
2641
2848
|
}
|
|
2849
|
+
resolveTaskQueue(taskName) {
|
|
2850
|
+
const resolved = this.taskDefinitions.get(taskName)?.queue ?? this.taskQueue;
|
|
2851
|
+
if (resolved != null) return resolved;
|
|
2852
|
+
return this.taskQueueResolution === "strict" ? void 0 : this.outboxQueue;
|
|
2853
|
+
}
|
|
2642
2854
|
async _startQueueInternal(ctxData, signal, queue) {
|
|
2643
|
-
|
|
2855
|
+
const taskQueue = this.taskQueue ?? (this.taskQueueResolution === "fallback" ? this.outboxQueue : void 0);
|
|
2856
|
+
const customQueues = this.taskDefinitions.values().map((def) => ["task", def.queue]);
|
|
2857
|
+
const targets = [
|
|
2858
|
+
["inbox", this.inboxQueue],
|
|
2859
|
+
["outbox", this.outboxQueue],
|
|
2860
|
+
["fanout", this.fanoutQueue],
|
|
2861
|
+
["task", taskQueue],
|
|
2862
|
+
...customQueues
|
|
2863
|
+
];
|
|
2644
2864
|
const logger = getLogger([
|
|
2645
2865
|
"fedify",
|
|
2646
2866
|
"federation",
|
|
2647
2867
|
"queue"
|
|
2648
2868
|
]);
|
|
2649
2869
|
const promises = [];
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
this.
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
logger.debug("Starting an outbox task worker.");
|
|
2657
|
-
this.outboxQueueStarted = true;
|
|
2658
|
-
promises.push(this.outboxQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
|
|
2659
|
-
}
|
|
2660
|
-
if (this.fanoutQueue != null && this.fanoutQueue !== this.inboxQueue && this.fanoutQueue !== this.outboxQueue && (queue == null || queue === "fanout") && !this.fanoutQueueStarted) {
|
|
2661
|
-
logger.debug("Starting a fanout task worker.");
|
|
2662
|
-
this.fanoutQueueStarted = true;
|
|
2663
|
-
promises.push(this.fanoutQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
|
|
2870
|
+
for (const [role, target] of targets) {
|
|
2871
|
+
if (target == null || !(queue == null || queue === role)) continue;
|
|
2872
|
+
if (this.startedQueues.has(target)) continue;
|
|
2873
|
+
this.startedQueues.add(target);
|
|
2874
|
+
logger.debug("Starting a {role} queue worker.", { role });
|
|
2875
|
+
promises.push(target.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
|
|
2664
2876
|
}
|
|
2665
2877
|
await Promise.all(promises);
|
|
2666
2878
|
}
|
|
2667
2879
|
processQueuedTask(contextData, message) {
|
|
2668
|
-
const tracer = this._getTracer();
|
|
2669
2880
|
const extractedContext = propagation.extract(context.active(), message.traceContext);
|
|
2670
|
-
const meter = getFederationMetrics(this.meterProvider);
|
|
2671
2881
|
return withContext({ messageId: message.id }, async () => {
|
|
2672
2882
|
if (message.type === "fanout") {
|
|
2673
2883
|
const common = {
|
|
@@ -2675,117 +2885,108 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2675
2885
|
queue: this.fanoutQueue,
|
|
2676
2886
|
activityType: message.activityType
|
|
2677
2887
|
};
|
|
2678
|
-
await
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
return await withContext({
|
|
2684
|
-
traceId: spanCtx.traceId,
|
|
2685
|
-
spanId: spanCtx.spanId
|
|
2686
|
-
}, async () => {
|
|
2687
|
-
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
2688
|
-
meter.recordQueueTaskStarted(common);
|
|
2689
|
-
meter.incrementQueueTaskInFlight(common);
|
|
2690
|
-
const startedAt = performance.now();
|
|
2691
|
-
let outcome = "completed";
|
|
2692
|
-
try {
|
|
2693
|
-
await this.#listenFanoutMessage(contextData, message);
|
|
2694
|
-
} catch (e) {
|
|
2695
|
-
const aborted = isAbortError(e);
|
|
2696
|
-
outcome = aborted ? "aborted" : "failed";
|
|
2697
|
-
if (!aborted) span.setStatus({
|
|
2698
|
-
code: SpanStatusCode.ERROR,
|
|
2699
|
-
message: String(e)
|
|
2700
|
-
});
|
|
2701
|
-
throw e;
|
|
2702
|
-
} finally {
|
|
2703
|
-
meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
|
|
2704
|
-
meter.decrementQueueTaskInFlight(common);
|
|
2705
|
-
span.end();
|
|
2706
|
-
}
|
|
2707
|
-
});
|
|
2708
|
-
});
|
|
2888
|
+
await this.#runWorkerSpan("activitypub.fanout", { "activitypub.activity.type": message.activityType }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
|
|
2889
|
+
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
2890
|
+
await this.#listenFanoutMessage(contextData, message);
|
|
2891
|
+
return { outcome: "completed" };
|
|
2892
|
+
}, classifyAbortableError));
|
|
2709
2893
|
} else if (message.type === "outbox") {
|
|
2710
2894
|
const common = {
|
|
2711
2895
|
role: "outbox",
|
|
2712
2896
|
queue: this.outboxQueue,
|
|
2713
2897
|
activityType: message.activityType
|
|
2714
2898
|
};
|
|
2715
|
-
await
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
return await withContext({
|
|
2724
|
-
traceId: spanCtx.traceId,
|
|
2725
|
-
spanId: spanCtx.spanId
|
|
2726
|
-
}, async () => {
|
|
2727
|
-
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
2728
|
-
meter.recordQueueTaskStarted(common);
|
|
2729
|
-
meter.incrementQueueTaskInFlight(common);
|
|
2730
|
-
const startedAt = performance.now();
|
|
2731
|
-
let outcome = "completed";
|
|
2732
|
-
try {
|
|
2733
|
-
await this.#listenOutboxMessage(contextData, message, span);
|
|
2734
|
-
} catch (e) {
|
|
2735
|
-
const aborted = isAbortError(e);
|
|
2736
|
-
outcome = aborted ? "aborted" : "failed";
|
|
2737
|
-
if (!aborted) span.setStatus({
|
|
2738
|
-
code: SpanStatusCode.ERROR,
|
|
2739
|
-
message: String(e)
|
|
2740
|
-
});
|
|
2741
|
-
throw e;
|
|
2742
|
-
} finally {
|
|
2743
|
-
meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
|
|
2744
|
-
meter.decrementQueueTaskInFlight(common);
|
|
2745
|
-
span.end();
|
|
2746
|
-
}
|
|
2747
|
-
});
|
|
2748
|
-
});
|
|
2899
|
+
await this.#runWorkerSpan("activitypub.outbox", {
|
|
2900
|
+
"activitypub.activity.type": message.activityType,
|
|
2901
|
+
"activitypub.activity.retries": message.attempt
|
|
2902
|
+
}, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
|
|
2903
|
+
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
2904
|
+
await this.#listenOutboxMessage(contextData, message, span);
|
|
2905
|
+
return { outcome: "completed" };
|
|
2906
|
+
}, classifyAbortableError));
|
|
2749
2907
|
} else if (message.type === "inbox") {
|
|
2750
2908
|
const common = {
|
|
2751
2909
|
role: "inbox",
|
|
2752
2910
|
queue: this.inboxQueue
|
|
2753
2911
|
};
|
|
2754
|
-
await
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
}, extractedContext, async (span) => {
|
|
2758
|
-
const spanCtx = span.spanContext();
|
|
2759
|
-
return await withContext({
|
|
2760
|
-
traceId: spanCtx.traceId,
|
|
2761
|
-
spanId: spanCtx.spanId
|
|
2762
|
-
}, async () => {
|
|
2763
|
-
meter.recordQueueTaskStarted(common);
|
|
2764
|
-
meter.incrementQueueTaskInFlight(common);
|
|
2765
|
-
const startedAt = performance.now();
|
|
2766
|
-
let outcome = "completed";
|
|
2767
|
-
try {
|
|
2768
|
-
await this.#listenInboxMessage(contextData, message, span, (activityType) => {
|
|
2769
|
-
common.activityType = activityType;
|
|
2770
|
-
});
|
|
2771
|
-
} catch (e) {
|
|
2772
|
-
const aborted = isAbortError(e);
|
|
2773
|
-
outcome = aborted ? "aborted" : "failed";
|
|
2774
|
-
if (!aborted) span.setStatus({
|
|
2775
|
-
code: SpanStatusCode.ERROR,
|
|
2776
|
-
message: String(e)
|
|
2777
|
-
});
|
|
2778
|
-
throw e;
|
|
2779
|
-
} finally {
|
|
2780
|
-
meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
|
|
2781
|
-
meter.decrementQueueTaskInFlight(common);
|
|
2782
|
-
span.end();
|
|
2783
|
-
}
|
|
2912
|
+
await this.#runWorkerSpan("activitypub.inbox", { "activitypub.shared_inbox": message.identifier == null }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
|
|
2913
|
+
await this.#listenInboxMessage(contextData, message, span, (activityType) => {
|
|
2914
|
+
common.activityType = activityType;
|
|
2784
2915
|
});
|
|
2785
|
-
|
|
2916
|
+
return { outcome: "completed" };
|
|
2917
|
+
}, classifyAbortableError));
|
|
2918
|
+
} else if (message.type === "task") {
|
|
2919
|
+
const registered = this.taskDefinitions.get(message.taskName) != null;
|
|
2920
|
+
const common = {
|
|
2921
|
+
role: "task",
|
|
2922
|
+
queue: this.resolveTaskQueue(message.taskName),
|
|
2923
|
+
taskName: registered ? message.taskName : void 0
|
|
2924
|
+
};
|
|
2925
|
+
await this.#runWorkerSpan("fedify.task", {
|
|
2926
|
+
"fedify.task.name": message.taskName,
|
|
2927
|
+
"fedify.task.attempt": message.attempt
|
|
2928
|
+
}, extractedContext, this.#instrumentWorkerBody(common, () => this.#listenTaskMessage(contextData, message), classifyTaskError));
|
|
2786
2929
|
}
|
|
2787
2930
|
});
|
|
2788
2931
|
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Opens a CONSUMER span for a queue worker and scopes the extracted trace
|
|
2934
|
+
* context around its body. The body—typically built by
|
|
2935
|
+
* {@link FederationImpl.instrumentWorkerBody}—receives the span and returns
|
|
2936
|
+
* the context-scoped worker callback.
|
|
2937
|
+
* @param spanName The consumer span name (for example, `"fedify.task"`).
|
|
2938
|
+
* @param spanAttributes The initial span attributes.
|
|
2939
|
+
* @param extractedContext The trace context extracted from the message.
|
|
2940
|
+
* @param instrumentedBody Builds the context-scoped worker callback for the
|
|
2941
|
+
* opened span.
|
|
2942
|
+
*/
|
|
2943
|
+
async #runWorkerSpan(spanName, spanAttributes, extractedContext, instrumentedBody) {
|
|
2944
|
+
await this._getTracer().startActiveSpan(spanName, {
|
|
2945
|
+
kind: SpanKind.CONSUMER,
|
|
2946
|
+
attributes: spanAttributes
|
|
2947
|
+
}, extractedContext, async (span) => {
|
|
2948
|
+
const spanCtx = span.spanContext();
|
|
2949
|
+
return await withContext({
|
|
2950
|
+
traceId: spanCtx.traceId,
|
|
2951
|
+
spanId: spanCtx.spanId
|
|
2952
|
+
}, instrumentedBody(span));
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Wraps a worker body with the shared `fedify.queue.task.*` boundary
|
|
2957
|
+
* telemetry, returning the instrumented body keyed by its span. The
|
|
2958
|
+
* instrumentation records the started, outcome, and duration metrics, pairs
|
|
2959
|
+
* the in-flight increment with its decrement, sets the span's error status
|
|
2960
|
+
* from the terminal outcome, and ends the span. The fanout, outbox, inbox,
|
|
2961
|
+
* and task workers differ only in `common`, the body, and how a thrown error
|
|
2962
|
+
* maps to an outcome.
|
|
2963
|
+
* @param common The shared queue-task metric attributes for this worker.
|
|
2964
|
+
* @param run Runs the worker body and resolves to its terminal outcome.
|
|
2965
|
+
* @param classifyError Maps a thrown error to a terminal outcome.
|
|
2966
|
+
*/
|
|
2967
|
+
#instrumentWorkerBody = (common, run, classifyError) => (span) => async () => {
|
|
2968
|
+
this.metrics.recordQueueTaskStarted(common);
|
|
2969
|
+
this.metrics.incrementQueueTaskInFlight(common);
|
|
2970
|
+
const startedAt = performance.now();
|
|
2971
|
+
let result = { outcome: "completed" };
|
|
2972
|
+
try {
|
|
2973
|
+
result = await run(span);
|
|
2974
|
+
} catch (e) {
|
|
2975
|
+
result = classifyError(e);
|
|
2976
|
+
throw e;
|
|
2977
|
+
} finally {
|
|
2978
|
+
if (result.outcome === "failed") {
|
|
2979
|
+
if (result.failureReason != null) span.setAttribute("fedify.task.failure_reason", result.failureReason);
|
|
2980
|
+
span.setStatus({
|
|
2981
|
+
code: SpanStatusCode.ERROR,
|
|
2982
|
+
...result.error == null ? {} : { message: String(result.error) }
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
this.metrics.recordQueueTaskOutcome(common, result.outcome, getDurationMs(startedAt), result.outcome === "failed" ? result.failureReason : void 0);
|
|
2986
|
+
this.metrics.decrementQueueTaskInFlight(common);
|
|
2987
|
+
span.end();
|
|
2988
|
+
}
|
|
2989
|
+
};
|
|
2789
2990
|
async #listenFanoutMessage(data, message) {
|
|
2790
2991
|
getLogger([
|
|
2791
2992
|
"fedify",
|
|
@@ -2875,7 +3076,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2875
3076
|
delay: clampNegativeDelay(delay),
|
|
2876
3077
|
orderingKey: message.orderingKey
|
|
2877
3078
|
});
|
|
2878
|
-
|
|
3079
|
+
this.metrics.recordQueueTaskEnqueued({
|
|
2879
3080
|
role: "outbox",
|
|
2880
3081
|
queue: outboxQueue,
|
|
2881
3082
|
activityType: heldMessage.activityType
|
|
@@ -3054,7 +3255,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3054
3255
|
return;
|
|
3055
3256
|
}
|
|
3056
3257
|
if (isPermanentFailure) {
|
|
3057
|
-
|
|
3258
|
+
this.metrics.recordPermanentFailure(error.inbox, error.statusCode);
|
|
3058
3259
|
logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
|
|
3059
3260
|
...logData,
|
|
3060
3261
|
status: error.statusCode
|
|
@@ -3114,7 +3315,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3114
3315
|
delay: clampNegativeDelay(delay),
|
|
3115
3316
|
orderingKey: message.orderingKey
|
|
3116
3317
|
});
|
|
3117
|
-
|
|
3318
|
+
this.metrics.recordQueueTaskEnqueued({
|
|
3118
3319
|
role: "outbox",
|
|
3119
3320
|
queue: outboxQueue,
|
|
3120
3321
|
activityType: retryMessage.activityType
|
|
@@ -3204,7 +3405,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3204
3405
|
};
|
|
3205
3406
|
await this.inboxQueue.enqueue(retryMessage, { delay: clampNegativeDelay(delay) });
|
|
3206
3407
|
if (activityType != null) {
|
|
3207
|
-
|
|
3408
|
+
this.metrics.recordQueueTaskEnqueued({
|
|
3208
3409
|
role: "inbox",
|
|
3209
3410
|
queue: this.inboxQueue,
|
|
3210
3411
|
activityType
|
|
@@ -3331,7 +3532,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3331
3532
|
try {
|
|
3332
3533
|
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
|
|
3333
3534
|
} finally {
|
|
3334
|
-
|
|
3535
|
+
this.metrics.recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
3335
3536
|
}
|
|
3336
3537
|
recordInboxActivity(this.meterProvider, "processed", activityType);
|
|
3337
3538
|
} catch (error) {
|
|
@@ -3347,6 +3548,105 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3347
3548
|
listenerSpan.end();
|
|
3348
3549
|
});
|
|
3349
3550
|
}
|
|
3551
|
+
async #listenTaskMessage(contextData, message) {
|
|
3552
|
+
const logger = getLogger([
|
|
3553
|
+
"fedify",
|
|
3554
|
+
"federation",
|
|
3555
|
+
"task"
|
|
3556
|
+
]);
|
|
3557
|
+
const def = this.taskDefinitions.get(message.taskName);
|
|
3558
|
+
if (def == null) {
|
|
3559
|
+
logger.warn("Received a custom task {taskName} with no registered handler; dropping.", { taskName: message.taskName });
|
|
3560
|
+
return {
|
|
3561
|
+
outcome: "failed",
|
|
3562
|
+
failureReason: "unknown_task"
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
const context = this.#createContext(new URL(message.baseUrl), contextData);
|
|
3566
|
+
const data = await context.codec.decode(def.schema, message.data);
|
|
3567
|
+
if (!data.ok) {
|
|
3568
|
+
if (data.phase === "deserialization") logger.error("Custom task {taskName} payload could not be deserialized; dropping:\n{error}", {
|
|
3569
|
+
taskName: message.taskName,
|
|
3570
|
+
error: data.error
|
|
3571
|
+
});
|
|
3572
|
+
else logger.error("Custom task {taskName} payload failed schema validation; dropping:\n{error}", {
|
|
3573
|
+
taskName: message.taskName,
|
|
3574
|
+
error: data.error
|
|
3575
|
+
});
|
|
3576
|
+
return {
|
|
3577
|
+
outcome: "failed",
|
|
3578
|
+
failureReason: data.phase
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
try {
|
|
3582
|
+
await def.handler(context, data.value);
|
|
3583
|
+
return { outcome: "completed" };
|
|
3584
|
+
} catch (error) {
|
|
3585
|
+
if (def.onError != null) try {
|
|
3586
|
+
await def.onError(context, error, data.value);
|
|
3587
|
+
} catch (onErrorError) {
|
|
3588
|
+
logger.error("onError for custom task {taskName} threw:\n{error}", {
|
|
3589
|
+
taskName: message.taskName,
|
|
3590
|
+
error: onErrorError
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
const queue = this.resolveTaskQueue(def.name);
|
|
3594
|
+
if (queue?.nativeRetrial) throw error;
|
|
3595
|
+
const retryPolicy = def.retryPolicy ?? this.taskRetryPolicy;
|
|
3596
|
+
let elapsedTime = Temporal.Duration.from({ seconds: 0 });
|
|
3597
|
+
try {
|
|
3598
|
+
elapsedTime = Temporal.Instant.from(message.started).until(Temporal.Now.instant());
|
|
3599
|
+
} catch (parseError) {
|
|
3600
|
+
logger.error("Custom task {taskName} has an unparsable started time {started}; treating elapsedTime as zero:\n{error}", {
|
|
3601
|
+
taskName: message.taskName,
|
|
3602
|
+
started: message.started,
|
|
3603
|
+
error: parseError
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
const delay = retryPolicy({
|
|
3607
|
+
elapsedTime,
|
|
3608
|
+
attempts: message.attempt
|
|
3609
|
+
});
|
|
3610
|
+
if (delay != null && queue != null) {
|
|
3611
|
+
logger.error("Custom task {taskName} failed (attempt #{attempt}); retry...:\n{error}", {
|
|
3612
|
+
taskName: message.taskName,
|
|
3613
|
+
attempt: message.attempt,
|
|
3614
|
+
error
|
|
3615
|
+
});
|
|
3616
|
+
const retryMessage = {
|
|
3617
|
+
...message,
|
|
3618
|
+
attempt: message.attempt + 1
|
|
3619
|
+
};
|
|
3620
|
+
try {
|
|
3621
|
+
await queue.enqueue(retryMessage, {
|
|
3622
|
+
delay: clampNegativeDelay(delay),
|
|
3623
|
+
orderingKey: message.orderingKey
|
|
3624
|
+
});
|
|
3625
|
+
} catch (enqueueError) {
|
|
3626
|
+
logger.error("Custom task {taskName} could not be re-enqueued for a retry (attempt #{attempt}):\n{error}", {
|
|
3627
|
+
taskName: message.taskName,
|
|
3628
|
+
attempt: retryMessage.attempt,
|
|
3629
|
+
error: enqueueError
|
|
3630
|
+
});
|
|
3631
|
+
throw new TaskRetryEnqueueError(error);
|
|
3632
|
+
}
|
|
3633
|
+
this.metrics.recordQueueTaskEnqueued({
|
|
3634
|
+
role: "task",
|
|
3635
|
+
queue,
|
|
3636
|
+
taskName: message.taskName
|
|
3637
|
+
}, retryMessage.attempt);
|
|
3638
|
+
return { outcome: "completed" };
|
|
3639
|
+
} else logger.error("Custom task {taskName} failed after {attempt} attempts; giving up:\n{error}", {
|
|
3640
|
+
taskName: message.taskName,
|
|
3641
|
+
attempt: message.attempt,
|
|
3642
|
+
error
|
|
3643
|
+
});
|
|
3644
|
+
return isAbortError(error) ? { outcome: "aborted" } : {
|
|
3645
|
+
outcome: "failed",
|
|
3646
|
+
failureReason: "handler"
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3350
3650
|
startQueue(contextData, options = {}) {
|
|
3351
3651
|
return this._startQueueInternal(contextData, options.signal, options.queue);
|
|
3352
3652
|
}
|
|
@@ -3580,7 +3880,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3580
3880
|
});
|
|
3581
3881
|
if (acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
|
|
3582
3882
|
} catch (error) {
|
|
3583
|
-
|
|
3883
|
+
this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
|
|
3584
3884
|
span.setStatus({
|
|
3585
3885
|
code: SpanStatusCode.ERROR,
|
|
3586
3886
|
message: `${error}`
|
|
@@ -3593,7 +3893,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3593
3893
|
});
|
|
3594
3894
|
throw error;
|
|
3595
3895
|
}
|
|
3596
|
-
|
|
3896
|
+
this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), {
|
|
3597
3897
|
statusCode: response.status,
|
|
3598
3898
|
routeTemplate: metricState.routeTemplate
|
|
3599
3899
|
});
|
|
@@ -3931,6 +4231,7 @@ var ContextImpl = class ContextImpl {
|
|
|
3931
4231
|
documentLoader;
|
|
3932
4232
|
contextLoader;
|
|
3933
4233
|
invokedFromActorKeyPairsDispatcher;
|
|
4234
|
+
#codec;
|
|
3934
4235
|
constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher }) {
|
|
3935
4236
|
this.url = url;
|
|
3936
4237
|
this.federation = federation;
|
|
@@ -3939,6 +4240,18 @@ var ContextImpl = class ContextImpl {
|
|
|
3939
4240
|
this.contextLoader = contextLoader;
|
|
3940
4241
|
this.invokedFromActorKeyPairsDispatcher = invokedFromActorKeyPairsDispatcher;
|
|
3941
4242
|
}
|
|
4243
|
+
/**
|
|
4244
|
+
* A {@link TaskCodec} bound to this context's loaders, used to encode
|
|
4245
|
+
* and decode custom task payloads. Lazily created and cached so a context
|
|
4246
|
+
* that never enqueues or dispatches a task pays nothing.
|
|
4247
|
+
* @internal
|
|
4248
|
+
*/
|
|
4249
|
+
get codec() {
|
|
4250
|
+
return this.#codec ??= new TaskCodec(this);
|
|
4251
|
+
}
|
|
4252
|
+
get #enqueueTasks() {
|
|
4253
|
+
return enqueueTasks(this);
|
|
4254
|
+
}
|
|
3942
4255
|
clone(data) {
|
|
3943
4256
|
return new ContextImpl({
|
|
3944
4257
|
url: this.url,
|
|
@@ -4257,6 +4570,12 @@ var ContextImpl = class ContextImpl {
|
|
|
4257
4570
|
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
4258
4571
|
});
|
|
4259
4572
|
}
|
|
4573
|
+
async enqueueTask(task, data, options = {}) {
|
|
4574
|
+
await this.#enqueueTasks(task, [data], options);
|
|
4575
|
+
}
|
|
4576
|
+
async enqueueTaskMany(task, payloads, options = {}) {
|
|
4577
|
+
await this.#enqueueTasks(task, payloads, options);
|
|
4578
|
+
}
|
|
4260
4579
|
sendActivity(sender, recipients, activity, options = {}) {
|
|
4261
4580
|
return this.tracerProvider.getTracer(name, version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
|
|
4262
4581
|
kind: this.federation.outboxQueue == null || options.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
|