Skip to content

Commit b83d774

Browse files
committed
refactor: introduce pRetry and error codes
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
1 parent 737f284 commit b83d774

File tree

5 files changed

+140
-71
lines changed

5 files changed

+140
-71
lines changed

indexer/lib/advertisement-walker.js

Lines changed: 82 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as multihash from 'multiformats/hashes/digest'
77
import assert from 'node:assert'
88
import timers from 'node:timers/promises'
99
import { assertOkResponse } from './http-assertions.js'
10+
import pRetry from 'p-retry'
1011

1112
/** @import { ProviderInfo, WalkerState } from './typings.js' */
1213
/** @import { RedisRepository as Repository } from '@filecoin-station/spark-piece-indexer-repository' */
@@ -151,7 +152,7 @@ export async function processNextAdvertisement ({
151152
assert(state.tail)
152153

153154
try {
154-
const { previousAdvertisementCid, entriesFetchError, ...entry } = await fetchAdvertisedPayload(
155+
const { previousAdvertisementCid, entry, error } = await fetchAdvertisedPayload(
155156
providerInfo.providerAddress,
156157
state.tail,
157158
{ fetchTimeout }
@@ -169,60 +170,68 @@ export async function processNextAdvertisement ({
169170
state.status = `Walking the advertisements from ${state.head}, next step: ${state.tail}`
170171
}
171172

172-
if (entriesFetchError) {
173+
if (error === 'CANNOT_FETCH_ENTRIES') {
173174
state.entriesNotRetrievable = (state.entriesNotRetrievable ?? 0) + 1
175+
} else if (error === 'CANNOT_DETERMINE_PIECE_CID') {
176+
state.adsMissingPieceCID = (state.adsMissingPieceCID ?? 0) + 1
174177
}
175-
const indexEntry = (entry.pieceCid && entry.payloadCid) ? entry : undefined
178+
179+
const indexEntry = entry?.pieceCid ? entry : undefined
176180
const finished = !state.tail
177181
return {
178182
newState: state,
179183
indexEntry,
180184
finished
181185
}
182186
} catch (err) {
183-
let reason
184-
if (err instanceof Error) {
185-
const url = 'url' in err ? err.url : providerInfo.providerAddress
186-
if ('serverMessage' in err && err.serverMessage) {
187-
reason = err.serverMessage
188-
if ('statusCode' in err && err.statusCode) {
189-
reason = `${err.statusCode} ${reason}`
190-
}
191-
} else if ('statusCode' in err && err.statusCode) {
192-
reason = err.statusCode
193-
} else if (err.name === 'TimeoutError') {
194-
reason = 'operation timed out'
195-
} else if (
196-
err.name === 'TypeError' &&
197-
err.message === 'fetch failed' &&
198-
err.cause &&
199-
err.cause instanceof Error
200-
) {
201-
reason = err.cause.message
202-
}
203-
204-
reason = `HTTP request to ${url} failed: ${reason}`
205-
}
187+
const errorDescription = describeFetchError(err, providerInfo.providerAddress)
206188

207189
debug(
208190
'Cannot process provider %s (%s) advertisement %s: %s',
209191
providerId,
210192
providerInfo.providerAddress,
211193
state.tail,
212-
reason ?? err
194+
errorDescription ?? err
213195
)
214-
state.status = `Error processing ${state.tail}: ${reason ?? 'internal error'}`
196+
state.status = `Error processing ${state.tail}: ${errorDescription ?? 'internal error'}`
215197
return {
216198
newState: state,
217199
failed: true
218200
}
219201
}
220202
}
221203

222-
/** @typedef {{
223-
pieceCid: string | undefined;
224-
payloadCid: string;
225-
}} AdvertisedPayload */
204+
/**
205+
* @param {unknown} err
206+
* @param {string} providerAddress
207+
*/
208+
function describeFetchError (err, providerAddress) {
209+
if (!(err instanceof Error)) return undefined
210+
211+
let reason
212+
if ('serverMessage' in err && err.serverMessage) {
213+
reason = err.serverMessage
214+
if ('statusCode' in err && err.statusCode) {
215+
reason = `${err.statusCode} ${reason}`
216+
}
217+
} else if ('statusCode' in err && err.statusCode) {
218+
reason = err.statusCode
219+
} else if (err.name === 'TimeoutError') {
220+
reason = 'operation timed out'
221+
} else if (
222+
err.name === 'TypeError' &&
223+
err.message === 'fetch failed' &&
224+
err.cause &&
225+
err.cause instanceof Error
226+
) {
227+
reason = err.cause.message
228+
}
229+
if (!reason) return undefined
230+
231+
const url = 'url' in err ? err.url : providerAddress
232+
reason = `HTTP request to ${url} failed: ${reason}`
233+
return reason
234+
}
226235

227236
/**
228237
* @param {string} providerAddress
@@ -262,41 +271,51 @@ export async function fetchAdvertisedPayload (providerAddress, advertisementCid,
262271

263272
const meta = parseMetadata(advertisement.Metadata['/'].bytes)
264273
const pieceCid = meta.deal?.PieceCID.toString()
265-
266-
try {
267-
const entriesChunk =
268-
/** @type {{
269-
Entries: { '/' : { bytes: string } }[]
270-
}} */(
271-
await fetchCid(providerAddress, entriesCid, { fetchTimeout })
272-
)
273-
debug('entriesChunk %s %j', entriesCid, entriesChunk.Entries.slice(0, 5))
274-
const entryHash = entriesChunk.Entries[0]['/'].bytes
275-
const payloadCid = CID.create(1, 0x55 /* raw */, multihash.decode(Buffer.from(entryHash, 'base64'))).toString()
276-
274+
if (!pieceCid) {
275+
debug('advertisement %s has no PieceCID in metadata: %j', advertisementCid, meta.deal)
277276
return {
278-
previousAdvertisementCid,
279-
pieceCid,
280-
payloadCid
277+
error: /** @type {const} */('CANNOT_DETERMINE_PIECE_CID'),
278+
previousAdvertisementCid
281279
}
282-
} catch (err) {
283-
if (err && typeof err === 'object' && 'statusCode' in err && err.statusCode === 404) {
284-
// The index provider cannot find the advertised entries. We cannot do much about that,
285-
// it's unlikely that further request will succeed. Let's skip this advertisement.
286-
debug(
287-
'Cannot fetch ad %s entries %s: %s %s',
288-
advertisementCid,
289-
entriesCid,
290-
err.statusCode,
291-
/** @type {any} */(err).serverMessage ?? '<not found>'
292-
)
293-
return {
294-
entriesFetchError: true,
295-
previousAdvertisementCid,
296-
pieceCid
280+
}
281+
282+
let entriesChunk
283+
try {
284+
entriesChunk = await pRetry(
285+
async () =>
286+
/** @type {{
287+
Entries: { '/' : { bytes: string } }[]
288+
}} */(
289+
await fetchCid(providerAddress, entriesCid, { fetchTimeout })
290+
),
291+
{
292+
shouldRetry: (err) =>
293+
err && 'statusCode' in err && typeof err.statusCode === 'number' && err.statusCode >= 500
297294
}
295+
)
296+
} catch (err) {
297+
// We are not able to fetch the advertised entries. Skip this advertisement so that we can
298+
// continue the ingestion of other advertisements.
299+
const errorDescription = describeFetchError(err, providerAddress)
300+
console.warn(
301+
'Cannot fetch ad %s entries %s: %s',
302+
advertisementCid,
303+
entriesCid,
304+
errorDescription ?? err
305+
)
306+
return {
307+
error: /** @type {const} */('CANNOT_FETCH_ENTRIES'),
308+
previousAdvertisementCid
298309
}
299-
throw err
310+
}
311+
312+
debug('entriesChunk %s %j', entriesCid, entriesChunk.Entries.slice(0, 5))
313+
const entryHash = entriesChunk.Entries[0]['/'].bytes
314+
const payloadCid = CID.create(1, 0x55 /* raw */, multihash.decode(Buffer.from(entryHash, 'base64'))).toString()
315+
316+
return {
317+
previousAdvertisementCid,
318+
entry: { pieceCid, payloadCid }
300319
}
301320
}
302321

indexer/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@sentry/node": "^8.31.0",
1717
"debug": "^4.3.7",
1818
"ioredis": "^5.4.1",
19-
"multiformats": "^13.3.0"
19+
"multiformats": "^13.3.0",
20+
"p-retry": "^6.2.0"
2021
}
2122
}

indexer/test/advertisement-walker.test.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ describe('processNextAdvertisement', () => {
173173
head: undefined, // we finished the walk, there is no head
174174
tail: undefined, // we finished the walk, there is no next step
175175
lastHead: FRISBII_AD_CID, // lastHead was updated to head of the walk we finished
176+
adsMissingPieceCID: 1,
176177
status: `All advertisements from ${newState?.lastHead} to the end of the chain were processed.`
177178
}))
178179

@@ -199,6 +200,7 @@ describe('processNextAdvertisement', () => {
199200
head: undefined, // we finished the walk, there is no head
200201
tail: undefined, // we finished the walk, there is no next step
201202
lastHead: FRISBII_AD_CID, // lastHead was updated to head of the walk we finished
203+
adsMissingPieceCID: 1,
202204
status: `All advertisements from ${newState?.lastHead} to the end of the chain were processed.`
203205
}))
204206

@@ -315,7 +317,7 @@ describe('processNextAdvertisement', () => {
315317
finished: true,
316318
indexEntry: undefined,
317319
newState: {
318-
entriesNotRetrievable: 1,
320+
adsMissingPieceCID: 1,
319321
head: undefined,
320322
tail: undefined,
321323
lastHead: FRISBII_AD_CID,
@@ -331,17 +333,18 @@ describe('fetchAdvertisedPayload', () => {
331333
it('returns previousAdvertisementCid, pieceCid and payloadCid for Graphsync retrievals', async () => {
332334
const result = await fetchAdvertisedPayload(providerAddress, knownAdvertisement.adCid)
333335
assert.deepStrictEqual(result, /** @type {AdvertisedPayload} */({
334-
payloadCid: knownAdvertisement.payloadCid,
335-
pieceCid: knownAdvertisement.pieceCid,
336+
entry: {
337+
payloadCid: knownAdvertisement.payloadCid,
338+
pieceCid: knownAdvertisement.pieceCid
339+
},
336340
previousAdvertisementCid: knownAdvertisement.previousAdCid
337341
}))
338342
})
339343

340-
it('returns undefined pieceCid for HTTP retrievals', async () => {
344+
it('returns CANNOT_DETERMINE_PIECE_CID error for HTTP retrievals', async () => {
341345
const result = await fetchAdvertisedPayload(FRISBII_ADDRESS, FRISBII_AD_CID)
342346
assert.deepStrictEqual(result, /** @type {AdvertisedPayload} */({
343-
payloadCid: 'bafkreih5zasorm4tlfga4ztwvm2dlnw6jxwwuvgnokyt3mjamfn3svvpyy',
344-
pieceCid: undefined,
347+
error: 'CANNOT_DETERMINE_PIECE_CID',
345348
// Our Frisbii instance announced only one advertisement
346349
// That's unrelated to HTTP vs Graphsync retrievals
347350
previousAdvertisementCid: undefined
@@ -452,13 +455,17 @@ describe('data schema for REST API', () => {
452455
// Is it a problem if our observability API does not tell the provider address?
453456
ingestionStatus: walkerState.status,
454457
lastHeadWalkedFrom: walkerState.lastHead ?? walkerState.head,
458+
adsMissingPieceCID: walkerState.adsMissingPieceCID ?? 0,
459+
entriesNotRetrievable: walkerState.entriesNotRetrievable ?? 0,
455460
piecesIndexed: await repository.countPiecesIndexed(providerId)
456461
}
457462

458463
assert.deepStrictEqual(response, {
459464
providerId,
460465
ingestionStatus: `Walking the advertisements from ${knownAdvertisement.adCid}, next step: ${knownAdvertisement.previousAdCid}`,
461466
lastHeadWalkedFrom: knownAdvertisement.adCid,
467+
adsMissingPieceCID: 0,
468+
entriesNotRetrievable: 0,
462469
piecesIndexed: 1
463470
})
464471
})

package-lock.json

Lines changed: 42 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

repository/lib/typings.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface WalkerState {
3131
lastHead?: string;
3232
status: string;
3333
entriesNotRetrievable?: number;
34+
adsMissingPieceCID?: number;
3435
}
3536

3637
export type ProviderToWalkerStateMap = Map<string, WalkerState>

0 commit comments

Comments
 (0)