@fedify/fedify 2.3.0-dev.1189 → 2.3.0-dev.1190

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.
Files changed (76) hide show
  1. package/dist/{builder-Dc6s3gPe.mjs → builder-BzgNpXoY.mjs} +2 -2
  2. package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/{context-CRXCkTM6.d.cts → context-DMHK7jqX.d.cts} +224 -3
  7. package/dist/{context-MgCh7YGu.d.ts → context-K9cg8oGx.d.ts} +224 -3
  8. package/dist/{deno-BomxIkHS.mjs → deno-CoAwVm1I.mjs} +1 -1
  9. package/dist/{docloader-CzS6F5sZ.mjs → docloader-hPqZT20O.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/circuit-breaker.test.d.mts +2 -0
  12. package/dist/federation/circuit-breaker.test.mjs +446 -0
  13. package/dist/federation/collection.test.mjs +1 -1
  14. package/dist/federation/handler.test.mjs +3 -3
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/keycache.test.mjs +1 -1
  17. package/dist/federation/metrics.test.mjs +16 -1
  18. package/dist/federation/middleware.test.mjs +817 -6
  19. package/dist/federation/mod.cjs +4 -1
  20. package/dist/federation/mod.d.cts +3 -3
  21. package/dist/federation/mod.d.ts +3 -3
  22. package/dist/federation/mod.js +2 -2
  23. package/dist/federation/negotiation.test.mjs +1 -1
  24. package/dist/federation/retry.test.mjs +1 -1
  25. package/dist/federation/send.test.mjs +43 -10
  26. package/dist/federation/temporal.test.mjs +1 -1
  27. package/dist/federation/webfinger.test.mjs +1 -1
  28. package/dist/{getMachineId-bsd-BY01PL1n.mjs → getMachineId-bsd-Bn0le7-J.mjs} +1 -1
  29. package/dist/{getMachineId-darwin-Dr1gkBkp.mjs → getMachineId-darwin-CVjKuDgj.mjs} +1 -1
  30. package/dist/{getMachineId-win-QEYwcJiy.mjs → getMachineId-win-c5zxTSS1.mjs} +1 -1
  31. package/dist/{http-DnJyL_6c.cjs → http-BAarxBe5.cjs} +30 -5
  32. package/dist/{http-DtWN_XvX.mjs → http-CSwCAQ-H.mjs} +3 -3
  33. package/dist/{http-B-psRIq6.js → http-Dq_qElWc.js} +25 -6
  34. package/dist/{key-CT2NnJuR.mjs → key-DYK_T_PD.mjs} +2 -2
  35. package/dist/{kv-cache-DKhLDCH8.js → kv-cache-BhPocHdd.js} +1 -1
  36. package/dist/{kv-cache-Bf8AoV6C.mjs → kv-cache-CFzIDCMJ.mjs} +1 -1
  37. package/dist/{kv-cache-CVre456Y.cjs → kv-cache-Ds1kjvnu.cjs} +1 -1
  38. package/dist/{ld-DCyQasTE.mjs → ld-BdcT_irA.mjs} +3 -3
  39. package/dist/{metrics-xgr0P4hO.mjs → metrics-Ci97wkob.mjs} +25 -6
  40. package/dist/{middleware-DK0thDHX.mjs → middleware-BUGT2LmO.mjs} +279 -40
  41. package/dist/{middleware-BgbdoV61.js → middleware-C-C_I_wJ.js} +615 -32
  42. package/dist/{middleware-DIJ_6KFI.cjs → middleware-ddMAHsyF.cjs} +632 -31
  43. package/dist/{middleware-sgx08IEk.mjs → middleware-hWs3qtrr.mjs} +1 -1
  44. package/dist/{mod-CpQHB3Ys.d.ts → mod-CfOFqS0w.d.ts} +1 -1
  45. package/dist/{mod-C7HOzGqH.d.cts → mod-YLnSsEHY.d.cts} +1 -1
  46. package/dist/mod.cjs +7 -4
  47. package/dist/mod.d.cts +4 -4
  48. package/dist/mod.d.ts +4 -4
  49. package/dist/mod.js +5 -5
  50. package/dist/nodeinfo/handler.test.mjs +1 -1
  51. package/dist/{owner-BIU_Sl7y.mjs → owner-B8ePZh4q.mjs} +2 -2
  52. package/dist/{proof-B9xbksrX.cjs → proof-CXdtqYKw.cjs} +1 -1
  53. package/dist/{proof-DDs7BRl7.mjs → proof-CzqluPMh.mjs} +3 -3
  54. package/dist/{proof-B5defvTr.js → proof-Dq_RyTjd.js} +1 -1
  55. package/dist/{send-BuxDCpxz.mjs → send-NzJqiStx.mjs} +21 -7
  56. package/dist/sig/http.test.mjs +2 -2
  57. package/dist/sig/key.test.mjs +1 -1
  58. package/dist/sig/ld.test.mjs +2 -2
  59. package/dist/sig/mod.cjs +2 -2
  60. package/dist/sig/mod.js +2 -2
  61. package/dist/sig/owner.test.mjs +1 -1
  62. package/dist/sig/proof.test.mjs +1 -1
  63. package/dist/{temporal-DHgeMWiP.mjs → temporal-CnhE0LLn.mjs} +1 -1
  64. package/dist/testing/mod.d.mts +36 -2
  65. package/dist/utils/docloader.test.mjs +2 -2
  66. package/dist/utils/kv-cache.test.mjs +1 -1
  67. package/dist/utils/mod.cjs +1 -1
  68. package/dist/utils/mod.js +1 -1
  69. package/package.json +7 -7
  70. /package/dist/{collection-CA3V5zyK.mjs → collection-Cc3DVAhE.mjs} +0 -0
  71. /package/dist/{execAsync-Dxb7rNf3.mjs → execAsync-Dmet7-28.mjs} +0 -0
  72. /package/dist/{getMachineId-linux-Bbhofx-s.mjs → getMachineId-linux-DbG4BXa-.mjs} +0 -0
  73. /package/dist/{getMachineId-unsupported-dIOte2Ct.mjs → getMachineId-unsupported-lC8T9hPE.mjs} +0 -0
  74. /package/dist/{keycache-BYMd8q7F.mjs → keycache-BeU0LCII.mjs} +0 -0
  75. /package/dist/{negotiation-CDW-_gUU.mjs → negotiation-DDstyBvc.mjs} +0 -0
  76. /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-BomxIkHS.mjs";
5
- import { _ as recordOutboxActivity, a as instrumentDocumentLoader, c as recordCollectionDispatchDuration, d as recordCollectionTotalItems, h as recordInboxActivity, i as getRemoteHost, l as recordCollectionPageItems, m as recordFanoutRecipients, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionRequest, v as recordOutboxEnqueue, y as recordWebFingerHandle } from "./metrics-xgr0P4hO.mjs";
4
+ import { n as version, t as name } from "./deno-CoAwVm1I.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-Ci97wkob.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-CceiKpCy.mjs";
7
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-CT2NnJuR.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-DtWN_XvX.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-CzS6F5sZ.mjs";
10
- import { n as kvCache } from "./kv-cache-Bf8AoV6C.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-DCyQasTE.mjs";
12
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-BIU_Sl7y.mjs";
7
+ import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-DYK_T_PD.mjs";
8
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-CSwCAQ-H.mjs";
9
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-hPqZT20O.mjs";
10
+ import { n as kvCache } from "./kv-cache-CFzIDCMJ.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-BdcT_irA.mjs";
12
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-B8ePZh4q.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-DDs7BRl7.mjs";
14
+ import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-CzqluPMh.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-Dc6s3gPe.mjs";
18
- import { t as buildCollectionSynchronizationHeader } from "./collection-CA3V5zyK.mjs";
19
- import { t as KvKeyCache } from "./keycache-BYMd8q7F.mjs";
20
- import { t as acceptsJsonLd } from "./negotiation-CDW-_gUU.mjs";
21
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-DHgeMWiP.mjs";
22
- import { t as createExponentialBackoffPolicy } from "./retry-_VvV0h9f.mjs";
23
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-BuxDCpxz.mjs";
17
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-BzgNpXoY.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-CnhE0LLn.mjs";
23
+ import { t as createExponentialBackoffPolicy } from "./retry-CXg_MBI-.mjs";
24
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-NzJqiStx.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: new URL(message.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": error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode),
2731
+ "activitypub.delivery.permanent_failure": isPermanentFailure,
2497
2732
  ...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
2498
2733
  });
2499
- const loaderOptions = this.#getLoaderOptions(message.baseUrl);
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 (error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode)) {
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: (message.actorIds ?? []).flatMap((id) => {
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 (this.outboxQueue?.nativeRetrial) {
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 delay = this.outboxRetryPolicy({
2554
- elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
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, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
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: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : 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",