-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathgraphnode-helpers.ts
More file actions
101 lines (89 loc) · 4.12 KB
/
graphnode-helpers.ts
File metadata and controls
101 lines (89 loc) · 4.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import pRetry from "p-retry";
import type { LabelHash, LiteralLabel } from "@ensnode/ensnode-sdk";
import { type EnsRainbow, ErrorCode, isHealError } from "@ensnode/ensrainbow-sdk";
import { ensRainbowClient } from "@/lib/ensrainbow/singleton";
import { logger } from "@/lib/logger";
/**
* Attempt to heal a labelHash to its original label.
* It mirrors the `ens.nameByHash` function implemented in GraphNode:
* https://github.com/graphprotocol/graph-node/blob/3c448de/runtime/test/wasm_test/api_version_0_0_4/ens_name_by_hash.ts#L9-L11
*
* ## Transient vs. non-transient errors
*
* ENSIndexer calls this function for every indexed ENS event that requires label healing. A single
* transient failure (e.g. a momentary network blip or a brief ENSRainbow server hiccup) would
* otherwise crash the ENSIndexer process, forcing a full restart and re-index from the last
* checkpoint. To avoid this disproportionate impact, transient failures are retried with
* exponential backoff (up to 3 retries, with backoff delays between 1 and 30 seconds between attempts) before the error is thrown:
* - Network/fetch failure: heal() throws because the ENSRainbow service was unreachable.
* - HealServerError (errorCode 500): ENSRainbow returned a transient server-side error.
*
* Non-transient outcomes are thrown immediately without retry, because retrying would not change
* the result:
* - HealSuccess: the label was healed successfully; returned.
* - HealNotFoundError (errorCode 404): no label is known for this labelHash; null returned.
* - HealBadRequestError (errorCode 400): the labelHash is malformed; retrying would not help.
*
* ## Non-recoverable throws
*
* Any throw from this function is not recoverable or has exceeded the max retries. It propagates to the calling indexing handler
* (Registry, Registrar, ThreeDNSToken, label-db-helpers) which does not catch it, causing the
* ENSIndexer process to terminate.
*
* @returns the original label if found, or null if not found for the labelHash.
* @throws if ENSRainbow returns a non-retryable error response (e.g. HealBadRequestError / 400),
* or if a transient error (network failure, HealServerError / 500) persists after all retries
* are exhausted.
*/
export async function labelByLabelHash(labelHash: LabelHash): Promise<LiteralLabel | null> {
// Reset at the start of each attempt so that after p-retry exhaustion we can distinguish
// "last failure was HealServerError" (set) from "last failure was a network throw" (undefined).
let lastServerError: EnsRainbow.HealServerError | undefined;
let response: EnsRainbow.HealResponse;
try {
response = await pRetry(
async () => {
lastServerError = undefined;
const result = await ensRainbowClient.heal(labelHash);
if (isHealError(result) && result.errorCode === ErrorCode.ServerError) {
lastServerError = result;
throw new Error(result.error);
}
return result;
},
{
retries: 3,
minTimeout: 1_000,
maxTimeout: 30_000,
onFailedAttempt({ attemptNumber, retriesLeft }) {
logger.warn({
msg: `ENSRainbow "heal" request failed`,
attempt: attemptNumber,
retriesLeft,
});
},
},
);
} catch (error) {
if (lastServerError) {
// Not recoverable; causes the ENSIndexer process to terminate.
throw new Error(
`Error healing labelHash: "${labelHash}". Error (${lastServerError.errorCode}): ${lastServerError.error}.`,
{ cause: error },
);
}
// Not recoverable; causes the ENSIndexer process to terminate.
if (error instanceof Error) {
error.message = `ENSRainbow Heal Request Failed: ENSRainbow unavailable at '${ensRainbowClient.getOptions().endpointUrl}'.`;
}
throw error;
}
if (isHealError(response)) {
if (response.errorCode === ErrorCode.NotFound) return null;
// Not recoverable; causes the ENSIndexer process to terminate.
throw new Error(
`Error healing labelHash: "${labelHash}". Error (${response.errorCode}): ${response.error}.`,
);
}
return response.label as LiteralLabel;
}