irongit

Git hosting and a container registry in one Rust binary (axum + Astro)

Registry: upload layers to R2 before taking the digest lock; wire up lazily loaded fragments

huncholanehuncholaneauthored
parent 0dbbe4dcommit a6b858308a81938e7918752c20fb472015778235Browse files

2 files changed, +22 -5

+17-3backend/src/registry/blobs.rs
@@ -344,13 +344,27 @@ async fn finalize(state: &AppState, package: &Package, path: &Path, digest: &str
344344 return Err(RegError::digest_invalid("uploaded content does not match the digest").detail(serde_json::json!({ "expected": digest, "actual": actual })));
345345 }
346346
347+ // Upload before taking the per-digest lock, so a large layer does not pin
348+ // a pooled connection for the whole transfer. Two concurrent uploads of
349+ // the same digest write identical bytes to the same key, which is fine.
350+ let known: bool = sqlx::query_scalar("select exists(select 1 from blobs where digest = $1)").bind(digest).fetch_one(&state.db).await?;
351+ let mut uploaded = false;
352+ if !known {
353+ let started = std::time::Instant::now();
354+ state.storage.put_file(&keys::blob(digest), path).await?;
355+ uploaded = true;
356+ tracing::info!(digest, size, ms = started.elapsed().as_millis() as u64, "blob stored in R2");
357+ }
358+
347359 let mut tx = state.db.begin().await?;
348360 sqlx::query("select pg_advisory_xact_lock(hashtextextended($1, 0))").bind(digest).execute(&mut *tx).await?;
349361 let exists: bool = sqlx::query_scalar("select exists(select 1 from blobs where digest = $1)").bind(digest).fetch_one(&mut *tx).await?;
350362 if !exists {
351- let started = std::time::Instant::now();
352- state.storage.put_file(&keys::blob(digest), path).await?;
353- tracing::info!(digest, size, ms = started.elapsed().as_millis() as u64, "blob stored in R2");
363+ if !uploaded {
364+ // GC removed the unlinked blob between the check and the lock.
365+ state.storage.put_file(&keys::blob(digest), path).await?;
366+ tracing::info!(digest, "blob re-stored after concurrent cleanup");
367+ }
354368 sqlx::query("insert into blobs (digest, size) values ($1, $2) on conflict do nothing")
355369 .bind(digest)
356370 .bind(size as i64)
+5-2frontend/src/scripts/app.ts
@@ -150,6 +150,9 @@ async function loadLazy(el: HTMLElement) {
150150 if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
151151 el.innerHTML = await response.text();
152152 el.removeAttribute("aria-busy");
153+ // Fragments arrive after page setup ran; wire up what they contain.
154+ initConfirmInputs(el);
155+ el.querySelectorAll<HTMLElement>("[data-scroll-end]").forEach((s) => (s.scrollLeft = s.scrollWidth));
153156 el.querySelectorAll<HTMLElement>("[data-lazy]").forEach(loadLazy);
154157 } catch (error) {
155158 el.innerHTML = `<div class="alert alert-error">Could not load this section (${(error as Error).message}). <button class="btn btn-sm" data-retry>Retry</button></div>`;
@@ -190,8 +193,8 @@ function initSidebar() {
190193
191194 // --- Dangerous forms: the submit button unlocks when the name is typed ------
192195
193-function initConfirmInputs() {
194- document.querySelectorAll<HTMLInputElement>("input[data-confirm-value]").forEach((input) => {
196+function initConfirmInputs(root: ParentNode = document) {
197+ root.querySelectorAll<HTMLInputElement>("input[data-confirm-value]").forEach((input) => {
195198 const form = input.form;
196199 const button = form?.querySelector<HTMLButtonElement>("button[type=submit]");
197200 if (!button) return;