@filoz/repair-cli 1.0.0 → 1.1.1
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/package.json +2 -2
- package/dist/src/commands/datasets.d.ts +2 -2
- package/dist/src/commands/datasets.js +9 -2
- package/dist/src/commands/datasets.js.map +1 -1
- package/dist/src/commands/providers.d.ts +2 -2
- package/dist/src/commands/repair.d.ts +2 -2
- package/dist/src/commands/repair.js +5 -1
- package/dist/src/commands/repair.js.map +1 -1
- package/dist/src/commands/replicate.d.ts +2 -2
- package/dist/src/commands/replicate.js +5 -1
- package/dist/src/commands/replicate.js.map +1 -1
- package/dist/src/commands/second-copy.d.ts +2 -2
- package/dist/src/commands/second-copy.js +5 -1
- package/dist/src/commands/second-copy.js.map +1 -1
- package/dist/src/commands/session-key.d.ts +2 -2
- package/dist/src/commands/wallet.d.ts +2 -2
- package/dist/src/middleware.d.ts +5 -5
- package/dist/src/middleware.d.ts.map +1 -1
- package/dist/src/middleware.js.map +1 -1
- package/dist/src/pipeline/add-pieces.d.ts +1 -1
- package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
- package/dist/src/pipeline/add-pieces.js +57 -43
- package/dist/src/pipeline/add-pieces.js.map +1 -1
- package/dist/src/types.d.ts +2 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils.d.ts +38 -27
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +46 -0
- package/dist/src/utils.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +9 -9
- package/src/commands/datasets.ts +9 -2
- package/src/commands/repair.ts +5 -1
- package/src/commands/replicate.ts +5 -1
- package/src/commands/second-copy.ts +5 -1
- package/src/middleware.ts +3 -3
- package/src/pipeline/add-pieces.ts +92 -46
- package/src/types.ts +2 -2
- package/src/utils.ts +69 -3
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { taskLog } from '@clack/prompts'
|
|
2
|
-
import * as Piece from '@filoz/synapse-core/piece'
|
|
3
2
|
import * as SP from '@filoz/synapse-core/sp'
|
|
4
3
|
import { and, asc, eq, gt, inArray } from 'drizzle-orm'
|
|
5
4
|
import PQueue from 'p-queue'
|
|
@@ -10,14 +9,35 @@ import { repairUpdate } from '../db/repair-update.ts'
|
|
|
10
9
|
import { upsertOperations } from '../db/upsert-operations.ts'
|
|
11
10
|
import type { OperationSelect, RepairSelect } from '../local-schema.ts'
|
|
12
11
|
import type { IndexerApi, LocalDatabase, WalletClient } from '../types.ts'
|
|
13
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
completeConfirmedOperations,
|
|
14
|
+
excludeOperationsByCid,
|
|
15
|
+
fitAddPiecesBatch,
|
|
16
|
+
hashLink,
|
|
17
|
+
operationsToAddPieces,
|
|
18
|
+
operationsToPullPieces,
|
|
19
|
+
} from '../utils.ts'
|
|
20
|
+
|
|
21
|
+
/** Operations read per batch before fitting; above the ~400 metadata-free pieces one addPieces message holds. */
|
|
22
|
+
const MAX_BATCH_CANDIDATES = 500
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Curio admits at most 108 unique pieces per pull request and 108 pending pulled pieces per client.
|
|
26
|
+
*
|
|
27
|
+
* @see https://github.com/filecoin-project/curio/blob/main/pdp/pull_types.go
|
|
28
|
+
*/
|
|
29
|
+
const MAX_PULL_PIECES = 108
|
|
30
|
+
|
|
31
|
+
/** Curio answers 429 with Retry-After (1-5 minutes) while its pull queue is full; wait it out instead of failing. */
|
|
32
|
+
const PULL_RETRY_COUNT = 10
|
|
14
33
|
|
|
15
34
|
export type RunPullPiecesPhaseOptions = {
|
|
16
35
|
localDb: LocalDatabase
|
|
17
36
|
api: IndexerApi
|
|
18
37
|
repair: RepairSelect
|
|
19
38
|
concurrency: number
|
|
20
|
-
|
|
39
|
+
/** Max pieces per batch. Defaults to as many as fit in one addPieces message. */
|
|
40
|
+
batchSize?: number
|
|
21
41
|
client: WalletClient
|
|
22
42
|
}
|
|
23
43
|
|
|
@@ -25,10 +45,11 @@ type CreateAddPiecesWorkerOptions = {
|
|
|
25
45
|
localDb: LocalDatabase
|
|
26
46
|
/** CIDs already in the target dataset; repairs only. Updated as batches are added. */
|
|
27
47
|
targetCids?: Set<string>
|
|
48
|
+
/** Max unique pieces per pull request. */
|
|
49
|
+
pullChunkSize: number
|
|
28
50
|
repair: RepairSelect
|
|
29
51
|
client: WalletClient
|
|
30
52
|
state: {
|
|
31
|
-
totalBatches: number
|
|
32
53
|
totalOperations: number
|
|
33
54
|
completedOperations: number
|
|
34
55
|
failedOperations: number
|
|
@@ -44,13 +65,21 @@ type AddPiecesJobOptions = {
|
|
|
44
65
|
/**
|
|
45
66
|
* Create a worker function that adds pieces to the target repair dataset.
|
|
46
67
|
*/
|
|
47
|
-
function createAddPiecesWorker({
|
|
68
|
+
function createAddPiecesWorker({
|
|
69
|
+
localDb,
|
|
70
|
+
targetCids,
|
|
71
|
+
pullChunkSize,
|
|
72
|
+
repair,
|
|
73
|
+
client,
|
|
74
|
+
state,
|
|
75
|
+
log,
|
|
76
|
+
}: CreateAddPiecesWorkerOptions) {
|
|
48
77
|
return async (options: AddPiecesJobOptions) => {
|
|
49
78
|
let completedOps = 0
|
|
50
79
|
let failedOps = 0
|
|
51
80
|
let operations: OperationSelect[] = options.operations
|
|
52
81
|
|
|
53
|
-
const group = log.group(`Batch ${options.batchNumber}
|
|
82
|
+
const group = log.group(`Batch ${options.batchNumber} (${options.operations.length} pieces)`)
|
|
54
83
|
|
|
55
84
|
try {
|
|
56
85
|
const dataset = await getTargetDataset({ localDb, repairId: repair.id, client })
|
|
@@ -69,36 +98,41 @@ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log
|
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
group.message(`Pulling ${operations.length} pieces...`)
|
|
72
|
-
// pull pieces
|
|
101
|
+
// pull pieces in chunks that Curio admits, one chunk at a time per worker
|
|
73
102
|
if (operations.length > 0) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
const pullPieces = operationsToPullPieces(operations)
|
|
104
|
+
const pullChunks = Math.ceil(pullPieces.length / pullChunkSize)
|
|
105
|
+
for (let offset = 0; offset < pullPieces.length; offset += pullChunkSize) {
|
|
106
|
+
const chunk = offset / pullChunkSize + 1
|
|
107
|
+
const pullResult = await SP.waitForPullPieces(client, {
|
|
108
|
+
serviceURL: repair.targetProviderUrl,
|
|
109
|
+
dataSetId: dataset.dataSetId,
|
|
110
|
+
clientDataSetId: dataset.clientDataSetId,
|
|
111
|
+
pieces: pullPieces.slice(offset, offset + pullChunkSize),
|
|
112
|
+
retryCount: PULL_RETRY_COUNT,
|
|
113
|
+
onStatus: (status) => {
|
|
114
|
+
const completed = status.pieces.filter((piece) => piece.status === 'complete').length
|
|
115
|
+
const failed = status.pieces.filter((piece) => piece.status === 'failed').length
|
|
116
|
+
group.message(`Pull ${chunk}/${pullChunks}: ${completed} completed, ${failed} failed`)
|
|
117
|
+
},
|
|
118
|
+
})
|
|
86
119
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
120
|
+
for (const { pieceCid, status } of pullResult.pieces) {
|
|
121
|
+
const cid = pieceCid.toString()
|
|
122
|
+
if (status !== 'complete') {
|
|
123
|
+
state.failedOperations++
|
|
124
|
+
failedOps++
|
|
125
|
+
const { operationToFailed, operationToCommit } = excludeOperationsByCid(operations, cid)
|
|
126
|
+
operations = operationToCommit
|
|
127
|
+
await upsertOperations({
|
|
128
|
+
localDb,
|
|
129
|
+
operations: operationToFailed.map((operation) => ({
|
|
130
|
+
...operation,
|
|
131
|
+
status: 'failed',
|
|
132
|
+
error: `pull failed with status ${status}`,
|
|
133
|
+
})),
|
|
134
|
+
})
|
|
135
|
+
}
|
|
102
136
|
}
|
|
103
137
|
}
|
|
104
138
|
}
|
|
@@ -110,10 +144,7 @@ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log
|
|
|
110
144
|
serviceURL: repair.targetProviderUrl,
|
|
111
145
|
dataSetId: dataset.dataSetId,
|
|
112
146
|
clientDataSetId: dataset.clientDataSetId,
|
|
113
|
-
pieces: operations
|
|
114
|
-
pieceCid: Piece.from(operation.cid),
|
|
115
|
-
metadata: targetCids ? undefined : operation.metadata,
|
|
116
|
-
})),
|
|
147
|
+
pieces: operationsToAddPieces(operations, !targetCids),
|
|
117
148
|
})
|
|
118
149
|
await upsertOperations({
|
|
119
150
|
localDb,
|
|
@@ -125,6 +156,8 @@ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log
|
|
|
125
156
|
|
|
126
157
|
group.message(`Waiting for add pieces ${hashLink(addPiecesResult.txHash, client.chain)}...`)
|
|
127
158
|
const waitForAddPieces = await SP.waitForAddPieces(addPiecesResult)
|
|
159
|
+
// Curio may replace the message by fee; record the hash that landed on chain.
|
|
160
|
+
const confirmedTxHash = waitForAddPieces.confirmedTxHash ?? waitForAddPieces.txHash
|
|
128
161
|
state.completedOperations += operations.length
|
|
129
162
|
completedOps += operations.length
|
|
130
163
|
for (const operation of operations) targetCids?.add(operation.cid)
|
|
@@ -134,7 +167,7 @@ function createAddPiecesWorker({ localDb, targetCids, repair, client, state, log
|
|
|
134
167
|
...operation,
|
|
135
168
|
status: 'completed',
|
|
136
169
|
error: null,
|
|
137
|
-
txHash:
|
|
170
|
+
txHash: confirmedTxHash,
|
|
138
171
|
})),
|
|
139
172
|
})
|
|
140
173
|
}
|
|
@@ -179,7 +212,6 @@ export async function runAddPieces({
|
|
|
179
212
|
)
|
|
180
213
|
let batchNumber = 0
|
|
181
214
|
const state = {
|
|
182
|
-
totalBatches: Math.ceil(totalOperations / batchSize),
|
|
183
215
|
totalOperations,
|
|
184
216
|
completedOperations: 0,
|
|
185
217
|
failedOperations: 0,
|
|
@@ -190,16 +222,20 @@ export async function runAddPieces({
|
|
|
190
222
|
limit: 1,
|
|
191
223
|
})
|
|
192
224
|
|
|
225
|
+
const dataset = totalOperations > 0 ? await getTargetDataset({ localDb, repairId: repair.id, client }) : undefined
|
|
226
|
+
|
|
193
227
|
// Repairs and second copies dedupe into one shared dataset; read its CIDs once instead of per batch.
|
|
194
228
|
let targetCids: Set<string> | undefined
|
|
195
|
-
if (repair.repairDataSetId == null &&
|
|
196
|
-
const dataset = await getTargetDataset({ localDb, repairId: repair.id, client })
|
|
229
|
+
if (repair.repairDataSetId == null && dataset) {
|
|
197
230
|
const pieces = await listDataSetPieces(api, dataset.dataSetId, { removed: false })
|
|
198
231
|
targetCids = new Set(pieces.flatMap((piece) => (piece.cid == null ? [] : [piece.cid])))
|
|
199
232
|
}
|
|
200
233
|
|
|
201
234
|
async function getNextBatch(): Promise<OperationSelect[] | null> {
|
|
202
|
-
|
|
235
|
+
if (!dataset) {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
const candidates = await localDb.query.operations.findMany({
|
|
203
239
|
where: and(
|
|
204
240
|
eq(localSchema.operations.repairId, repair.id),
|
|
205
241
|
eq(localSchema.operations.type, 'add_piece'),
|
|
@@ -207,17 +243,27 @@ export async function runAddPieces({
|
|
|
207
243
|
gt(localSchema.operations.id, cursor)
|
|
208
244
|
),
|
|
209
245
|
orderBy: [asc(localSchema.operations.id)],
|
|
210
|
-
limit: batchSize,
|
|
246
|
+
limit: batchSize ?? MAX_BATCH_CANDIDATES,
|
|
211
247
|
})
|
|
212
|
-
if (
|
|
248
|
+
if (candidates.length === 0) {
|
|
213
249
|
return null
|
|
214
250
|
}
|
|
215
251
|
|
|
252
|
+
// Size the batch by what fits in one addPieces message rather than a fixed piece count.
|
|
253
|
+
const operations = fitAddPiecesBatch({
|
|
254
|
+
operations: candidates,
|
|
255
|
+
dataSetId: dataset.dataSetId,
|
|
256
|
+
chain: client.chain,
|
|
257
|
+
withMetadata: !targetCids,
|
|
258
|
+
})
|
|
259
|
+
|
|
216
260
|
cursor = operations.at(-1)?.id ?? cursor
|
|
217
261
|
return operations
|
|
218
262
|
}
|
|
219
263
|
|
|
220
|
-
|
|
264
|
+
// Keep pieces pulling across all workers within Curio's per-client limit.
|
|
265
|
+
const pullChunkSize = Math.max(1, Math.floor(MAX_PULL_PIECES / concurrency))
|
|
266
|
+
const addPiecesJob = createAddPiecesWorker({ localDb, targetCids, pullChunkSize, repair, client, state, log })
|
|
221
267
|
const queue = new PQueue({ concurrency })
|
|
222
268
|
|
|
223
269
|
while (true) {
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ApiClient } from '@filecoin-project/api-client'
|
|
2
|
-
import type {
|
|
2
|
+
import type { FilecoinChain } from '@filoz/synapse-core/chains'
|
|
3
3
|
import type { Client } from '@libsql/client'
|
|
4
4
|
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
|
5
5
|
import type { z } from 'incur'
|
|
@@ -26,7 +26,7 @@ export interface Config {
|
|
|
26
26
|
dbPath: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export type WalletClient = ViemClient<Transport,
|
|
29
|
+
export type WalletClient = ViemClient<Transport, FilecoinChain, Account>
|
|
30
30
|
|
|
31
31
|
export type Context = z.infer<typeof contextSchema>
|
|
32
32
|
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type FilecoinChain, getChain } from '@filoz/synapse-core/chains'
|
|
2
2
|
import * as Piece from '@filoz/synapse-core/piece'
|
|
3
|
-
import
|
|
3
|
+
import * as SP from '@filoz/synapse-core/sp'
|
|
4
4
|
import { getTableColumns, inArray, type SQL, sql } from 'drizzle-orm'
|
|
5
5
|
import { drizzle } from 'drizzle-orm/libsql'
|
|
6
6
|
import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
@@ -166,7 +166,7 @@ export async function migrateLocalDatabase(db: LocalDatabase) {
|
|
|
166
166
|
* @param chain - The chain to use
|
|
167
167
|
* @returns The link
|
|
168
168
|
*/
|
|
169
|
-
export function hashLink(hash: string, chain:
|
|
169
|
+
export function hashLink(hash: string, chain: FilecoinChain) {
|
|
170
170
|
const link = terminalLink(hash, `${chain.blockExplorers?.default?.url}/tx/${hash}`)
|
|
171
171
|
return link
|
|
172
172
|
}
|
|
@@ -327,3 +327,69 @@ export function operationsToPullPieces(operations: OperationSelect[]) {
|
|
|
327
327
|
}
|
|
328
328
|
return pieces
|
|
329
329
|
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Convert operations to addPieces inputs. Repairs and second copies add pieces without metadata.
|
|
333
|
+
*/
|
|
334
|
+
export function operationsToAddPieces(operations: OperationSelect[], withMetadata: boolean) {
|
|
335
|
+
return operations.map((operation) => ({
|
|
336
|
+
pieceCid: Piece.from(operation.cid),
|
|
337
|
+
metadata: withMetadata ? operation.metadata : undefined,
|
|
338
|
+
}))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Take the longest prefix of operations that fits in one addPieces message.
|
|
343
|
+
* Always returns at least one operation so an oversized or invalid piece fails in the worker instead of stalling the queue.
|
|
344
|
+
*/
|
|
345
|
+
export function fitAddPiecesBatch({
|
|
346
|
+
operations,
|
|
347
|
+
dataSetId,
|
|
348
|
+
chain,
|
|
349
|
+
withMetadata,
|
|
350
|
+
}: {
|
|
351
|
+
operations: OperationSelect[]
|
|
352
|
+
dataSetId: bigint
|
|
353
|
+
chain: FilecoinChain
|
|
354
|
+
withMetadata: boolean
|
|
355
|
+
}): OperationSelect[] {
|
|
356
|
+
// Malformed CIDs throw; stop before the first one so it fails alone in the worker.
|
|
357
|
+
const pieces: SP.LimiterPiece[] = []
|
|
358
|
+
for (const operation of operations) {
|
|
359
|
+
try {
|
|
360
|
+
pieces.push(...operationsToAddPieces([operation], withMetadata))
|
|
361
|
+
} catch {
|
|
362
|
+
break
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (pieces.length === 0) {
|
|
366
|
+
return operations.slice(0, 1)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const fits = (count: number) => {
|
|
370
|
+
try {
|
|
371
|
+
return SP.addPiecesFits({
|
|
372
|
+
kind: 'addPieces',
|
|
373
|
+
dataSetId,
|
|
374
|
+
legacyPieceStorageIdLimit: chain.legacyPieceStorageIdLimit,
|
|
375
|
+
pieces: pieces.slice(0, count),
|
|
376
|
+
})
|
|
377
|
+
} catch {
|
|
378
|
+
// Invalid metadata throws; isolate that piece so addPieces fails it in the worker.
|
|
379
|
+
return false
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Calldata only grows as pieces are added, so binary search the largest prefix that fits.
|
|
384
|
+
let low = 1
|
|
385
|
+
let high = pieces.length
|
|
386
|
+
while (low < high) {
|
|
387
|
+
const mid = Math.ceil((low + high) / 2)
|
|
388
|
+
if (fits(mid)) {
|
|
389
|
+
low = mid
|
|
390
|
+
} else {
|
|
391
|
+
high = mid - 1
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return operations.slice(0, low)
|
|
395
|
+
}
|