Skip to content
File

Blob: src/worker/git/object-store/peel.ts

typescript271 lines
1import type { CacheContext } from "@/worker/cache";
2import type { Logger } from "@/worker/common/logger";
3import type { GitObjectType } from "@/worker/git/core";
4import type { IdxView, PackCatalogRow } from "./types";
5 
6import { inflate } from "@/worker/common";
7import { parseTagTarget } from "@/worker/git/core";
8import { countSubrequest, getLimiter } from "@/worker/git/operations/limits";
9import { SequentialReader } from "@/worker/git/pack/indexer/resolve/reader";
10import { readPackHeaderExFromBuf } from "@/worker/git/pack/packMeta";
11import { loadActivePackCatalog } from "./catalog";
12import { applyGitDelta } from "./delta";
13import { findOidIndex, findOffsetIndex, getNextOffsetByIndex, loadIdxView } from "./idxView";
14import { readObject } from "./store";
15import { ensureMemo, getPackedObjectStoreLogger, logOnce, typeCodeToObjectType } from "./support";
16 
17type TagRef = {
18 name: string;
19 oid: string;
20};
21 
22type PackedObjectPayload = {
23 type: GitObjectType;
24 payload: Uint8Array;
25};
26 
27type PackedTagLocation = {
28 pack: PackCatalogRow;
29 idx: IdxView;
30 objectIndex: number;
31 offset: number;
32 nextOffset: number;
33 oid: string;
34 refNames: string[];
35};
36 
37const LS_REFS_PEELED_CHUNK_SIZE = 1_048_576;
38 
39function countPeelSubrequest(
40 cacheCtx: CacheContext | undefined,
41 log: Logger,
42 packKey: string
43): boolean {
44 const allowed = countSubrequest(cacheCtx);
45 if (allowed) return true;
46 const flag = `ls-refs:peel-soft-budget:${packKey}`;
47 logOnce(cacheCtx, flag, () => {
48 log.warn("soft-budget-exhausted", {
49 op: "r2:ls-refs-peel",
50 packKey,
51 });
52 });
53 return false;
54}
55 
56function buildPackedTagLocation(
57 pack: PackCatalogRow,
58 idx: IdxView,
59 oid: string,
60 refNames: string[]
61): PackedTagLocation | undefined {
62 const objectIndex = findOidIndex(idx, oid);
63 if (objectIndex < 0) return undefined;
64 const offset = idx.offsets[objectIndex];
65 const nextOffset = getNextOffsetByIndex(idx, objectIndex);
66 if (nextOffset === undefined) return undefined;
67 return {
68 pack,
69 idx,
70 objectIndex,
71 offset,
72 nextOffset,
73 oid,
74 refNames,
75 };
76}
77 
78async function readPackedObjectFromLocation(args: {
79 env: Env;
80 repoId: string;
81 reader: SequentialReader;
82 cacheCtx: CacheContext | undefined;
83 location: PackedTagLocation;
84 cache: Map<number, PackedObjectPayload>;
85 visited: Set<string>;
86}): Promise<PackedObjectPayload | undefined> {
87 const cached = args.cache.get(args.location.objectIndex);
88 if (cached) return cached;
89 
90 const visitKey = `${args.location.pack.packKey}#${args.location.objectIndex}`;
91 if (args.visited.has(visitKey)) {
92 throw new Error(`ls-refs peel recursion cycle for ${visitKey}`);
93 }
94 args.visited.add(visitKey);
95 
96 try {
97 const entryLength = args.location.nextOffset - args.location.offset;
98 if (entryLength <= 0) return undefined;
99 
100 // Read the full pack entry in one shot. The SequentialReader keeps a
101 // large sliding window, so nearby tag entries in offset order collapse
102 // into a small number of sequential R2 range reads.
103 const entry = await args.reader.readRange(args.location.offset, entryLength);
104 const header = readPackHeaderExFromBuf(entry, 0);
105 if (!header) return undefined;
106 
107 const inflated = await inflate(entry.subarray(header.headerLen));
108 const baseType = typeCodeToObjectType(header.type);
109 if (baseType) {
110 const resolved = { type: baseType, payload: inflated };
111 args.cache.set(args.location.objectIndex, resolved);
112 return resolved;
113 }
114 
115 if (header.type === 6) {
116 const baseOffset = args.location.offset - (header.baseRel || 0);
117 const baseIndex = findOffsetIndex(args.location.idx, baseOffset);
118 if (baseIndex === undefined) return undefined;
119 const baseNextOffset = getNextOffsetByIndex(args.location.idx, baseIndex);
120 if (baseNextOffset === undefined) return undefined;
121 const base = await readPackedObjectFromLocation({
122 ...args,
123 location: {
124 pack: args.location.pack,
125 idx: args.location.idx,
126 objectIndex: baseIndex,
127 offset: baseOffset,
128 nextOffset: baseNextOffset,
129 oid: args.location.oid,
130 refNames: args.location.refNames,
131 },
132 });
133 if (!base) return undefined;
134 const resolved = {
135 type: base.type,
136 payload: applyGitDelta(base.payload, inflated),
137 };
138 args.cache.set(args.location.objectIndex, resolved);
139 return resolved;
140 }
141 
142 if (header.type === 7 && header.baseOid) {
143 const samePackBase = buildPackedTagLocation(
144 args.location.pack,
145 args.location.idx,
146 header.baseOid,
147 args.location.refNames
148 );
149 if (samePackBase) {
150 const base = await readPackedObjectFromLocation({
151 ...args,
152 location: samePackBase,
153 });
154 if (!base) return undefined;
155 const resolved = {
156 type: base.type,
157 payload: applyGitDelta(base.payload, inflated),
158 };
159 args.cache.set(args.location.objectIndex, resolved);
160 return resolved;
161 }
162 
163 const base = await readObject(
164 args.env,
165 args.repoId,
166 header.baseOid,
167 args.cacheCtx,
168 args.visited
169 );
170 if (!base) return undefined;
171 const resolved = {
172 type: base.type,
173 payload: applyGitDelta(base.payload, inflated),
174 };
175 args.cache.set(args.location.objectIndex, resolved);
176 return resolved;
177 }
178 
179 return undefined;
180 } finally {
181 args.visited.delete(visitKey);
182 }
183}
184 
185export async function loadPeeledTagTargets(
186 env: Env,
187 repoId: string,
188 refs: TagRef[],
189 cacheCtx?: CacheContext
190): Promise<Map<string, string>> {
191 const peeledByRef = new Map<string, string>();
192 if (refs.length === 0) return peeledByRef;
193 
194 ensureMemo(cacheCtx, repoId);
195 const log = getPackedObjectStoreLogger(env, repoId);
196 const packs = await loadActivePackCatalog(env, repoId, cacheCtx);
197 if (packs.length === 0) return peeledByRef;
198 
199 const unresolved = new Map<string, string[]>();
200 for (const ref of refs) {
201 const oid = ref.oid.toLowerCase();
202 const existing = unresolved.get(oid);
203 if (existing) {
204 existing.push(ref.name);
205 } else {
206 unresolved.set(oid, [ref.name]);
207 }
208 }
209 
210 const locationsByPack = new Map<string, PackedTagLocation[]>();
211 for (const pack of packs) {
212 if (unresolved.size === 0) break;
213 const idx = await loadIdxView(env, pack.packKey, cacheCtx, pack.packBytes);
214 if (!idx) continue;
215 
216 for (const [oid, refNames] of Array.from(unresolved.entries())) {
217 const location = buildPackedTagLocation(pack, idx, oid, refNames);
218 if (!location) continue;
219 const locations = locationsByPack.get(pack.packKey) || [];
220 locations.push(location);
221 locationsByPack.set(pack.packKey, locations);
222 unresolved.delete(oid);
223 }
224 }
225 
226 log.debug("ls-refs:peel-batch", {
227 requestedRefs: refs.length,
228 matchedPacks: locationsByPack.size,
229 unresolvedTags: unresolved.size,
230 });
231 
232 const limiter = getLimiter(cacheCtx);
233 for (const [packKey, locations] of locationsByPack) {
234 const pack = locations[0]?.pack;
235 if (!pack) continue;
236 
237 locations.sort((left, right) => left.offset - right.offset);
238 const reader = new SequentialReader(
239 env,
240 packKey,
241 pack.packBytes,
242 LS_REFS_PEELED_CHUNK_SIZE,
243 limiter,
244 () => countPeelSubrequest(cacheCtx, log, packKey),
245 log,
246 cacheCtx?.req.signal
247 );
248 const cache = new Map<number, PackedObjectPayload>();
249 
250 for (const location of locations) {
251 const object = await readPackedObjectFromLocation({
252 env,
253 repoId,
254 reader,
255 cacheCtx,
256 location,
257 cache,
258 visited: new Set<string>(),
259 });
260 if (!object || object.type !== "tag") continue;
261 const peeled = parseTagTarget(object.payload);
262 if (!peeled?.targetOid) continue;
263 for (const refName of location.refNames) {
264 peeledByRef.set(refName, peeled.targetOid);
265 }
266 }
267 }
268 
269 return peeledByRef;
270}