Skip to content
File

Blob: src/worker/do/repo/packOperations.ts

typescript184 lines
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 
10import type { Logger } from "@/worker/common/logger";
11 
12import { createLogger } from "@/worker/common";
13import { MAX_SIMULTANEOUS_CONNECTIONS, SubrequestLimiter } from "@/worker/git/operations/limits";
14import { doPrefix, packIndexKey, packRefsKey } from "@/worker/keys";
15import { deletePackCatalogRows, getDb, getPackCatalogCount, getPackCatalogRow } from "./db";
16import { getActivePackCatalogSnapshot } from "./catalog";
17 
18export 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 
28async 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 */
60export 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 */
168export 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}