@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.
- package/dist/{builder-Dc6s3gPe.mjs → builder-BzgNpXoY.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-CoAwVm1I.mjs} +1 -1
- package/dist/{docloader-CzS6F5sZ.mjs → docloader-hPqZT20O.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-DnJyL_6c.cjs → http-BAarxBe5.cjs} +30 -5
- package/dist/{http-DtWN_XvX.mjs → http-CSwCAQ-H.mjs} +3 -3
- package/dist/{http-B-psRIq6.js → http-Dq_qElWc.js} +25 -6
- package/dist/{key-CT2NnJuR.mjs → key-DYK_T_PD.mjs} +2 -2
- package/dist/{kv-cache-DKhLDCH8.js → kv-cache-BhPocHdd.js} +1 -1
- package/dist/{kv-cache-Bf8AoV6C.mjs → kv-cache-CFzIDCMJ.mjs} +1 -1
- package/dist/{kv-cache-CVre456Y.cjs → kv-cache-Ds1kjvnu.cjs} +1 -1
- package/dist/{ld-DCyQasTE.mjs → ld-BdcT_irA.mjs} +3 -3
- package/dist/{metrics-xgr0P4hO.mjs → metrics-Ci97wkob.mjs} +25 -6
- package/dist/{middleware-DK0thDHX.mjs → middleware-BUGT2LmO.mjs} +279 -40
- package/dist/{middleware-BgbdoV61.js → middleware-C-C_I_wJ.js} +615 -32
- package/dist/{middleware-DIJ_6KFI.cjs → middleware-ddMAHsyF.cjs} +632 -31
- package/dist/{middleware-sgx08IEk.mjs → middleware-hWs3qtrr.mjs} +1 -1
- 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-B8ePZh4q.mjs} +2 -2
- package/dist/{proof-B9xbksrX.cjs → proof-CXdtqYKw.cjs} +1 -1
- package/dist/{proof-DDs7BRl7.mjs → proof-CzqluPMh.mjs} +3 -3
- package/dist/{proof-B5defvTr.js → proof-Dq_RyTjd.js} +1 -1
- package/dist/{send-BuxDCpxz.mjs → send-NzJqiStx.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-CnhE0LLn.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 +7 -7
- /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
|
@@ -2,10 +2,10 @@ const { Temporal } = require("@js-temporal/polyfill");
|
|
|
2
2
|
const { URLPattern } = require("urlpattern-polyfill");
|
|
3
3
|
const require_chunk = require("./chunk-DDcVe30Y.cjs");
|
|
4
4
|
const require_transformers = require("./transformers-NeAONrAq.cjs");
|
|
5
|
-
const require_http = require("./http-
|
|
6
|
-
const require_proof = require("./proof-
|
|
5
|
+
const require_http = require("./http-BAarxBe5.cjs");
|
|
6
|
+
const require_proof = require("./proof-CXdtqYKw.cjs");
|
|
7
7
|
const require_types = require("./types-KC4QAoxe.cjs");
|
|
8
|
-
const require_kv_cache = require("./kv-cache-
|
|
8
|
+
const require_kv_cache = require("./kv-cache-Ds1kjvnu.cjs");
|
|
9
9
|
let _logtape_logtape = require("@logtape/logtape");
|
|
10
10
|
let _fedify_uri_template = require("@fedify/uri-template");
|
|
11
11
|
let _fedify_vocab = require("@fedify/vocab");
|
|
@@ -653,6 +653,338 @@ function createFederationBuilder() {
|
|
|
653
653
|
return new FederationBuilderImpl();
|
|
654
654
|
}
|
|
655
655
|
//#endregion
|
|
656
|
+
//#region src/federation/circuit-breaker.ts
|
|
657
|
+
const MAX_CUSTOM_FAILURE_HISTORY = 100;
|
|
658
|
+
/**
|
|
659
|
+
* Tracks reachability state for remote outbox delivery hosts.
|
|
660
|
+
* @since 2.3.0
|
|
661
|
+
*/
|
|
662
|
+
var CircuitBreaker = class {
|
|
663
|
+
#kv;
|
|
664
|
+
#prefix;
|
|
665
|
+
#options;
|
|
666
|
+
#now;
|
|
667
|
+
#stateChangeObserver;
|
|
668
|
+
constructor(options) {
|
|
669
|
+
this.#kv = options.kv;
|
|
670
|
+
this.#prefix = options.prefix;
|
|
671
|
+
this.#options = normalizeCircuitBreakerOptions(options.options ?? {});
|
|
672
|
+
this.#now = options.now ?? (() => Temporal.Now.instant());
|
|
673
|
+
this.#stateChangeObserver = options.stateChangeObserver;
|
|
674
|
+
}
|
|
675
|
+
get options() {
|
|
676
|
+
return this.#options;
|
|
677
|
+
}
|
|
678
|
+
capHeldDelay(heldSince, delay) {
|
|
679
|
+
const now = this.#now();
|
|
680
|
+
return now.until(this.#capHeldRetryAt(now, heldSince, now.add(delay)));
|
|
681
|
+
}
|
|
682
|
+
async beforeSend(remoteHost, message) {
|
|
683
|
+
const heldSince = parseHeldSince(message.circuitHeldSince);
|
|
684
|
+
const now = this.#now();
|
|
685
|
+
if (heldSince != null && Temporal.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
|
|
686
|
+
type: "drop",
|
|
687
|
+
heldSince
|
|
688
|
+
};
|
|
689
|
+
let lastConflictingState;
|
|
690
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
691
|
+
const oldState = await this.#get(remoteHost);
|
|
692
|
+
if (oldState == null || oldState.state === "closed") return {
|
|
693
|
+
type: "send",
|
|
694
|
+
probe: false
|
|
695
|
+
};
|
|
696
|
+
if (oldState.state === "half-open") {
|
|
697
|
+
const halfOpened = oldState.halfOpened == null ? void 0 : Temporal.Instant.from(oldState.halfOpened);
|
|
698
|
+
if (halfOpened != null) {
|
|
699
|
+
const staleAt = halfOpened.add(this.#options.recoveryDelay);
|
|
700
|
+
if (Temporal.Instant.compare(now, staleAt) < 0) {
|
|
701
|
+
const releaseAt = now.add(this.#options.releaseInterval);
|
|
702
|
+
const retryAt = Temporal.Instant.compare(releaseAt, staleAt) < 0 ? releaseAt : staleAt;
|
|
703
|
+
const cappedRetryAt = this.#capHeldRetryAt(now, heldSince, retryAt);
|
|
704
|
+
return {
|
|
705
|
+
type: "hold",
|
|
706
|
+
state: "half-open",
|
|
707
|
+
delay: now.until(cappedRetryAt),
|
|
708
|
+
heldSince: heldSince ?? now
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const newState = {
|
|
713
|
+
...oldState,
|
|
714
|
+
state: "half-open",
|
|
715
|
+
halfOpened: now.toString()
|
|
716
|
+
};
|
|
717
|
+
if (await this.#replace(remoteHost, oldState, newState)) return {
|
|
718
|
+
type: "send",
|
|
719
|
+
probe: true
|
|
720
|
+
};
|
|
721
|
+
lastConflictingState = "half-open";
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const probeAt = (oldState.opened == null ? now : Temporal.Instant.from(oldState.opened)).add(this.#options.recoveryDelay);
|
|
725
|
+
if (Temporal.Instant.compare(now, probeAt) < 0) {
|
|
726
|
+
const retryAt = this.#capHeldRetryAt(now, heldSince, probeAt);
|
|
727
|
+
return {
|
|
728
|
+
type: "hold",
|
|
729
|
+
state: "open",
|
|
730
|
+
delay: now.until(retryAt),
|
|
731
|
+
heldSince: heldSince ?? now
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const newState = {
|
|
735
|
+
...oldState,
|
|
736
|
+
state: "half-open",
|
|
737
|
+
halfOpened: now.toString()
|
|
738
|
+
};
|
|
739
|
+
if (await this.#replace(remoteHost, oldState, newState)) {
|
|
740
|
+
await this.#notifyStateChange(remoteHost, "open", "half-open");
|
|
741
|
+
return {
|
|
742
|
+
type: "send",
|
|
743
|
+
probe: true,
|
|
744
|
+
stateChange: {
|
|
745
|
+
previousState: "open",
|
|
746
|
+
newState: "half-open"
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
lastConflictingState = "open";
|
|
751
|
+
}
|
|
752
|
+
if (lastConflictingState != null) {
|
|
753
|
+
const retryAt = this.#capHeldRetryAt(now, heldSince, now.add(this.#options.releaseInterval));
|
|
754
|
+
return {
|
|
755
|
+
type: "hold",
|
|
756
|
+
state: lastConflictingState,
|
|
757
|
+
delay: now.until(retryAt),
|
|
758
|
+
heldSince: heldSince ?? now
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
|
|
762
|
+
}
|
|
763
|
+
async recordSuccess(remoteHost) {
|
|
764
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
765
|
+
const oldState = await this.#get(remoteHost);
|
|
766
|
+
if (oldState == null) return void 0;
|
|
767
|
+
if (await this.#replace(remoteHost, oldState, void 0)) {
|
|
768
|
+
if (oldState.state !== "closed") {
|
|
769
|
+
await this.#notifyStateChange(remoteHost, oldState.state, "closed");
|
|
770
|
+
return {
|
|
771
|
+
previousState: oldState.state,
|
|
772
|
+
newState: "closed"
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
|
|
779
|
+
}
|
|
780
|
+
async recordReachableFailure(remoteHost) {
|
|
781
|
+
return await this.recordSuccess(remoteHost);
|
|
782
|
+
}
|
|
783
|
+
async recordFailure(remoteHost) {
|
|
784
|
+
const now = this.#now();
|
|
785
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
786
|
+
const oldState = await this.#get(remoteHost);
|
|
787
|
+
if (oldState?.state === "open") return void 0;
|
|
788
|
+
const oldFailures = oldState?.failures.map(Temporal.Instant.from) ?? [];
|
|
789
|
+
const failures = this.#options.pruneFailures([...oldFailures, now], now);
|
|
790
|
+
let newState;
|
|
791
|
+
let transition;
|
|
792
|
+
if (oldState?.state === "half-open" || this.#options.failure(failures)) {
|
|
793
|
+
newState = {
|
|
794
|
+
state: "open",
|
|
795
|
+
failures: failures.map((t) => t.toString()),
|
|
796
|
+
opened: now.toString()
|
|
797
|
+
};
|
|
798
|
+
transition = [oldState?.state ?? "closed", "open"];
|
|
799
|
+
} else newState = {
|
|
800
|
+
state: "closed",
|
|
801
|
+
failures: failures.map((t) => t.toString())
|
|
802
|
+
};
|
|
803
|
+
if (await this.#replace(remoteHost, oldState, newState)) {
|
|
804
|
+
if (transition != null) {
|
|
805
|
+
await this.#notifyStateChange(remoteHost, transition[0], transition[1]);
|
|
806
|
+
return {
|
|
807
|
+
previousState: transition[0],
|
|
808
|
+
newState: transition[1]
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
|
|
815
|
+
}
|
|
816
|
+
async dropActivity(remoteHost, details) {
|
|
817
|
+
try {
|
|
818
|
+
await this.#options.onActivityDrop?.(remoteHost, details);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
(0, _logtape_logtape.getLogger)([
|
|
821
|
+
"fedify",
|
|
822
|
+
"federation",
|
|
823
|
+
"circuit"
|
|
824
|
+
]).error("An unexpected error occurred in circuit breaker activity drop handler:\n{error}", {
|
|
825
|
+
remoteHost,
|
|
826
|
+
error
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async getState(remoteHost) {
|
|
831
|
+
return await this.#get(remoteHost);
|
|
832
|
+
}
|
|
833
|
+
#key(remoteHost) {
|
|
834
|
+
return [...this.#prefix, remoteHost];
|
|
835
|
+
}
|
|
836
|
+
#capHeldRetryAt(now, heldSince, retryAt) {
|
|
837
|
+
const expiresAt = (heldSince ?? now).add(this.#options.heldActivityTtl);
|
|
838
|
+
return Temporal.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
|
|
839
|
+
}
|
|
840
|
+
async #get(remoteHost) {
|
|
841
|
+
return parseCircuitBreakerKvState(await this.#kv.get(this.#key(remoteHost)));
|
|
842
|
+
}
|
|
843
|
+
async #replace(remoteHost, oldState, newState) {
|
|
844
|
+
const key = this.#key(remoteHost);
|
|
845
|
+
if (this.#kv.cas == null) {
|
|
846
|
+
if (newState == null) await this.#kv.delete(key);
|
|
847
|
+
else await this.#kv.set(key, newState);
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
return await this.#kv.cas(key, oldState, newState);
|
|
851
|
+
}
|
|
852
|
+
async #notifyStateChange(remoteHost, previousState, newState) {
|
|
853
|
+
try {
|
|
854
|
+
await this.#options.onStateChange?.(remoteHost, previousState, newState);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
(0, _logtape_logtape.getLogger)([
|
|
857
|
+
"fedify",
|
|
858
|
+
"federation",
|
|
859
|
+
"circuit"
|
|
860
|
+
]).error("An unexpected error occurred in circuit breaker state change handler:\n{error}", {
|
|
861
|
+
remoteHost,
|
|
862
|
+
previousState,
|
|
863
|
+
newState,
|
|
864
|
+
error
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
await this.#stateChangeObserver?.(remoteHost, previousState, newState);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
(0, _logtape_logtape.getLogger)([
|
|
871
|
+
"fedify",
|
|
872
|
+
"federation",
|
|
873
|
+
"circuit"
|
|
874
|
+
]).error("An unexpected error occurred in circuit breaker state change observer:\n{error}", {
|
|
875
|
+
remoteHost,
|
|
876
|
+
previousState,
|
|
877
|
+
newState,
|
|
878
|
+
error
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
/**
|
|
884
|
+
* Normalizes user-provided circuit breaker options into the internal policy
|
|
885
|
+
* shape used while processing queued outbox deliveries.
|
|
886
|
+
*
|
|
887
|
+
* @param options The public circuit breaker options supplied to Fedify.
|
|
888
|
+
* @returns The normalized failure predicate, failure pruning function,
|
|
889
|
+
* duration values, and optional callbacks with defaults applied.
|
|
890
|
+
* @throws {RangeError} If any configured duration is not positive.
|
|
891
|
+
* @throws {TypeError} If `failureThreshold` is not a positive integer.
|
|
892
|
+
*/
|
|
893
|
+
function normalizeCircuitBreakerOptions(options) {
|
|
894
|
+
const recoveryDelay = toInstantDuration(options.recoveryDelay ?? { minutes: 30 });
|
|
895
|
+
const heldActivityTtl = toInstantDuration(options.heldActivityTtl ?? { hours: 168 });
|
|
896
|
+
const releaseInterval = toInstantDuration(options.releaseInterval ?? { seconds: 1 });
|
|
897
|
+
assertPositiveDuration(recoveryDelay, "recoveryDelay");
|
|
898
|
+
assertPositiveDuration(heldActivityTtl, "heldActivityTtl");
|
|
899
|
+
assertPositiveDuration(releaseInterval, "releaseInterval");
|
|
900
|
+
let failure;
|
|
901
|
+
let pruneFailures;
|
|
902
|
+
if (options.failure == null) {
|
|
903
|
+
const failureThreshold = options.failureThreshold ?? 5;
|
|
904
|
+
if (!Number.isInteger(failureThreshold) || failureThreshold <= 0) throw new TypeError("failureThreshold must be a positive integer.");
|
|
905
|
+
const failureWindow = toInstantDuration(options.failureWindow ?? { minutes: 10 });
|
|
906
|
+
assertPositiveDuration(failureWindow, "failureWindow");
|
|
907
|
+
pruneFailures = (timestamps, now) => {
|
|
908
|
+
const earliest = now.subtract(failureWindow);
|
|
909
|
+
return timestamps.filter((timestamp) => Temporal.Instant.compare(timestamp, earliest) >= 0).slice(-failureThreshold);
|
|
910
|
+
};
|
|
911
|
+
failure = (timestamps) => {
|
|
912
|
+
if (timestamps.length < failureThreshold) return false;
|
|
913
|
+
const first = timestamps[timestamps.length - failureThreshold];
|
|
914
|
+
const last = timestamps[timestamps.length - 1];
|
|
915
|
+
return Temporal.Duration.compare(first.until(last), failureWindow) <= 0;
|
|
916
|
+
};
|
|
917
|
+
} else {
|
|
918
|
+
failure = options.failure;
|
|
919
|
+
pruneFailures = (timestamps) => timestamps.slice(-MAX_CUSTOM_FAILURE_HISTORY);
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
failure,
|
|
923
|
+
pruneFailures,
|
|
924
|
+
recoveryDelay,
|
|
925
|
+
heldActivityTtl,
|
|
926
|
+
releaseInterval,
|
|
927
|
+
onStateChange: options.onStateChange,
|
|
928
|
+
onActivityDrop: options.onActivityDrop
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function toInstantDuration(duration) {
|
|
932
|
+
const parsed = Temporal.Duration.from(duration);
|
|
933
|
+
return Temporal.Duration.from({ milliseconds: Math.trunc(parsed.total({
|
|
934
|
+
unit: "millisecond",
|
|
935
|
+
relativeTo: Temporal.PlainDateTime.from("2026-01-01T00:00:00")
|
|
936
|
+
})) });
|
|
937
|
+
}
|
|
938
|
+
function assertPositiveDuration(duration, name) {
|
|
939
|
+
if (Temporal.Duration.compare(duration, { seconds: 0 }) <= 0) throw new RangeError(`${name} must be a positive duration.`);
|
|
940
|
+
}
|
|
941
|
+
function parseHeldSince(value) {
|
|
942
|
+
if (value == null) return void 0;
|
|
943
|
+
try {
|
|
944
|
+
return Temporal.Instant.from(value);
|
|
945
|
+
} catch (error) {
|
|
946
|
+
(0, _logtape_logtape.getLogger)([
|
|
947
|
+
"fedify",
|
|
948
|
+
"federation",
|
|
949
|
+
"circuit"
|
|
950
|
+
]).warn("Invalid circuitHeldSince value in queued outbox message: {value}", {
|
|
951
|
+
value,
|
|
952
|
+
error
|
|
953
|
+
});
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Parses a value loaded from the circuit breaker KV store.
|
|
959
|
+
*
|
|
960
|
+
* @param value The raw KV value to validate.
|
|
961
|
+
* @returns A circuit breaker state when `value` has a recognized state and
|
|
962
|
+
* valid instant strings, or `undefined` when the stored value is malformed.
|
|
963
|
+
*/
|
|
964
|
+
function parseCircuitBreakerKvState(value) {
|
|
965
|
+
const isInstantString = (v) => {
|
|
966
|
+
if (typeof v !== "string") return false;
|
|
967
|
+
try {
|
|
968
|
+
Temporal.Instant.from(v);
|
|
969
|
+
return true;
|
|
970
|
+
} catch {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
if (typeof value !== "object" || value == null) return void 0;
|
|
975
|
+
const record = value;
|
|
976
|
+
if (record.state !== "closed" && record.state !== "open" && record.state !== "half-open") return;
|
|
977
|
+
if (!Array.isArray(record.failures) || !record.failures.every((failure) => isInstantString(failure))) return;
|
|
978
|
+
if (record.opened != null && !isInstantString(record.opened)) return;
|
|
979
|
+
if (record.halfOpened != null && !isInstantString(record.halfOpened)) return;
|
|
980
|
+
return {
|
|
981
|
+
state: record.state,
|
|
982
|
+
failures: record.failures,
|
|
983
|
+
...record.opened == null ? {} : { opened: record.opened },
|
|
984
|
+
...record.halfOpened == null ? {} : { halfOpened: record.halfOpened }
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
//#endregion
|
|
656
988
|
//#region src/federation/collection.ts
|
|
657
989
|
/**
|
|
658
990
|
* Calculates the [partial follower collection digest][1].
|
|
@@ -2911,13 +3243,14 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
2911
3243
|
specDeterminer
|
|
2912
3244
|
});
|
|
2913
3245
|
} catch (error) {
|
|
3246
|
+
const transportError = error instanceof _fedify_vocab_runtime.FetchError ? error : createFetchError(inbox.href, error);
|
|
2914
3247
|
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
2915
3248
|
activityId,
|
|
2916
3249
|
inbox: inbox.href,
|
|
2917
|
-
error
|
|
3250
|
+
error: transportError
|
|
2918
3251
|
});
|
|
2919
3252
|
federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
|
|
2920
|
-
throw
|
|
3253
|
+
throw transportError;
|
|
2921
3254
|
}
|
|
2922
3255
|
try {
|
|
2923
3256
|
if (!response.ok) {
|
|
@@ -2934,7 +3267,7 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
2934
3267
|
statusText: response.statusText,
|
|
2935
3268
|
error
|
|
2936
3269
|
});
|
|
2937
|
-
throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error);
|
|
3270
|
+
throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
|
|
2938
3271
|
}
|
|
2939
3272
|
deliverySuccess = true;
|
|
2940
3273
|
const eventAttributes = {
|
|
@@ -2949,6 +3282,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
2949
3282
|
federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
|
|
2950
3283
|
}
|
|
2951
3284
|
}
|
|
3285
|
+
function createFetchError(url, cause) {
|
|
3286
|
+
const error = new _fedify_vocab_runtime.FetchError(url, cause instanceof Error ? cause.message : String(cause));
|
|
3287
|
+
error.cause = cause;
|
|
3288
|
+
return error;
|
|
3289
|
+
}
|
|
2952
3290
|
/**
|
|
2953
3291
|
* An error that is thrown when an activity fails to send to a remote inbox.
|
|
2954
3292
|
* It contains structured information about the failure, including the HTTP
|
|
@@ -2972,18 +3310,25 @@ var SendActivityError = class extends Error {
|
|
|
2972
3310
|
*/
|
|
2973
3311
|
responseBody;
|
|
2974
3312
|
/**
|
|
3313
|
+
* The response headers from the inbox.
|
|
3314
|
+
* @since 2.3.0
|
|
3315
|
+
*/
|
|
3316
|
+
responseHeaders;
|
|
3317
|
+
/**
|
|
2975
3318
|
* Creates a new {@link SendActivityError}.
|
|
2976
3319
|
* @param inbox The inbox URL.
|
|
2977
3320
|
* @param statusCode The HTTP status code.
|
|
2978
3321
|
* @param message The error message.
|
|
2979
3322
|
* @param responseBody The response body.
|
|
3323
|
+
* @param responseHeaders The response headers.
|
|
2980
3324
|
*/
|
|
2981
|
-
constructor(inbox, statusCode, message, responseBody) {
|
|
3325
|
+
constructor(inbox, statusCode, message, responseBody, responseHeaders) {
|
|
2982
3326
|
super(message);
|
|
2983
3327
|
this.name = "SendActivityError";
|
|
2984
3328
|
this.inbox = inbox;
|
|
2985
3329
|
this.statusCode = statusCode;
|
|
2986
3330
|
this.responseBody = responseBody;
|
|
3331
|
+
this.responseHeaders = new Headers(responseHeaders);
|
|
2987
3332
|
}
|
|
2988
3333
|
};
|
|
2989
3334
|
//#endregion
|
|
@@ -3182,6 +3527,57 @@ var middleware_exports = /* @__PURE__ */ require_chunk.__exportAll({
|
|
|
3182
3527
|
OutboxContextImpl: () => OutboxContextImpl,
|
|
3183
3528
|
createFederation: () => createFederation
|
|
3184
3529
|
});
|
|
3530
|
+
const circuitBreakerCasWarningKvStores = /* @__PURE__ */ new WeakSet();
|
|
3531
|
+
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})$");
|
|
3532
|
+
function parseRetryAfter(headers, now = Temporal.Now.instant()) {
|
|
3533
|
+
const value = headers.get("Retry-After");
|
|
3534
|
+
if (value == null) return void 0;
|
|
3535
|
+
const trimmed = value.trim();
|
|
3536
|
+
if (/^\d+$/.test(trimmed)) {
|
|
3537
|
+
const seconds = Number(trimmed);
|
|
3538
|
+
if (!Number.isFinite(seconds)) return void 0;
|
|
3539
|
+
return parseRetryAfterDuration({ seconds });
|
|
3540
|
+
}
|
|
3541
|
+
if (!retryAfterHttpDate.test(trimmed)) return void 0;
|
|
3542
|
+
const httpDate = trimmed.endsWith("GMT") ? trimmed : `${trimmed} GMT`;
|
|
3543
|
+
const retryAtMs = Date.parse(httpDate);
|
|
3544
|
+
if (Number.isNaN(retryAtMs)) return void 0;
|
|
3545
|
+
const nowMs = Number(now.epochMilliseconds);
|
|
3546
|
+
return parseRetryAfterDuration({ milliseconds: Math.max(0, retryAtMs - nowMs) });
|
|
3547
|
+
}
|
|
3548
|
+
function parseRetryAfterDuration(durationLike) {
|
|
3549
|
+
try {
|
|
3550
|
+
return Temporal.Duration.from(durationLike);
|
|
3551
|
+
} catch (error) {
|
|
3552
|
+
if (error instanceof RangeError) return void 0;
|
|
3553
|
+
throw error;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
function clampNegativeDelay(delay) {
|
|
3557
|
+
return delay.sign < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay;
|
|
3558
|
+
}
|
|
3559
|
+
function maxDelay(first, second) {
|
|
3560
|
+
return Temporal.Duration.compare(first, second) >= 0 ? first : second;
|
|
3561
|
+
}
|
|
3562
|
+
function isTransportDeliveryError(error) {
|
|
3563
|
+
return error instanceof _fedify_vocab_runtime.FetchError || require_http.isAbortError(error);
|
|
3564
|
+
}
|
|
3565
|
+
function toCircuitBreakerMetricState(state) {
|
|
3566
|
+
return state === "half-open" ? "half_open" : state;
|
|
3567
|
+
}
|
|
3568
|
+
function recordCircuitBreakerSpanEvent(span, remoteHost, change) {
|
|
3569
|
+
span.addEvent("activitypub.circuit_breaker.state_change", {
|
|
3570
|
+
"activitypub.remote.host": remoteHost,
|
|
3571
|
+
"activitypub.circuit_breaker.previous_state": toCircuitBreakerMetricState(change.previousState),
|
|
3572
|
+
"activitypub.circuit_breaker.state": toCircuitBreakerMetricState(change.newState)
|
|
3573
|
+
});
|
|
3574
|
+
}
|
|
3575
|
+
function recordCircuitBreakerHeldSpanEvent(span, remoteHost, state) {
|
|
3576
|
+
span.addEvent("activitypub.circuit_breaker.held", {
|
|
3577
|
+
"activitypub.remote.host": remoteHost,
|
|
3578
|
+
"activitypub.circuit_breaker.state": toCircuitBreakerMetricState(state)
|
|
3579
|
+
});
|
|
3580
|
+
}
|
|
3185
3581
|
function isRemoteContextLoadingFailure(error) {
|
|
3186
3582
|
return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
|
|
3187
3583
|
}
|
|
@@ -3225,6 +3621,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3225
3621
|
skipSignatureVerification;
|
|
3226
3622
|
outboxRetryPolicy;
|
|
3227
3623
|
inboxRetryPolicy;
|
|
3624
|
+
circuitBreaker;
|
|
3228
3625
|
activityTransformers;
|
|
3229
3626
|
_tracerProvider;
|
|
3230
3627
|
_meterProvider;
|
|
@@ -3239,6 +3636,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3239
3636
|
publicKey: ["_fedify", "publicKey"],
|
|
3240
3637
|
httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
|
|
3241
3638
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
3639
|
+
circuitBreaker: ["_fedify", "circuit"],
|
|
3242
3640
|
...options.kvPrefixes ?? {}
|
|
3243
3641
|
};
|
|
3244
3642
|
if (options.queue == null) {
|
|
@@ -3254,6 +3652,25 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3254
3652
|
this.outboxQueue = options.queue.outbox;
|
|
3255
3653
|
this.fanoutQueue = options.queue.fanout;
|
|
3256
3654
|
}
|
|
3655
|
+
if (options.circuitBreaker !== false && this.outboxQueue != null) {
|
|
3656
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
3657
|
+
kv: options.kv,
|
|
3658
|
+
prefix: this.kvPrefixes.circuitBreaker,
|
|
3659
|
+
options: options.circuitBreaker,
|
|
3660
|
+
stateChangeObserver: (remoteHost, _previousState, newState) => {
|
|
3661
|
+
const metricState = toCircuitBreakerMetricState(newState);
|
|
3662
|
+
require_http.recordCircuitBreakerStateChange(this.meterProvider, remoteHost, metricState);
|
|
3663
|
+
}
|
|
3664
|
+
});
|
|
3665
|
+
if (options.kv.cas == null && !circuitBreakerCasWarningKvStores.has(options.kv)) {
|
|
3666
|
+
circuitBreakerCasWarningKvStores.add(options.kv);
|
|
3667
|
+
(0, _logtape_logtape.getLogger)([
|
|
3668
|
+
"fedify",
|
|
3669
|
+
"federation",
|
|
3670
|
+
"circuit"
|
|
3671
|
+
]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3257
3674
|
this.inboxQueueStarted = false;
|
|
3258
3675
|
this.outboxQueueStarted = false;
|
|
3259
3676
|
this.fanoutQueueStarted = false;
|
|
@@ -3562,19 +3979,129 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3562
3979
|
if (rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKeyPair = pair;
|
|
3563
3980
|
keys.push(pair);
|
|
3564
3981
|
}
|
|
3982
|
+
const loaderOptions = this.#getLoaderOptions(message.baseUrl);
|
|
3983
|
+
let parsedActorIds;
|
|
3984
|
+
const getActorIds = () => {
|
|
3985
|
+
parsedActorIds ??= (message.actorIds ?? []).flatMap((id) => {
|
|
3986
|
+
try {
|
|
3987
|
+
return [new URL(id)];
|
|
3988
|
+
} catch {
|
|
3989
|
+
logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
|
|
3990
|
+
return [];
|
|
3991
|
+
}
|
|
3992
|
+
});
|
|
3993
|
+
return parsedActorIds;
|
|
3994
|
+
};
|
|
3995
|
+
const parseActivity = () => _fedify_vocab.Activity.fromJsonLd(message.activity, {
|
|
3996
|
+
contextLoader: this.contextLoaderFactory(loaderOptions),
|
|
3997
|
+
documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
|
|
3998
|
+
tracerProvider: this.tracerProvider
|
|
3999
|
+
});
|
|
4000
|
+
const enqueueHeldOutboxMessage = async (delay, heldSince) => {
|
|
4001
|
+
const { outboxQueue } = this;
|
|
4002
|
+
if (outboxQueue == null) return;
|
|
4003
|
+
const heldMessage = {
|
|
4004
|
+
...message,
|
|
4005
|
+
circuitHeld: true,
|
|
4006
|
+
circuitHeldSince: heldSince.toString()
|
|
4007
|
+
};
|
|
4008
|
+
await outboxQueue.enqueue(heldMessage, {
|
|
4009
|
+
delay: clampNegativeDelay(delay),
|
|
4010
|
+
orderingKey: message.orderingKey
|
|
4011
|
+
});
|
|
4012
|
+
require_http.getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
4013
|
+
role: "outbox",
|
|
4014
|
+
queue: outboxQueue,
|
|
4015
|
+
activityType: heldMessage.activityType
|
|
4016
|
+
}, heldMessage.attempt);
|
|
4017
|
+
};
|
|
4018
|
+
const dropHeldOutboxMessage = async (circuit, remoteHost, inbox, heldSince, activity) => {
|
|
4019
|
+
await circuit.dropActivity(remoteHost, {
|
|
4020
|
+
inbox,
|
|
4021
|
+
activity,
|
|
4022
|
+
activityId: message.activityId,
|
|
4023
|
+
activityType: message.activityType,
|
|
4024
|
+
actorIds: getActorIds(),
|
|
4025
|
+
heldSince
|
|
4026
|
+
});
|
|
4027
|
+
if (this.outboxPermanentFailureHandler != null) {
|
|
4028
|
+
const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
|
|
4029
|
+
try {
|
|
4030
|
+
await this.outboxPermanentFailureHandler(ctx, {
|
|
4031
|
+
reason: "circuit-breaker-ttl",
|
|
4032
|
+
inbox,
|
|
4033
|
+
activity,
|
|
4034
|
+
error: new SendActivityError(inbox, 0, "Circuit breaker held activity expired.", ""),
|
|
4035
|
+
statusCode: 0,
|
|
4036
|
+
circuitHeldSince: heldSince,
|
|
4037
|
+
actorIds: getActorIds()
|
|
4038
|
+
});
|
|
4039
|
+
} catch (handlerError) {
|
|
4040
|
+
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
4041
|
+
...logData,
|
|
4042
|
+
error: handlerError
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
require_http.recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
4047
|
+
};
|
|
3565
4048
|
try {
|
|
4049
|
+
const inbox = new URL(message.inbox);
|
|
4050
|
+
const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
|
|
4051
|
+
const remoteHost = require_http.getRemoteHost(inbox);
|
|
4052
|
+
let decision;
|
|
4053
|
+
if (circuit != null) try {
|
|
4054
|
+
decision = await circuit.beforeSend(remoteHost, message);
|
|
4055
|
+
} catch (circuitError) {
|
|
4056
|
+
(0, _logtape_logtape.getLogger)([
|
|
4057
|
+
"fedify",
|
|
4058
|
+
"federation",
|
|
4059
|
+
"circuit"
|
|
4060
|
+
]).error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
|
|
4061
|
+
...logData,
|
|
4062
|
+
remoteHost,
|
|
4063
|
+
error: circuitError
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
if (decision != null && circuit != null) {
|
|
4067
|
+
if (decision.type === "hold") {
|
|
4068
|
+
recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
|
|
4069
|
+
await enqueueHeldOutboxMessage(decision.delay, decision.heldSince);
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
4072
|
+
if (decision.type === "drop") {
|
|
4073
|
+
const activity = await parseActivity();
|
|
4074
|
+
await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
4077
|
+
if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
|
|
4078
|
+
}
|
|
3566
4079
|
await sendActivity({
|
|
3567
4080
|
keys,
|
|
3568
4081
|
activity: message.activity,
|
|
3569
4082
|
activityId: message.activityId,
|
|
3570
4083
|
activityType: message.activityType,
|
|
3571
|
-
inbox
|
|
4084
|
+
inbox,
|
|
3572
4085
|
sharedInbox: message.sharedInbox,
|
|
3573
4086
|
headers: new Headers(message.headers),
|
|
3574
4087
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
3575
4088
|
meterProvider: this.meterProvider,
|
|
3576
4089
|
tracerProvider: this.tracerProvider
|
|
3577
4090
|
});
|
|
4091
|
+
if (circuit != null) try {
|
|
4092
|
+
const stateChange = await circuit.recordSuccess(remoteHost);
|
|
4093
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
4094
|
+
} catch (error) {
|
|
4095
|
+
(0, _logtape_logtape.getLogger)([
|
|
4096
|
+
"fedify",
|
|
4097
|
+
"federation",
|
|
4098
|
+
"circuit"
|
|
4099
|
+
]).error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
|
|
4100
|
+
...logData,
|
|
4101
|
+
remoteHost,
|
|
4102
|
+
error
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
3578
4105
|
} catch (error) {
|
|
3579
4106
|
span.setStatus({
|
|
3580
4107
|
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
@@ -3589,18 +4116,65 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3589
4116
|
return;
|
|
3590
4117
|
}
|
|
3591
4118
|
})();
|
|
4119
|
+
let retryAfterDelay;
|
|
4120
|
+
let circuitHold;
|
|
4121
|
+
let circuitDrop;
|
|
4122
|
+
let retryPolicyDelay;
|
|
4123
|
+
let policyDelayCalculated = false;
|
|
4124
|
+
const getPolicyDelay = () => {
|
|
4125
|
+
if (!policyDelayCalculated) {
|
|
4126
|
+
retryPolicyDelay = this.outboxRetryPolicy({
|
|
4127
|
+
elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
|
|
4128
|
+
attempts: message.attempt
|
|
4129
|
+
});
|
|
4130
|
+
policyDelayCalculated = true;
|
|
4131
|
+
}
|
|
4132
|
+
return retryPolicyDelay;
|
|
4133
|
+
};
|
|
4134
|
+
const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
|
|
4135
|
+
if (!isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503)) retryAfterDelay = parseRetryAfter(error.responseHeaders);
|
|
4136
|
+
if (remoteHost != null && this.outboxQueue != null && this.circuitBreaker != null) try {
|
|
4137
|
+
if (error instanceof SendActivityError) {
|
|
4138
|
+
const { statusCode } = error;
|
|
4139
|
+
const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await this.circuitBreaker.recordReachableFailure(remoteHost) : statusCode >= 500 ? await this.circuitBreaker.recordFailure(remoteHost) : void 0;
|
|
4140
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
4141
|
+
} else if (isTransportDeliveryError(error)) {
|
|
4142
|
+
const stateChange = await this.circuitBreaker.recordFailure(remoteHost);
|
|
4143
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
4144
|
+
}
|
|
4145
|
+
if (!isPermanentFailure) {
|
|
4146
|
+
const circuitDecision = await this.circuitBreaker.beforeSend(remoteHost, message);
|
|
4147
|
+
if (circuitDecision.type === "hold") circuitHold = {
|
|
4148
|
+
delay: circuitDecision.delay,
|
|
4149
|
+
heldSince: circuitDecision.heldSince,
|
|
4150
|
+
remoteHost,
|
|
4151
|
+
state: circuitDecision.state
|
|
4152
|
+
};
|
|
4153
|
+
else if (circuitDecision.type === "drop") circuitDrop = {
|
|
4154
|
+
circuit: this.circuitBreaker,
|
|
4155
|
+
remoteHost,
|
|
4156
|
+
inbox: new URL(message.inbox),
|
|
4157
|
+
heldSince: circuitDecision.heldSince
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
} catch (circuitError) {
|
|
4161
|
+
(0, _logtape_logtape.getLogger)([
|
|
4162
|
+
"fedify",
|
|
4163
|
+
"federation",
|
|
4164
|
+
"circuit"
|
|
4165
|
+
]).error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
|
|
4166
|
+
...logData,
|
|
4167
|
+
remoteHost,
|
|
4168
|
+
error: circuitError
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
3592
4171
|
span.addEvent("activitypub.delivery.failed", {
|
|
3593
4172
|
...remoteHost == null ? {} : { "activitypub.remote.host": remoteHost },
|
|
3594
4173
|
"activitypub.delivery.attempt": message.attempt,
|
|
3595
|
-
"activitypub.delivery.permanent_failure":
|
|
4174
|
+
"activitypub.delivery.permanent_failure": isPermanentFailure,
|
|
3596
4175
|
...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
|
|
3597
4176
|
});
|
|
3598
|
-
const
|
|
3599
|
-
const activity = await _fedify_vocab.Activity.fromJsonLd(message.activity, {
|
|
3600
|
-
contextLoader: this.contextLoaderFactory(loaderOptions),
|
|
3601
|
-
documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
|
|
3602
|
-
tracerProvider: this.tracerProvider
|
|
3603
|
-
});
|
|
4177
|
+
const activity = await parseActivity();
|
|
3604
4178
|
try {
|
|
3605
4179
|
await this.onOutboxError?.(error, activity);
|
|
3606
4180
|
} catch (error) {
|
|
@@ -3609,7 +4183,11 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3609
4183
|
error
|
|
3610
4184
|
});
|
|
3611
4185
|
}
|
|
3612
|
-
if (
|
|
4186
|
+
if (circuitDrop != null) {
|
|
4187
|
+
await dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
if (isPermanentFailure) {
|
|
3613
4191
|
require_http.getFederationMetrics(this.meterProvider).recordPermanentFailure(error.inbox, error.statusCode);
|
|
3614
4192
|
logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
|
|
3615
4193
|
...logData,
|
|
@@ -3619,18 +4197,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3619
4197
|
const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
|
|
3620
4198
|
try {
|
|
3621
4199
|
await this.outboxPermanentFailureHandler(ctx, {
|
|
4200
|
+
reason: "http",
|
|
3622
4201
|
inbox: new URL(message.inbox),
|
|
3623
4202
|
activity,
|
|
3624
4203
|
error,
|
|
3625
4204
|
statusCode: error.statusCode,
|
|
3626
|
-
actorIds: (
|
|
3627
|
-
try {
|
|
3628
|
-
return [new URL(id)];
|
|
3629
|
-
} catch {
|
|
3630
|
-
logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
|
|
3631
|
-
return [];
|
|
3632
|
-
}
|
|
3633
|
-
})
|
|
4205
|
+
actorIds: getActorIds()
|
|
3634
4206
|
});
|
|
3635
4207
|
} catch (handlerError) {
|
|
3636
4208
|
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
@@ -3642,17 +4214,25 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3642
4214
|
require_http.recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
3643
4215
|
return;
|
|
3644
4216
|
}
|
|
3645
|
-
if (
|
|
4217
|
+
if (circuitHold != null && getPolicyDelay() != null) {
|
|
4218
|
+
logger.error("Failed to send activity {activityId} to {inbox}; holding because the remote host circuit is open:\n{error}", {
|
|
4219
|
+
...logData,
|
|
4220
|
+
error
|
|
4221
|
+
});
|
|
4222
|
+
recordCircuitBreakerHeldSpanEvent(span, circuitHold.remoteHost, circuitHold.state);
|
|
4223
|
+
const circuit = this.circuitBreaker;
|
|
4224
|
+
await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince);
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
if (this.outboxQueue?.nativeRetrial && retryAfterDelay == null) {
|
|
3646
4228
|
logger.error("Failed to send activity {activityId} to {inbox}; backend will handle retry:\n{error}", {
|
|
3647
4229
|
...logData,
|
|
3648
4230
|
error
|
|
3649
4231
|
});
|
|
3650
4232
|
throw error;
|
|
3651
4233
|
}
|
|
3652
|
-
const
|
|
3653
|
-
|
|
3654
|
-
attempts: message.attempt
|
|
3655
|
-
});
|
|
4234
|
+
const policyDelay = getPolicyDelay();
|
|
4235
|
+
const delay = policyDelay == null ? null : retryAfterDelay ?? policyDelay;
|
|
3656
4236
|
if (delay != null) {
|
|
3657
4237
|
logger.error("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); retry...:\n{error}", {
|
|
3658
4238
|
...logData,
|
|
@@ -3664,7 +4244,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3664
4244
|
};
|
|
3665
4245
|
const { outboxQueue } = this;
|
|
3666
4246
|
if (outboxQueue != null) {
|
|
3667
|
-
await outboxQueue.enqueue(retryMessage, {
|
|
4247
|
+
await outboxQueue.enqueue(retryMessage, {
|
|
4248
|
+
delay: clampNegativeDelay(delay),
|
|
4249
|
+
orderingKey: message.orderingKey
|
|
4250
|
+
});
|
|
3668
4251
|
require_http.getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
3669
4252
|
role: "outbox",
|
|
3670
4253
|
queue: outboxQueue,
|
|
@@ -3753,7 +4336,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3753
4336
|
...message,
|
|
3754
4337
|
attempt: message.attempt + 1
|
|
3755
4338
|
};
|
|
3756
|
-
await this.inboxQueue.enqueue(retryMessage, { delay:
|
|
4339
|
+
await this.inboxQueue.enqueue(retryMessage, { delay: clampNegativeDelay(delay) });
|
|
3757
4340
|
if (activityType != null) {
|
|
3758
4341
|
require_http.getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
3759
4342
|
role: "inbox",
|
|
@@ -5506,6 +6089,12 @@ function getRequestId(request) {
|
|
|
5506
6089
|
return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
5507
6090
|
}
|
|
5508
6091
|
//#endregion
|
|
6092
|
+
Object.defineProperty(exports, "CircuitBreaker", {
|
|
6093
|
+
enumerable: true,
|
|
6094
|
+
get: function() {
|
|
6095
|
+
return CircuitBreaker;
|
|
6096
|
+
}
|
|
6097
|
+
});
|
|
5509
6098
|
Object.defineProperty(exports, "SendActivityError", {
|
|
5510
6099
|
enumerable: true,
|
|
5511
6100
|
get: function() {
|
|
@@ -5554,6 +6143,18 @@ Object.defineProperty(exports, "middleware_exports", {
|
|
|
5554
6143
|
return middleware_exports;
|
|
5555
6144
|
}
|
|
5556
6145
|
});
|
|
6146
|
+
Object.defineProperty(exports, "normalizeCircuitBreakerOptions", {
|
|
6147
|
+
enumerable: true,
|
|
6148
|
+
get: function() {
|
|
6149
|
+
return normalizeCircuitBreakerOptions;
|
|
6150
|
+
}
|
|
6151
|
+
});
|
|
6152
|
+
Object.defineProperty(exports, "parseCircuitBreakerKvState", {
|
|
6153
|
+
enumerable: true,
|
|
6154
|
+
get: function() {
|
|
6155
|
+
return parseCircuitBreakerKvState;
|
|
6156
|
+
}
|
|
6157
|
+
});
|
|
5557
6158
|
Object.defineProperty(exports, "respondWithObject", {
|
|
5558
6159
|
enumerable: true,
|
|
5559
6160
|
get: function() {
|