File
Blob: src/worker/do/repo/packOperations.ts
| 1 | /** |
| 2 | * Pack operations for repository maintenance |
| 3 | * |
| 4 | * This module provides operations for managing packs and clearing |
| 5 | * Durable Object storage. R2 enumeration during a full repo delete is |
| 6 | * handled by the `repository-delete` queue consumer to keep the |
| 7 | * Worker -> DO -> R2 boundary clean. |
| 8 | */ |
| 9 | |
| 10 | import type { Logger } from "@/worker/common/logger"; |
| 11 | |
| 12 | import { createLogger } from "@/worker/common"; |
| 13 | import { MAX_SIMULTANEOUS_CONNECTIONS, SubrequestLimiter } from "@/worker/git/operations/limits"; |
| 14 | import { doPrefix, packIndexKey, packRefsKey } from "@/worker/keys"; |
| 15 | import { deletePackCatalogRows, getDb, getPackCatalogCount, getPackCatalogRow } from "./db"; |
| 16 | import { getActivePackCatalogSnapshot } from "./catalog"; |
| 17 | |
| 18 | export type RemovePackResult = { |
| 19 | removed: boolean; |
| 20 | deletedPack: boolean; |
| 21 | deletedIndex: boolean; |
| 22 | deletedRefs: boolean; |
| 23 | deletedMetadata: boolean; |
| 24 | rejected?: "active-pack" | "non-superseded-pack"; |
| 25 | packState?: "active" | "superseded" | "unknown"; |
| 26 | }; |
| 27 | |
| 28 | async function deletePackArtifact(args: { |
| 29 | bucket: R2Bucket; |
| 30 | limiter: SubrequestLimiter; |
| 31 | key: string; |
| 32 | op: string; |
| 33 | log: Logger; |
| 34 | deletedMessage: string; |
| 35 | failedMessage: string; |
| 36 | }): Promise<boolean> { |
| 37 | try { |
| 38 | await args.limiter.run(args.op, async () => { |
| 39 | await args.bucket.delete(args.key); |
| 40 | }); |
| 41 | args.log.info(args.deletedMessage, { key: args.key, op: args.op }); |
| 42 | return true; |
| 43 | } catch (error) { |
| 44 | args.log.error(args.failedMessage, { |
| 45 | key: args.key, |
| 46 | op: args.op, |
| 47 | error: String(error), |
| 48 | }); |
| 49 | return false; |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * Remove a specific pack file and its associated data |
| 55 | * @param ctx - Durable Object state context |
| 56 | * @param env - Worker environment |
| 57 | * @param packKey - The pack key to remove (can be either short name or full R2 key) |
| 58 | * @returns Object with removal statistics |
| 59 | */ |
| 60 | export async function removePack( |
| 61 | ctx: DurableObjectState, |
| 62 | env: Env, |
| 63 | packKey: string |
| 64 | ): Promise<RemovePackResult> { |
| 65 | const log = createLogger(env.LOG_LEVEL, { |
| 66 | service: "packOperations:removePack", |
| 67 | doId: ctx.id.toString(), |
| 68 | }); |
| 69 | const limiter = new SubrequestLimiter(MAX_SIMULTANEOUS_CONNECTIONS); |
| 70 | |
| 71 | const result: RemovePackResult = { |
| 72 | removed: false, |
| 73 | deletedPack: false, |
| 74 | deletedIndex: false, |
| 75 | deletedRefs: false, |
| 76 | deletedMetadata: false, |
| 77 | }; |
| 78 | |
| 79 | try { |
| 80 | const prefix = doPrefix(ctx.id.toString()); |
| 81 | let fullPackKey = packKey; |
| 82 | const db = getDb(ctx.storage); |
| 83 | |
| 84 | if (!packKey.startsWith(prefix)) { |
| 85 | fullPackKey = `${prefix}/objects/pack/${packKey}`; |
| 86 | } |
| 87 | |
| 88 | if ((await getPackCatalogCount(db)) === 0) { |
| 89 | await getActivePackCatalogSnapshot(ctx); |
| 90 | } |
| 91 | |
| 92 | const packCatalogRow = await getPackCatalogRow(db, fullPackKey); |
| 93 | const packState: "active" | "superseded" | "unknown" = |
| 94 | packCatalogRow?.state === "active" |
| 95 | ? "active" |
| 96 | : packCatalogRow?.state === "superseded" |
| 97 | ? "superseded" |
| 98 | : "unknown"; |
| 99 | result.packState = packState; |
| 100 | if (packState !== "superseded") { |
| 101 | const rejected = packState === "active" ? "active-pack" : "non-superseded-pack"; |
| 102 | log.warn("reject-pack-delete", { |
| 103 | packKey: fullPackKey, |
| 104 | packState, |
| 105 | rejected, |
| 106 | }); |
| 107 | return { |
| 108 | ...result, |
| 109 | rejected, |
| 110 | }; |
| 111 | } |
| 112 | |
| 113 | log.info("removing-pack", { packKey: fullPackKey }); |
| 114 | |
| 115 | result.deletedPack = await deletePackArtifact({ |
| 116 | bucket: env.REPO_BUCKET, |
| 117 | limiter, |
| 118 | key: fullPackKey, |
| 119 | op: "r2:delete-pack", |
| 120 | log, |
| 121 | deletedMessage: "deleted-pack-file", |
| 122 | failedMessage: "failed-to-delete-pack", |
| 123 | }); |
| 124 | |
| 125 | const indexKey = packIndexKey(fullPackKey); |
| 126 | result.deletedIndex = await deletePackArtifact({ |
| 127 | bucket: env.REPO_BUCKET, |
| 128 | limiter, |
| 129 | key: indexKey, |
| 130 | op: "r2:delete-pack-idx", |
| 131 | log, |
| 132 | deletedMessage: "deleted-index-file", |
| 133 | failedMessage: "failed-to-delete-index", |
| 134 | }); |
| 135 | |
| 136 | const refsKey = packRefsKey(fullPackKey); |
| 137 | result.deletedRefs = await deletePackArtifact({ |
| 138 | bucket: env.REPO_BUCKET, |
| 139 | limiter, |
| 140 | key: refsKey, |
| 141 | op: "r2:delete-pack-refs", |
| 142 | log, |
| 143 | deletedMessage: "deleted-ref-index-file", |
| 144 | failedMessage: "failed-to-delete-ref-index", |
| 145 | }); |
| 146 | |
| 147 | // Remove from pack catalog metadata |
| 148 | await deletePackCatalogRows(db, [fullPackKey]); |
| 149 | result.deletedMetadata = true; |
| 150 | |
| 151 | result.removed = |
| 152 | result.deletedPack || result.deletedIndex || result.deletedRefs || result.deletedMetadata; |
| 153 | |
| 154 | log.info("pack-removal-complete", result); |
| 155 | } catch (e) { |
| 156 | log.error("pack-removal-error", { packKey, error: String(e) }); |
| 157 | throw e; |
| 158 | } |
| 159 | |
| 160 | return result; |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Clears the per-repo Durable Object storage and any pending alarm. |
| 165 | * R2 cleanup is owned by the `repository-delete` queue consumer so we keep |
| 166 | * the DO call free of cross-runtime hops (no Worker -> DO -> R2 chain). |
| 167 | */ |
| 168 | export async function clearRepositoryStorage( |
| 169 | ctx: DurableObjectState, |
| 170 | env: Env |
| 171 | ): Promise<{ deletedDO: boolean }> { |
| 172 | const log = createLogger(env.LOG_LEVEL, { |
| 173 | service: "packOperations:clearRepositoryStorage", |
| 174 | doId: ctx.id.toString(), |
| 175 | }); |
| 176 | |
| 177 | // The 2026-05-13 compatibility date includes `delete_all_deletes_alarm`, |
| 178 | // so this also clears any pending alarm for the deleted repository. |
| 179 | await ctx.storage.deleteAll(); |
| 180 | log.info("clear:storage-deleted-all"); |
| 181 | |
| 182 | return { deletedDO: true }; |
| 183 | } |