This repository was archived by the owner on Jan 6, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
Add alternative provider retrieval check #132
Closed
Closed
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
aedec80
Add network wide retrieval check
pyropy 233cc1f
Use status code instead of boolean retrieval flag
pyropy 83e7f31
Simplify name for network wide measurements
pyropy afe30dd
Refactor code for picking random provider
pyropy 23ee203
Add network retrieval protocol field
pyropy 4bc1076
Add basic test for testing network retrieval
pyropy 63424ff
Refactor function for picking random providers
pyropy 8a94f4e
Only return providers in case of no valid advert
pyropy c4350b6
Convert network stats to object inside stats obj
pyropy edfdef1
Format testNetworkRetrieval func
pyropy dbf0fd7
Refactor queryTheIndex function
pyropy d33f276
Handle case when no random provider is picked
pyropy 97bee91
Test function for picking random providers
pyropy 4b6d0bc
Rename network retrieval to alternative provider check
pyropy 97fcc28
Update logging to reflect metric name change
pyropy 5121a49
Update logging to reflect metric name change
pyropy 4065784
Rename providers field to alternativeProviders
pyropy 74f06e9
Rename testNetworkRetrieval to checkRetrievalFromAlternativeProvider
pyropy ea8cce4
Return retrieval stats from checkRetrievalFromAlternativeProvider
pyropy f9afe34
Update lib/spark.js
pyropy 9959b50
Update lib/spark.js
pyropy a2da050
Rename functions to match new metric name
pyropy 9759d80
Merge branch 'add/network-wide-retrieval-check' of github.com:filecoi…
pyropy 820e8a3
Pick alternative provider using supplied randomness
pyropy 5b13287
Replace custom rng implementation with Prando
pyropy 3c14f84
Fix typos
pyropy fe0f1f5
Merge remote-tracking branch 'origin/main' into add/network-wide-retr…
pyropy ad8a8e8
Lint fix
pyropy 31019d0
Add ID to Provider
pyropy 3710910
Filter out bitswap providers before picking random provider
pyropy c61a196
Update lib/ipni-client.js
pyropy d1f62fa
Update lib/spark.js
pyropy 05bd1c2
Update lib/spark.js
pyropy 3451eff
Rename random to alternative provider
pyropy 8b8db36
Merge branch 'add/network-wide-retrieval-check' of github.com:Checker…
pyropy 59d3d22
Simplify pseudo-rng
pyropy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| /* global Zinnia */ | ||
|
|
||
| /** @import {Provider} from './ipni-client.js' */ | ||
| import { ActivityState } from './activity-state.js' | ||
| import { | ||
| SPARK_VERSION, | ||
|
|
@@ -44,18 +45,20 @@ export default class Spark { | |
|
|
||
| async getRetrieval() { | ||
| const retrieval = await this.#tasker.next() | ||
| if (retrieval) { | ||
| console.log({ retrieval }) | ||
| const { retrievalTask } = retrieval | ||
| if (retrievalTask) { | ||
| console.log({ retrievalTask }) | ||
| } | ||
|
|
||
| return retrieval | ||
| } | ||
|
|
||
| async executeRetrievalCheck(retrieval, stats) { | ||
| async executeRetrievalCheck({ retrievalTask, stats, randomness }) { | ||
| console.log( | ||
| `Calling Filecoin JSON-RPC to get PeerId of miner ${retrieval.minerId}`, | ||
| `Calling Filecoin JSON-RPC to get PeerId of miner ${retrievalTask.minerId}`, | ||
| ) | ||
| try { | ||
| const peerId = await this.#getIndexProviderPeerId(retrieval.minerId) | ||
| const peerId = await this.#getIndexProviderPeerId(retrievalTask.minerId) | ||
| console.log(`Found peer id: ${peerId}`) | ||
| stats.providerId = peerId | ||
| } catch (err) { | ||
|
|
@@ -78,29 +81,45 @@ export default class Spark { | |
| } | ||
|
|
||
| console.log( | ||
| `Querying IPNI to find retrieval providers for ${retrieval.cid}`, | ||
| ) | ||
| const { indexerResult, provider } = await queryTheIndex( | ||
| retrieval.cid, | ||
| stats.providerId, | ||
| `Querying IPNI to find retrieval providers for ${retrievalTask.cid}`, | ||
| ) | ||
| const { indexerResult, provider, alternativeProviders } = | ||
| await queryTheIndex(retrievalTask.cid, stats.providerId) | ||
| stats.indexerResult = indexerResult | ||
|
|
||
| const providerFound = | ||
| indexerResult === 'OK' || indexerResult === 'HTTP_NOT_ADVERTISED' | ||
| if (!providerFound) return | ||
| const noValidAdvertisement = indexerResult === 'NO_VALID_ADVERTISEMENT' | ||
|
|
||
| // In case index lookup failed we will not perform any retrieval | ||
| if (!providerFound && !noValidAdvertisement) return | ||
|
|
||
| // In case we fail to find a valid advertisement for the provider | ||
| // we will try to perform network wide retrieval from other providers | ||
| if (noValidAdvertisement) { | ||
| console.log( | ||
| 'No valid advertisement found. Trying to retrieve from an alternative provider...', | ||
| ) | ||
| stats.alternativeProviderCheck = | ||
| await this.checkRetrievalFromAlternativeProvider({ | ||
| alternativeProviders, | ||
| randomness, | ||
| cid: retrievalTask.cid, | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| stats.protocol = provider.protocol | ||
| stats.providerAddress = provider.address | ||
|
|
||
| await this.fetchCAR( | ||
| provider.protocol, | ||
| provider.address, | ||
| retrieval.cid, | ||
| retrievalTask.cid, | ||
| stats, | ||
| ) | ||
| if (stats.protocol === 'http') { | ||
| await this.testHeadRequest(provider.address, retrieval.cid, stats) | ||
| await this.testHeadRequest(provider.address, retrievalTask.cid, stats) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -220,6 +239,51 @@ export default class Spark { | |
| } | ||
| } | ||
|
|
||
| async checkRetrievalFromAlternativeProvider({ | ||
| alternativeProviders, | ||
| randomness, | ||
| cid, | ||
| }) { | ||
| if (!alternativeProviders.length) { | ||
| console.info('No alternative providers found for this CID.') | ||
| return | ||
| } | ||
|
|
||
| const validAlternativeProviders = alternativeProviders.filter( | ||
| (p) => p.protocol !== 'bitswap', | ||
| ) | ||
|
|
||
| if (!validAlternativeProviders.length) { | ||
| console.warn( | ||
| 'No providers serving the content via HTTP or Graphsync found. Skipping network-wide retrieval check.', | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| const alternativeProvider = pickRandomProvider( | ||
| validAlternativeProviders, | ||
| randomness, | ||
| ) | ||
| if (!alternativeProvider) { | ||
| console.warn( | ||
| 'Failed to pick a alternative provider. Skipping network-wide retrieval check.', | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| const alternativeProviderRetrievalStats = newAlternativeProviderCheckStats() | ||
| alternativeProviderRetrievalStats.providerId = alternativeProvider.peerId | ||
|
|
||
| await this.fetchCAR( | ||
| alternativeProvider.protocol, | ||
| alternativeProvider.address, | ||
| cid, | ||
| alternativeProviderRetrievalStats, | ||
| ) | ||
|
|
||
| return alternativeProviderRetrievalStats | ||
| } | ||
|
|
||
| async submitMeasurement(task, stats) { | ||
| console.log('Submitting measurement...') | ||
| const payload = { | ||
|
|
@@ -246,8 +310,8 @@ export default class Spark { | |
| } | ||
|
|
||
| async nextRetrieval() { | ||
| const retrieval = await this.getRetrieval() | ||
| if (!retrieval) { | ||
| const { retrievalTask, randomness } = await this.getRetrieval() | ||
| if (!retrievalTask) { | ||
| console.log( | ||
| 'Completed all tasks for the current round. Waiting for the next round to start.', | ||
| ) | ||
|
|
@@ -256,9 +320,11 @@ export default class Spark { | |
|
|
||
| const stats = newStats() | ||
|
|
||
| await this.executeRetrievalCheck(retrieval, stats) | ||
| await this.executeRetrievalCheck({ retrievalTask, randomness, stats }) | ||
|
|
||
| const measurementId = await this.submitMeasurement(retrieval, { ...stats }) | ||
| const measurementId = await this.submitMeasurement(retrievalTask, { | ||
| ...stats, | ||
| }) | ||
| Zinnia.jobCompleted() | ||
| return measurementId | ||
| } | ||
|
|
@@ -335,6 +401,17 @@ export function newStats() { | |
| carChecksum: null, | ||
| statusCode: null, | ||
| headStatusCode: null, | ||
| alternativeProviderCheck: null, | ||
| } | ||
| } | ||
|
|
||
| function newAlternativeProviderCheckStats() { | ||
| return { | ||
| statusCode: null, | ||
| timeout: false, | ||
| endAt: null, | ||
| carTooLarge: false, | ||
| providerId: null, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -418,3 +495,64 @@ function mapErrorToStatusCode(err) { | |
| // Fallback code for unknown errors | ||
| return 600 | ||
| } | ||
|
|
||
| /** | ||
| * Picks a random provider based on their priority and supplied randomness. | ||
| * | ||
| * Providers are prioritized in the following order: | ||
| * | ||
| * 1. HTTP Providers with Piece Info advertised in their ContextID. | ||
| * 2. Graphsync Providers with Piece Info advertised in their ContextID. | ||
| * 3. HTTP Providers. | ||
| * 4. Graphsync Providers. | ||
| * | ||
| * @param {Provider[]} providers | ||
| * @param {string} randomness | ||
| * @returns {Provider | undefined} | ||
| */ | ||
| export function pickRandomProvider(providers, randomness) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const filterByProtocol = (items, protocol) => | ||
| items.filter((provider) => provider.protocol === protocol) | ||
|
|
||
| const pickRandomItem = (items) => { | ||
| if (!items.length) return undefined | ||
| const randomValue = BigInt('0x' + randomness) | ||
| const ix = Number(randomValue % BigInt(items.length)) | ||
| return items[ix] | ||
| } | ||
|
|
||
| const providersWithPieceInfoContextID = providers.filter((p) => | ||
| p.contextId.startsWith('ghsA'), | ||
| ) | ||
|
|
||
| // Priority 1: HTTP providers with ContextID containing PieceCID | ||
| const httpProvidersWithPieceInfoContextID = filterByProtocol( | ||
| providersWithPieceInfoContextID, | ||
| 'http', | ||
| ) | ||
| if (httpProvidersWithPieceInfoContextID.length) { | ||
| return pickRandomItem(httpProvidersWithPieceInfoContextID, randomness) | ||
| } | ||
|
|
||
| // Priority 2: Graphsync providers with ContextID containing PieceCID | ||
| const graphsyncProvidersWithPieceInfoContextID = filterByProtocol( | ||
| providersWithPieceInfoContextID, | ||
| 'graphsync', | ||
| ) | ||
| if (graphsyncProvidersWithPieceInfoContextID.length) { | ||
| return pickRandomItem(graphsyncProvidersWithPieceInfoContextID, randomness) | ||
| } | ||
|
|
||
| // Priority 3: HTTP providers | ||
| const httpProviders = filterByProtocol(providers, 'http') | ||
| if (httpProviders.length) return pickRandomItem(httpProviders, randomness) | ||
|
|
||
| // Priority 4: Graphsync providers | ||
| const graphsyncProviders = filterByProtocol(providers, 'graphsync') | ||
| if (graphsyncProviders.length) { | ||
| return pickRandomItem(graphsyncProviders, randomness) | ||
| } | ||
|
|
||
| // No valid providers found | ||
| return undefined | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ export class Tasker { | |
| #remainingRoundTasks | ||
| #fetch | ||
| #activity | ||
| #randomness | ||
|
|
||
| /** | ||
| * @param {object} args | ||
|
|
@@ -37,10 +38,11 @@ export class Tasker { | |
| this.#remainingRoundTasks = [] | ||
| } | ||
|
|
||
| /** @returns {Task | undefined} */ | ||
| /** @returns {Promise<{ retrievalTask?: RetrievalTask; randomness: string }>} */ | ||
| async next() { | ||
| await this.#updateCurrentRound() | ||
| return this.#remainingRoundTasks.pop() | ||
| const retrievalTask = this.#remainingRoundTasks.pop() | ||
| return { retrievalTask, randomness: this.#randomness } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We somehow need to export the round randomness so I have opted for returning object with Maybe adding the |
||
| } | ||
|
|
||
| async #updateCurrentRound() { | ||
|
|
@@ -76,13 +78,13 @@ export class Tasker { | |
| console.log(' %s retrieval tasks', retrievalTasks.length) | ||
| this.maxTasksPerRound = maxTasksPerNode | ||
|
|
||
| const randomness = await getRandomnessForSparkRound(round.startEpoch) | ||
| console.log(' randomness: %s', randomness) | ||
| this.#randomness = await getRandomnessForSparkRound(round.startEpoch) | ||
| console.log(' randomness: %s', this.#randomness) | ||
|
|
||
| this.#remainingRoundTasks = await pickTasksForNode({ | ||
| tasks: retrievalTasks, | ||
| maxTasksPerRound: this.maxTasksPerRound, | ||
| randomness, | ||
| randomness: this.#randomness, | ||
| stationId: Zinnia.stationId, | ||
| }) | ||
|
|
||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this also have
byteLength,carChecksumandheadStatusCode?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or are we consciously omitting them? If so, could you please add a code comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if we're supposed to have them; I think it wouldn't be a big deal to add those fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't have them it means we could have a successful retrieval (using the alternative provider method) but not know the byte length, car checksum and head status code. @bajtos wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on what do we want to use the alternative retrieval check measurement for.
As I understand it, we want to calculate network-wide RSR for retrievals that include alternative providers so that we can show this RSR on the leaderboard. I don't see how we need
byteLength,carChecksumorheadStatusCodefor that.I'd say YAGNI, exclude these fields for now, and wait until we need them.