@fedify/fedify 2.3.0-dev.1189 → 2.3.0-dev.1212
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/{builder-Dc6s3gPe.mjs → builder-DdbtvTFp.mjs} +2 -2
- package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{context-CRXCkTM6.d.cts → context-DMHK7jqX.d.cts} +224 -3
- package/dist/{context-MgCh7YGu.d.ts → context-K9cg8oGx.d.ts} +224 -3
- package/dist/{deno-BomxIkHS.mjs → deno-DTaoLXHr.mjs} +1 -1
- package/dist/{docloader-CzS6F5sZ.mjs → docloader-CdNiXmNg.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/circuit-breaker.test.d.mts +2 -0
- package/dist/federation/circuit-breaker.test.mjs +446 -0
- package/dist/federation/collection.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +3 -3
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/keycache.test.mjs +1 -1
- package/dist/federation/metrics.test.mjs +16 -1
- package/dist/federation/middleware.test.mjs +817 -6
- package/dist/federation/mod.cjs +4 -1
- package/dist/federation/mod.d.cts +3 -3
- package/dist/federation/mod.d.ts +3 -3
- package/dist/federation/mod.js +2 -2
- package/dist/federation/negotiation.test.mjs +1 -1
- package/dist/federation/retry.test.mjs +1 -1
- package/dist/federation/send.test.mjs +43 -10
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{getMachineId-bsd-BY01PL1n.mjs → getMachineId-bsd-Bn0le7-J.mjs} +1 -1
- package/dist/{getMachineId-darwin-Dr1gkBkp.mjs → getMachineId-darwin-CVjKuDgj.mjs} +1 -1
- package/dist/{getMachineId-win-QEYwcJiy.mjs → getMachineId-win-c5zxTSS1.mjs} +1 -1
- package/dist/{http-B-psRIq6.js → http-BEG9kx13.js} +25 -6
- package/dist/{http-DtWN_XvX.mjs → http-ByCfCX5K.mjs} +3 -3
- package/dist/{http-DnJyL_6c.cjs → http-Czeyq7if.cjs} +30 -5
- package/dist/{key-CT2NnJuR.mjs → key-Bhsx9PrC.mjs} +2 -2
- package/dist/{kv-cache-Bf8AoV6C.mjs → kv-cache-D4jzgeYW.mjs} +1 -1
- package/dist/{kv-cache-DKhLDCH8.js → kv-cache-D9U1AnXH.js} +1 -1
- package/dist/{kv-cache-CVre456Y.cjs → kv-cache-qRBN2G2Z.cjs} +1 -1
- package/dist/{ld-DCyQasTE.mjs → ld-CHtLb_Uh.mjs} +3 -3
- package/dist/{metrics-xgr0P4hO.mjs → metrics-uwSF8DLC.mjs} +25 -6
- package/dist/{middleware-DK0thDHX.mjs → middleware-BmSzD5U9.mjs} +279 -40
- package/dist/{middleware-DIJ_6KFI.cjs → middleware-CRORNnSU.cjs} +632 -31
- package/dist/{middleware-sgx08IEk.mjs → middleware-CyiBzIwY.mjs} +1 -1
- package/dist/{middleware-BgbdoV61.js → middleware-DrKDd2JT.js} +615 -32
- package/dist/{mod-CpQHB3Ys.d.ts → mod-CfOFqS0w.d.ts} +1 -1
- package/dist/{mod-C7HOzGqH.d.cts → mod-YLnSsEHY.d.cts} +1 -1
- package/dist/mod.cjs +7 -4
- package/dist/mod.d.cts +4 -4
- package/dist/mod.d.ts +4 -4
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-BIU_Sl7y.mjs → owner-B0Zrhs0w.mjs} +2 -2
- package/dist/{proof-B5defvTr.js → proof-CZhAX94C.js} +1 -1
- package/dist/{proof-DDs7BRl7.mjs → proof-DbJFxpzD.mjs} +3 -3
- package/dist/{proof-B9xbksrX.cjs → proof-frzCtYji.cjs} +1 -1
- package/dist/{send-BuxDCpxz.mjs → send-kst2L0Df.mjs} +21 -7
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/proof.test.mjs +1 -1
- package/dist/{temporal-DHgeMWiP.mjs → temporal-CcGypkzd.mjs} +1 -1
- package/dist/testing/mod.d.mts +36 -2
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +6 -6
- /package/dist/{collection-CA3V5zyK.mjs → collection-Cc3DVAhE.mjs} +0 -0
- /package/dist/{execAsync-Dxb7rNf3.mjs → execAsync-Dmet7-28.mjs} +0 -0
- /package/dist/{getMachineId-linux-Bbhofx-s.mjs → getMachineId-linux-DbG4BXa-.mjs} +0 -0
- /package/dist/{getMachineId-unsupported-dIOte2Ct.mjs → getMachineId-unsupported-lC8T9hPE.mjs} +0 -0
- /package/dist/{keycache-BYMd8q7F.mjs → keycache-BeU0LCII.mjs} +0 -0
- /package/dist/{negotiation-CDW-_gUU.mjs → negotiation-DDstyBvc.mjs} +0 -0
- /package/dist/{retry-_VvV0h9f.mjs → retry-CXg_MBI-.mjs} +0 -0
|
@@ -1,26 +1,27 @@
|
|
|
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 {
|
|
4
|
+
import { n as version, t as name } from "./deno-DTaoLXHr.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, y as recordOutboxEnqueue } from "./metrics-uwSF8DLC.mjs";
|
|
6
6
|
import { t as formatAcceptSignature } from "./accept-CceiKpCy.mjs";
|
|
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-
|
|
7
|
+
import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-Bhsx9PrC.mjs";
|
|
8
|
+
import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-ByCfCX5K.mjs";
|
|
9
|
+
import { t as getAuthenticatedDocumentLoader } from "./docloader-CdNiXmNg.mjs";
|
|
10
|
+
import { n as kvCache } from "./kv-cache-D4jzgeYW.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-CHtLb_Uh.mjs";
|
|
12
|
+
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-B0Zrhs0w.mjs";
|
|
13
13
|
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-BgFLCJQ_.mjs";
|
|
14
|
-
import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-
|
|
14
|
+
import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-DbJFxpzD.mjs";
|
|
15
15
|
import { t as getNodeInfo } from "./client-B_A6mfn3.mjs";
|
|
16
16
|
import { t as nodeInfoToJson } from "./types-BFowWFTT.mjs";
|
|
17
|
-
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-
|
|
18
|
-
import { t as
|
|
19
|
-
import { t as
|
|
20
|
-
import { t as
|
|
21
|
-
import { t as
|
|
22
|
-
import { t as
|
|
23
|
-
import {
|
|
17
|
+
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-DdbtvTFp.mjs";
|
|
18
|
+
import { t as CircuitBreaker } from "./circuit-breaker-CSWsyoef.mjs";
|
|
19
|
+
import { t as buildCollectionSynchronizationHeader } from "./collection-Cc3DVAhE.mjs";
|
|
20
|
+
import { t as KvKeyCache } from "./keycache-BeU0LCII.mjs";
|
|
21
|
+
import { t as acceptsJsonLd } from "./negotiation-DDstyBvc.mjs";
|
|
22
|
+
import { t as hasMalformedKnownTemporalLiteral } from "./temporal-CcGypkzd.mjs";
|
|
23
|
+
import { t as createExponentialBackoffPolicy } from "./retry-CXg_MBI-.mjs";
|
|
24
|
+
import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-kst2L0Df.mjs";
|
|
24
25
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
25
26
|
import { RouterError } from "@fedify/uri-template";
|
|
26
27
|
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
@@ -2083,6 +2084,57 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2083
2084
|
}
|
|
2084
2085
|
//#endregion
|
|
2085
2086
|
//#region src/federation/middleware.ts
|
|
2087
|
+
const circuitBreakerCasWarningKvStores = /* @__PURE__ */ new WeakSet();
|
|
2088
|
+
const retryAfterHttpDate = /* @__PURE__ */ new RegExp("^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \\d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d{4} \\d{2}:\\d{2}:\\d{2} GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \\d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\d{2} \\d{2}:\\d{2}:\\d{2} GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: \\d|\\d{2}) \\d{2}:\\d{2}:\\d{2} \\d{4})$");
|
|
2089
|
+
function parseRetryAfter(headers, now = Temporal.Now.instant()) {
|
|
2090
|
+
const value = headers.get("Retry-After");
|
|
2091
|
+
if (value == null) return void 0;
|
|
2092
|
+
const trimmed = value.trim();
|
|
2093
|
+
if (/^\d+$/.test(trimmed)) {
|
|
2094
|
+
const seconds = Number(trimmed);
|
|
2095
|
+
if (!Number.isFinite(seconds)) return void 0;
|
|
2096
|
+
return parseRetryAfterDuration({ seconds });
|
|
2097
|
+
}
|
|
2098
|
+
if (!retryAfterHttpDate.test(trimmed)) return void 0;
|
|
2099
|
+
const httpDate = trimmed.endsWith("GMT") ? trimmed : `${trimmed} GMT`;
|
|
2100
|
+
const retryAtMs = Date.parse(httpDate);
|
|
2101
|
+
if (Number.isNaN(retryAtMs)) return void 0;
|
|
2102
|
+
const nowMs = Number(now.epochMilliseconds);
|
|
2103
|
+
return parseRetryAfterDuration({ milliseconds: Math.max(0, retryAtMs - nowMs) });
|
|
2104
|
+
}
|
|
2105
|
+
function parseRetryAfterDuration(durationLike) {
|
|
2106
|
+
try {
|
|
2107
|
+
return Temporal.Duration.from(durationLike);
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
if (error instanceof RangeError) return void 0;
|
|
2110
|
+
throw error;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
function clampNegativeDelay(delay) {
|
|
2114
|
+
return delay.sign < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay;
|
|
2115
|
+
}
|
|
2116
|
+
function maxDelay(first, second) {
|
|
2117
|
+
return Temporal.Duration.compare(first, second) >= 0 ? first : second;
|
|
2118
|
+
}
|
|
2119
|
+
function isTransportDeliveryError(error) {
|
|
2120
|
+
return error instanceof FetchError || isAbortError(error);
|
|
2121
|
+
}
|
|
2122
|
+
function toCircuitBreakerMetricState(state) {
|
|
2123
|
+
return state === "half-open" ? "half_open" : state;
|
|
2124
|
+
}
|
|
2125
|
+
function recordCircuitBreakerSpanEvent(span, remoteHost, change) {
|
|
2126
|
+
span.addEvent("activitypub.circuit_breaker.state_change", {
|
|
2127
|
+
"activitypub.remote.host": remoteHost,
|
|
2128
|
+
"activitypub.circuit_breaker.previous_state": toCircuitBreakerMetricState(change.previousState),
|
|
2129
|
+
"activitypub.circuit_breaker.state": toCircuitBreakerMetricState(change.newState)
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
function recordCircuitBreakerHeldSpanEvent(span, remoteHost, state) {
|
|
2133
|
+
span.addEvent("activitypub.circuit_breaker.held", {
|
|
2134
|
+
"activitypub.remote.host": remoteHost,
|
|
2135
|
+
"activitypub.circuit_breaker.state": toCircuitBreakerMetricState(state)
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2086
2138
|
function isRemoteContextLoadingFailure(error) {
|
|
2087
2139
|
return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
|
|
2088
2140
|
}
|
|
@@ -2126,6 +2178,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2126
2178
|
skipSignatureVerification;
|
|
2127
2179
|
outboxRetryPolicy;
|
|
2128
2180
|
inboxRetryPolicy;
|
|
2181
|
+
circuitBreaker;
|
|
2129
2182
|
activityTransformers;
|
|
2130
2183
|
_tracerProvider;
|
|
2131
2184
|
_meterProvider;
|
|
@@ -2140,6 +2193,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2140
2193
|
publicKey: ["_fedify", "publicKey"],
|
|
2141
2194
|
httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
|
|
2142
2195
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
2196
|
+
circuitBreaker: ["_fedify", "circuit"],
|
|
2143
2197
|
...options.kvPrefixes ?? {}
|
|
2144
2198
|
};
|
|
2145
2199
|
if (options.queue == null) {
|
|
@@ -2155,6 +2209,25 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2155
2209
|
this.outboxQueue = options.queue.outbox;
|
|
2156
2210
|
this.fanoutQueue = options.queue.fanout;
|
|
2157
2211
|
}
|
|
2212
|
+
if (options.circuitBreaker !== false && this.outboxQueue != null) {
|
|
2213
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
2214
|
+
kv: options.kv,
|
|
2215
|
+
prefix: this.kvPrefixes.circuitBreaker,
|
|
2216
|
+
options: options.circuitBreaker,
|
|
2217
|
+
stateChangeObserver: (remoteHost, _previousState, newState) => {
|
|
2218
|
+
const metricState = toCircuitBreakerMetricState(newState);
|
|
2219
|
+
recordCircuitBreakerStateChange(this.meterProvider, remoteHost, metricState);
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
if (options.kv.cas == null && !circuitBreakerCasWarningKvStores.has(options.kv)) {
|
|
2223
|
+
circuitBreakerCasWarningKvStores.add(options.kv);
|
|
2224
|
+
getLogger([
|
|
2225
|
+
"fedify",
|
|
2226
|
+
"federation",
|
|
2227
|
+
"circuit"
|
|
2228
|
+
]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2158
2231
|
this.inboxQueueStarted = false;
|
|
2159
2232
|
this.outboxQueueStarted = false;
|
|
2160
2233
|
this.fanoutQueueStarted = false;
|
|
@@ -2463,19 +2536,129 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2463
2536
|
if (rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKeyPair = pair;
|
|
2464
2537
|
keys.push(pair);
|
|
2465
2538
|
}
|
|
2539
|
+
const loaderOptions = this.#getLoaderOptions(message.baseUrl);
|
|
2540
|
+
let parsedActorIds;
|
|
2541
|
+
const getActorIds = () => {
|
|
2542
|
+
parsedActorIds ??= (message.actorIds ?? []).flatMap((id) => {
|
|
2543
|
+
try {
|
|
2544
|
+
return [new URL(id)];
|
|
2545
|
+
} catch {
|
|
2546
|
+
logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
|
|
2547
|
+
return [];
|
|
2548
|
+
}
|
|
2549
|
+
});
|
|
2550
|
+
return parsedActorIds;
|
|
2551
|
+
};
|
|
2552
|
+
const parseActivity = () => Activity.fromJsonLd(message.activity, {
|
|
2553
|
+
contextLoader: this.contextLoaderFactory(loaderOptions),
|
|
2554
|
+
documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
|
|
2555
|
+
tracerProvider: this.tracerProvider
|
|
2556
|
+
});
|
|
2557
|
+
const enqueueHeldOutboxMessage = async (delay, heldSince) => {
|
|
2558
|
+
const { outboxQueue } = this;
|
|
2559
|
+
if (outboxQueue == null) return;
|
|
2560
|
+
const heldMessage = {
|
|
2561
|
+
...message,
|
|
2562
|
+
circuitHeld: true,
|
|
2563
|
+
circuitHeldSince: heldSince.toString()
|
|
2564
|
+
};
|
|
2565
|
+
await outboxQueue.enqueue(heldMessage, {
|
|
2566
|
+
delay: clampNegativeDelay(delay),
|
|
2567
|
+
orderingKey: message.orderingKey
|
|
2568
|
+
});
|
|
2569
|
+
getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
2570
|
+
role: "outbox",
|
|
2571
|
+
queue: outboxQueue,
|
|
2572
|
+
activityType: heldMessage.activityType
|
|
2573
|
+
}, heldMessage.attempt);
|
|
2574
|
+
};
|
|
2575
|
+
const dropHeldOutboxMessage = async (circuit, remoteHost, inbox, heldSince, activity) => {
|
|
2576
|
+
await circuit.dropActivity(remoteHost, {
|
|
2577
|
+
inbox,
|
|
2578
|
+
activity,
|
|
2579
|
+
activityId: message.activityId,
|
|
2580
|
+
activityType: message.activityType,
|
|
2581
|
+
actorIds: getActorIds(),
|
|
2582
|
+
heldSince
|
|
2583
|
+
});
|
|
2584
|
+
if (this.outboxPermanentFailureHandler != null) {
|
|
2585
|
+
const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
|
|
2586
|
+
try {
|
|
2587
|
+
await this.outboxPermanentFailureHandler(ctx, {
|
|
2588
|
+
reason: "circuit-breaker-ttl",
|
|
2589
|
+
inbox,
|
|
2590
|
+
activity,
|
|
2591
|
+
error: new SendActivityError(inbox, 0, "Circuit breaker held activity expired.", ""),
|
|
2592
|
+
statusCode: 0,
|
|
2593
|
+
circuitHeldSince: heldSince,
|
|
2594
|
+
actorIds: getActorIds()
|
|
2595
|
+
});
|
|
2596
|
+
} catch (handlerError) {
|
|
2597
|
+
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
2598
|
+
...logData,
|
|
2599
|
+
error: handlerError
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
2604
|
+
};
|
|
2466
2605
|
try {
|
|
2606
|
+
const inbox = new URL(message.inbox);
|
|
2607
|
+
const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
|
|
2608
|
+
const remoteHost = getRemoteHost(inbox);
|
|
2609
|
+
let decision;
|
|
2610
|
+
if (circuit != null) try {
|
|
2611
|
+
decision = await circuit.beforeSend(remoteHost, message);
|
|
2612
|
+
} catch (circuitError) {
|
|
2613
|
+
getLogger([
|
|
2614
|
+
"fedify",
|
|
2615
|
+
"federation",
|
|
2616
|
+
"circuit"
|
|
2617
|
+
]).error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
|
|
2618
|
+
...logData,
|
|
2619
|
+
remoteHost,
|
|
2620
|
+
error: circuitError
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
if (decision != null && circuit != null) {
|
|
2624
|
+
if (decision.type === "hold") {
|
|
2625
|
+
recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
|
|
2626
|
+
await enqueueHeldOutboxMessage(decision.delay, decision.heldSince);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
if (decision.type === "drop") {
|
|
2630
|
+
const activity = await parseActivity();
|
|
2631
|
+
await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
|
|
2635
|
+
}
|
|
2467
2636
|
await sendActivity({
|
|
2468
2637
|
keys,
|
|
2469
2638
|
activity: message.activity,
|
|
2470
2639
|
activityId: message.activityId,
|
|
2471
2640
|
activityType: message.activityType,
|
|
2472
|
-
inbox
|
|
2641
|
+
inbox,
|
|
2473
2642
|
sharedInbox: message.sharedInbox,
|
|
2474
2643
|
headers: new Headers(message.headers),
|
|
2475
2644
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
2476
2645
|
meterProvider: this.meterProvider,
|
|
2477
2646
|
tracerProvider: this.tracerProvider
|
|
2478
2647
|
});
|
|
2648
|
+
if (circuit != null) try {
|
|
2649
|
+
const stateChange = await circuit.recordSuccess(remoteHost);
|
|
2650
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
2651
|
+
} catch (error) {
|
|
2652
|
+
getLogger([
|
|
2653
|
+
"fedify",
|
|
2654
|
+
"federation",
|
|
2655
|
+
"circuit"
|
|
2656
|
+
]).error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
|
|
2657
|
+
...logData,
|
|
2658
|
+
remoteHost,
|
|
2659
|
+
error
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2479
2662
|
} catch (error) {
|
|
2480
2663
|
span.setStatus({
|
|
2481
2664
|
code: SpanStatusCode.ERROR,
|
|
@@ -2490,18 +2673,65 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2490
2673
|
return;
|
|
2491
2674
|
}
|
|
2492
2675
|
})();
|
|
2676
|
+
let retryAfterDelay;
|
|
2677
|
+
let circuitHold;
|
|
2678
|
+
let circuitDrop;
|
|
2679
|
+
let retryPolicyDelay;
|
|
2680
|
+
let policyDelayCalculated = false;
|
|
2681
|
+
const getPolicyDelay = () => {
|
|
2682
|
+
if (!policyDelayCalculated) {
|
|
2683
|
+
retryPolicyDelay = this.outboxRetryPolicy({
|
|
2684
|
+
elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
|
|
2685
|
+
attempts: message.attempt
|
|
2686
|
+
});
|
|
2687
|
+
policyDelayCalculated = true;
|
|
2688
|
+
}
|
|
2689
|
+
return retryPolicyDelay;
|
|
2690
|
+
};
|
|
2691
|
+
const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
|
|
2692
|
+
if (!isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503)) retryAfterDelay = parseRetryAfter(error.responseHeaders);
|
|
2693
|
+
if (remoteHost != null && this.outboxQueue != null && this.circuitBreaker != null) try {
|
|
2694
|
+
if (error instanceof SendActivityError) {
|
|
2695
|
+
const { statusCode } = error;
|
|
2696
|
+
const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await this.circuitBreaker.recordReachableFailure(remoteHost) : statusCode >= 500 ? await this.circuitBreaker.recordFailure(remoteHost) : void 0;
|
|
2697
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
2698
|
+
} else if (isTransportDeliveryError(error)) {
|
|
2699
|
+
const stateChange = await this.circuitBreaker.recordFailure(remoteHost);
|
|
2700
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
2701
|
+
}
|
|
2702
|
+
if (!isPermanentFailure) {
|
|
2703
|
+
const circuitDecision = await this.circuitBreaker.beforeSend(remoteHost, message);
|
|
2704
|
+
if (circuitDecision.type === "hold") circuitHold = {
|
|
2705
|
+
delay: circuitDecision.delay,
|
|
2706
|
+
heldSince: circuitDecision.heldSince,
|
|
2707
|
+
remoteHost,
|
|
2708
|
+
state: circuitDecision.state
|
|
2709
|
+
};
|
|
2710
|
+
else if (circuitDecision.type === "drop") circuitDrop = {
|
|
2711
|
+
circuit: this.circuitBreaker,
|
|
2712
|
+
remoteHost,
|
|
2713
|
+
inbox: new URL(message.inbox),
|
|
2714
|
+
heldSince: circuitDecision.heldSince
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
} catch (circuitError) {
|
|
2718
|
+
getLogger([
|
|
2719
|
+
"fedify",
|
|
2720
|
+
"federation",
|
|
2721
|
+
"circuit"
|
|
2722
|
+
]).error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
|
|
2723
|
+
...logData,
|
|
2724
|
+
remoteHost,
|
|
2725
|
+
error: circuitError
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2493
2728
|
span.addEvent("activitypub.delivery.failed", {
|
|
2494
2729
|
...remoteHost == null ? {} : { "activitypub.remote.host": remoteHost },
|
|
2495
2730
|
"activitypub.delivery.attempt": message.attempt,
|
|
2496
|
-
"activitypub.delivery.permanent_failure":
|
|
2731
|
+
"activitypub.delivery.permanent_failure": isPermanentFailure,
|
|
2497
2732
|
...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
|
|
2498
2733
|
});
|
|
2499
|
-
const
|
|
2500
|
-
const activity = await Activity.fromJsonLd(message.activity, {
|
|
2501
|
-
contextLoader: this.contextLoaderFactory(loaderOptions),
|
|
2502
|
-
documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
|
|
2503
|
-
tracerProvider: this.tracerProvider
|
|
2504
|
-
});
|
|
2734
|
+
const activity = await parseActivity();
|
|
2505
2735
|
try {
|
|
2506
2736
|
await this.onOutboxError?.(error, activity);
|
|
2507
2737
|
} catch (error) {
|
|
@@ -2510,7 +2740,11 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2510
2740
|
error
|
|
2511
2741
|
});
|
|
2512
2742
|
}
|
|
2513
|
-
if (
|
|
2743
|
+
if (circuitDrop != null) {
|
|
2744
|
+
await dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
if (isPermanentFailure) {
|
|
2514
2748
|
getFederationMetrics(this.meterProvider).recordPermanentFailure(error.inbox, error.statusCode);
|
|
2515
2749
|
logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
|
|
2516
2750
|
...logData,
|
|
@@ -2520,18 +2754,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2520
2754
|
const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
|
|
2521
2755
|
try {
|
|
2522
2756
|
await this.outboxPermanentFailureHandler(ctx, {
|
|
2757
|
+
reason: "http",
|
|
2523
2758
|
inbox: new URL(message.inbox),
|
|
2524
2759
|
activity,
|
|
2525
2760
|
error,
|
|
2526
2761
|
statusCode: error.statusCode,
|
|
2527
|
-
actorIds: (
|
|
2528
|
-
try {
|
|
2529
|
-
return [new URL(id)];
|
|
2530
|
-
} catch {
|
|
2531
|
-
logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
|
|
2532
|
-
return [];
|
|
2533
|
-
}
|
|
2534
|
-
})
|
|
2762
|
+
actorIds: getActorIds()
|
|
2535
2763
|
});
|
|
2536
2764
|
} catch (handlerError) {
|
|
2537
2765
|
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
@@ -2543,17 +2771,25 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2543
2771
|
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
2544
2772
|
return;
|
|
2545
2773
|
}
|
|
2546
|
-
if (
|
|
2774
|
+
if (circuitHold != null && getPolicyDelay() != null) {
|
|
2775
|
+
logger.error("Failed to send activity {activityId} to {inbox}; holding because the remote host circuit is open:\n{error}", {
|
|
2776
|
+
...logData,
|
|
2777
|
+
error
|
|
2778
|
+
});
|
|
2779
|
+
recordCircuitBreakerHeldSpanEvent(span, circuitHold.remoteHost, circuitHold.state);
|
|
2780
|
+
const circuit = this.circuitBreaker;
|
|
2781
|
+
await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince);
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
if (this.outboxQueue?.nativeRetrial && retryAfterDelay == null) {
|
|
2547
2785
|
logger.error("Failed to send activity {activityId} to {inbox}; backend will handle retry:\n{error}", {
|
|
2548
2786
|
...logData,
|
|
2549
2787
|
error
|
|
2550
2788
|
});
|
|
2551
2789
|
throw error;
|
|
2552
2790
|
}
|
|
2553
|
-
const
|
|
2554
|
-
|
|
2555
|
-
attempts: message.attempt
|
|
2556
|
-
});
|
|
2791
|
+
const policyDelay = getPolicyDelay();
|
|
2792
|
+
const delay = policyDelay == null ? null : retryAfterDelay ?? policyDelay;
|
|
2557
2793
|
if (delay != null) {
|
|
2558
2794
|
logger.error("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); retry...:\n{error}", {
|
|
2559
2795
|
...logData,
|
|
@@ -2565,7 +2801,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2565
2801
|
};
|
|
2566
2802
|
const { outboxQueue } = this;
|
|
2567
2803
|
if (outboxQueue != null) {
|
|
2568
|
-
await outboxQueue.enqueue(retryMessage, {
|
|
2804
|
+
await outboxQueue.enqueue(retryMessage, {
|
|
2805
|
+
delay: clampNegativeDelay(delay),
|
|
2806
|
+
orderingKey: message.orderingKey
|
|
2807
|
+
});
|
|
2569
2808
|
getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
2570
2809
|
role: "outbox",
|
|
2571
2810
|
queue: outboxQueue,
|
|
@@ -2654,7 +2893,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2654
2893
|
...message,
|
|
2655
2894
|
attempt: message.attempt + 1
|
|
2656
2895
|
};
|
|
2657
|
-
await this.inboxQueue.enqueue(retryMessage, { delay:
|
|
2896
|
+
await this.inboxQueue.enqueue(retryMessage, { delay: clampNegativeDelay(delay) });
|
|
2658
2897
|
if (activityType != null) {
|
|
2659
2898
|
getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
2660
2899
|
role: "inbox",
|