Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/backend/src/ipni/ipni-verification.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,37 @@ describe("IpniVerificationService", () => {
expect(result.rootCIDVerified).toBe(false);
});

it("logs ipni_verification_timed_out even when external signal also aborted in the same tick", async () => {
const service = new IpniVerificationService();
const loggerError = vi.spyOn((service as unknown as { logger: { error: (m: object) => void } }).logger, "error");
const abortController = new AbortController();
waitForIpniProviderResultsMock.mockImplementation(
async (_cid: CID, options: { signal?: AbortSignal } | undefined) =>
await new Promise<boolean>((_resolve, reject) => {
options?.signal?.addEventListener(
"abort",
() => {
// Race: outer signal aborts before the catch handler runs.
abortController.abort(new Error("outer aborted in same tick"));
reject(new Error("inner aborted"));
},
{ once: true },
);
}),
);

const result = await service.verify({
rootCid,
storageProvider: buildStorageProvider(),
timeoutMs: 20,
pollIntervalMs: 2_000,
signal: abortController.signal,
});

expect(result.rootCIDVerified).toBe(false);
expect(loggerError).toHaveBeenCalledWith(expect.objectContaining({ event: "ipni_verification_timed_out" }));
});

it("is capped by the external deal signal", async () => {
const service = new IpniVerificationService();
const abortController = new AbortController();
Expand Down
11 changes: 7 additions & 4 deletions apps/backend/src/ipni/ipni-verification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ export class IpniVerificationService {
expectedProviders,
signal: verificationSignal,
}).catch((error) => {
if (signal?.aborted) {
signal.throwIfAborted();
}
if (verificationSignal.aborted) {
// Inner-timeout check runs first so the `ipni_verification_timed_out` event
// fires even when the outer job signal aborts in the same tick. Otherwise
// the inner-timeout log path is unreachable whenever outer < inner.
if (timeoutSignal.aborted) {
failureReason = `IPNI verification timed out after ${timeoutMs}ms`;
this.logger.error({
event: "ipni_verification_timed_out",
Expand All @@ -89,6 +89,9 @@ export class IpniVerificationService {
});
return false;
}
if (signal?.aborted) {
signal.throwIfAborted();
}
const errorMessage = error instanceof Error ? error.message : String(error);
failureReason = errorMessage;
this.logger.error({
Expand Down
19 changes: 19 additions & 0 deletions apps/backend/src/retrieval/retrieval.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,25 @@ describe("RetrievalService parallel IPNI + transport", () => {
expect(mockDiscoverabilityMetrics.recordStatus).toHaveBeenCalledWith(labels, "failure.timedout");
});

it("logs retrieval_ipni_verification_timed_out when outer signal aborts and IPNI verify throws", async () => {
service = await createService();
setupCommonMocks();
const loggerWarn = vi.spyOn((service as unknown as { logger: { warn: (m: object) => void } }).logger, "warn");
const abortController = new AbortController();
mockIpniVerificationService.verify.mockImplementation(async () => {
abortController.abort(new Error("outer timeout"));
throw new Error("ipni aborted");
});
mockRetrievalAddonsService.testAllRetrievalMethods.mockResolvedValue(successfulTransport);

await service.performAllRetrievals(buildDealWithIpni(), abortController.signal);

expect(loggerWarn).toHaveBeenCalledWith(
expect.objectContaining({ event: "retrieval_ipni_verification_timed_out" }),
);
expect(mockRetrievalMetrics.recordStatus).toHaveBeenCalledWith(labels, "failure.timedout");
});

it("runs IPNI and transport concurrently", async () => {
service = await createService();
setupCommonMocks();
Expand Down
10 changes: 10 additions & 0 deletions apps/backend/src/retrieval/retrieval.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,16 @@ export class RetrievalService {
} catch (error) {
if (signal?.aborted) {
const failureStatus = "failure.timedout";
this.logger.warn({
event: "retrieval_ipni_verification_timed_out",
message: "Retrieval IPNI verification aborted by outer job timeout",
dealId,
providerId: provider.providerId,
providerName: provider.name,
providerAddress: provider.address,
ipfsRootCID: ipniContext.rootCid.toString(),
error: toStructuredError(error),
});
this.discoverabilityMetrics.recordStatus(providerLabels, failureStatus);
return { ok: false, failureStatus };
}
Expand Down