|
| 1 | +import { and, desc, eq, inArray } from 'drizzle-orm' |
| 2 | +import { Cli, z } from 'incur' |
| 3 | +import { isAddress } from 'viem' |
| 4 | +import { repairDelete } from '../db/repair-delete.ts' |
| 5 | +import { secondCopyCreate } from '../db/second-copy-create.ts' |
| 6 | +import { contextMiddleware, contextSchema } from '../middleware.ts' |
| 7 | +import { runAddPieces } from '../pipeline/add-pieces.ts' |
| 8 | +import { ensureRepairDataset } from '../pipeline/create-datasets.ts' |
| 9 | +import { globalOptions } from '../utils.ts' |
| 10 | + |
| 11 | +export const secondCopy = Cli.create('second-copy', { |
| 12 | + description: 'Second-copy piece commands', |
| 13 | + vars: contextSchema, |
| 14 | +}) |
| 15 | + |
| 16 | +secondCopy.command('create', { |
| 17 | + description: 'Create a second-copy job from a pieces file', |
| 18 | + options: globalOptions.extend({ |
| 19 | + piecesFile: z.string().describe('Path to JSON file with aggregates[].piece_cid'), |
| 20 | + sourceProviderUrl: z.string().url().describe('Source provider URL to pull pieces from'), |
| 21 | + targetProviderId: z.coerce.bigint().describe('Target provider ID for second-copy'), |
| 22 | + }), |
| 23 | + middleware: [contextMiddleware], |
| 24 | + run: async (c) => { |
| 25 | + try { |
| 26 | + const { piecesFile, sourceProviderUrl, targetProviderId } = c.options |
| 27 | + |
| 28 | + const secondCopyId = await secondCopyCreate({ |
| 29 | + ...c.var, |
| 30 | + piecesFile, |
| 31 | + sourceProviderUrl, |
| 32 | + targetProviderId, |
| 33 | + }) |
| 34 | + |
| 35 | + return c.ok({ |
| 36 | + secondCopyId, |
| 37 | + }) |
| 38 | + } catch (error) { |
| 39 | + console.error(error) |
| 40 | + return c.error({ |
| 41 | + code: 'SECOND_COPY_FAILED', |
| 42 | + message: error instanceof Error ? error.message : 'Failed to create the second-copy job', |
| 43 | + retryable: true, |
| 44 | + }) |
| 45 | + } |
| 46 | + }, |
| 47 | +}) |
| 48 | + |
| 49 | +secondCopy.command('list', { |
| 50 | + description: 'List all second-copy jobs', |
| 51 | + options: globalOptions, |
| 52 | + middleware: [contextMiddleware], |
| 53 | + run: async (c) => { |
| 54 | + try { |
| 55 | + const localSchema = c.var.localDb._.fullSchema |
| 56 | + const jobs = await c.var.localDb.query.repairs.findMany({ |
| 57 | + where: eq(localSchema.repairs.kind, 'second_copy'), |
| 58 | + orderBy: [desc(localSchema.repairs.createdAt)], |
| 59 | + with: { |
| 60 | + operations: true, |
| 61 | + }, |
| 62 | + }) |
| 63 | + |
| 64 | + const secondCopies = jobs.map((repair) => { |
| 65 | + const { operations, ...job } = repair |
| 66 | + return { |
| 67 | + id: job.id, |
| 68 | + status: job.status, |
| 69 | + sourceProviderUrl: job.sourceProviderUrl, |
| 70 | + targetProviderId: job.targetProviderId, |
| 71 | + targetProviderUrl: job.targetProviderUrl, |
| 72 | + targetDataSetId: job.targetDataSetId, |
| 73 | + blockNumber: job.blockNumber, |
| 74 | + operations: operations.length, |
| 75 | + pending: operations.filter((operation) => operation.status === 'pending').length, |
| 76 | + failed: operations.filter((operation) => operation.status === 'failed').length, |
| 77 | + completed: operations.filter((operation) => operation.status === 'completed').length, |
| 78 | + skipped: operations.filter((operation) => operation.status === 'skipped').length, |
| 79 | + } |
| 80 | + }) |
| 81 | + |
| 82 | + return c.ok({ |
| 83 | + secondCopies, |
| 84 | + }) |
| 85 | + } catch (error) { |
| 86 | + console.error(error) |
| 87 | + return c.error({ |
| 88 | + code: 'SECOND_COPY_FAILED', |
| 89 | + message: error instanceof Error ? error.message : 'Failed to list second-copy jobs', |
| 90 | + retryable: true, |
| 91 | + }) |
| 92 | + } |
| 93 | + }, |
| 94 | +}) |
| 95 | + |
| 96 | +secondCopy.command('delete', { |
| 97 | + description: 'Delete a second-copy job', |
| 98 | + args: z.object({ |
| 99 | + secondCopyId: z.coerce.number().describe('Second-copy ID to delete'), |
| 100 | + }), |
| 101 | + options: globalOptions, |
| 102 | + middleware: [contextMiddleware], |
| 103 | + run: async (c) => { |
| 104 | + try { |
| 105 | + const { deleted, operationsDeleted } = await repairDelete({ |
| 106 | + localDb: c.var.localDb, |
| 107 | + repairId: c.args.secondCopyId, |
| 108 | + }) |
| 109 | + |
| 110 | + if (!deleted) { |
| 111 | + return c.error({ |
| 112 | + code: 'SECOND_COPY_NOT_FOUND', |
| 113 | + message: 'Second-copy job not found', |
| 114 | + retryable: false, |
| 115 | + }) |
| 116 | + } |
| 117 | + |
| 118 | + return c.ok({ |
| 119 | + secondCopyId: c.args.secondCopyId, |
| 120 | + operationsDeleted, |
| 121 | + }) |
| 122 | + } catch (error) { |
| 123 | + console.error(error) |
| 124 | + return c.error({ |
| 125 | + code: 'SECOND_COPY_FAILED', |
| 126 | + message: error instanceof Error ? error.message : 'Failed to delete the second-copy job', |
| 127 | + retryable: true, |
| 128 | + }) |
| 129 | + } |
| 130 | + }, |
| 131 | +}) |
| 132 | + |
| 133 | +secondCopy.command('run', { |
| 134 | + description: 'Run a second-copy job', |
| 135 | + args: z.object({ |
| 136 | + secondCopyId: z.coerce.number().describe('Second-copy ID to run'), |
| 137 | + }), |
| 138 | + options: globalOptions.extend({ |
| 139 | + concurrency: z.coerce.number().min(1).max(10).default(4).describe('Concurrency level'), |
| 140 | + batchSize: z.coerce.number().min(1).max(40).default(40).describe('Max pieces per batch'), |
| 141 | + payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Payer address'), |
| 142 | + }), |
| 143 | + middleware: [contextMiddleware], |
| 144 | + run: async (c) => { |
| 145 | + try { |
| 146 | + const schema = c.var.localDb._.fullSchema |
| 147 | + const payer = c.options.payer ?? c.var.client.account.address |
| 148 | + const repair = await c.var.localDb.query.repairs.findFirst({ |
| 149 | + where: and( |
| 150 | + eq(schema.repairs.id, c.args.secondCopyId), |
| 151 | + inArray(schema.repairs.status, ['pending', 'failed']), |
| 152 | + eq(schema.repairs.kind, 'second_copy') |
| 153 | + ), |
| 154 | + }) |
| 155 | + if (!repair) { |
| 156 | + return c.error({ |
| 157 | + code: 'SECOND_COPY_NOT_FOUND', |
| 158 | + message: 'Second-copy job not found, it may have already been run or completed', |
| 159 | + retryable: false, |
| 160 | + }) |
| 161 | + } |
| 162 | + |
| 163 | + await ensureRepairDataset({ |
| 164 | + ...c.var, |
| 165 | + repair, |
| 166 | + payer, |
| 167 | + }) |
| 168 | + |
| 169 | + await runAddPieces({ |
| 170 | + ...c.var, |
| 171 | + repair, |
| 172 | + concurrency: c.options.concurrency, |
| 173 | + batchSize: c.options.batchSize, |
| 174 | + }) |
| 175 | + return c.ok({ |
| 176 | + secondCopyId: repair.id, |
| 177 | + }) |
| 178 | + } catch (error) { |
| 179 | + console.error(error) |
| 180 | + return c.error({ |
| 181 | + code: 'SECOND_COPY_FAILED', |
| 182 | + message: error instanceof Error ? error.message : 'Failed to run the second-copy job', |
| 183 | + retryable: true, |
| 184 | + }) |
| 185 | + } |
| 186 | + }, |
| 187 | +}) |
0 commit comments