fix: async panproto-vcs import on push (non-blocking) The panproto-vcs import (import_git_repo) walks the entire commit history on every push, taking minutes for repos with 100+ commits. This was blocking the git push response, causing timeouts. Fix: the git mirror ref updates happen synchronously (fast), and the panproto-vcs import runs in a background spawn_blocking task. The push response returns immediately. The import continues in the background and logs when complete. The incremental import optimization is tracked upstream at panproto/panproto#26. Until that lands, the background approach ensures pushes don't hang.
Author: Aaron Steven White
Commit
c76a66099971869d6429f912636c010a1557a39dParent: 9380bcbccb
Structural diff unavailable
These commits were pushed via plain git push, so no pre-parsed
schemas are available. Install git-remote-cospan and re-push via panproto:// to
see scope-level changes, breaking change detection, and semantic diffs.
brew install panproto/tap/git-remote-cospan1 file changed +50 -39
@@ -179,26 +179,18 @@ pub async fn git_receive_pack(
179179 } 180180 } 181181 182- // 4. Apply ref updates on the mirror, then mirror them into panproto-vcs. 183- let mut vcs_store = match store_guard.open_or_init(&did, &repo) { 184- Ok(s) => s, 185- Err(e) => return err_response(format!("open panproto store: {e}")).into_response(), 186- }; 187- 182+ // 4. Apply ref updates on the git mirror (fast: just updating ref pointers). 188183 let mut response = String::new(); 184+ let mut import_tasks: Vec<(String, String)> = Vec::new(); // (new_oid, refname) for async import 189185 190186 for (_old_oid, new_oid, refname) in &ref_updates { 191187 let zero_oid = "0".repeat(40); 192188 193189 if new_oid == &zero_oid { 194- // Ref deletion: remove from mirror and panproto store. 195190 match git_mirror.find_reference(refname) { 196- Ok(mut r) => { 197- let _ = r.delete(); 198- } 191+ Ok(mut r) => { let _ = r.delete(); } 199192 Err(_) => {} 200193 } 201- let _ = panproto_vcs::Store::delete_ref(&mut vcs_store, refname); 202194 response.push_str(&pkt_line(&format!("ok {refname}\n"))); 203195 continue; 204196 }
@@ -211,44 +203,63 @@ pub async fn git_receive_pack(
211203 } 212204 }; 213205 214- // Update the mirror ref. 215206 if let Err(e) = git_mirror.reference(refname, git_oid, true, "receive-pack") { 216207 response.push_str(&pkt_line(&format!("ng {refname} mirror ref: {e}\n"))); 217208 continue; 218209 } 219210 220- // Import the git commit into panproto-vcs so schema operations 221- // can see it. Failure is logged but not fatal to the push — 222- // the mirror is the source of truth for git clients. 223- match panproto_git::import_git_repo(&git_mirror, &mut vcs_store, new_oid) { 224- Ok(import_result) => { 225- if let Err(e) = 226- panproto_vcs::Store::set_ref(&mut vcs_store, refname, import_result.head_id) 227- { 228- tracing::warn!( 229- %did, %repo, %refname, error = %e, 230- "failed to set panproto ref after import" 231- ); 232- } 233- tracing::info!( 234- %did, %repo, %refname, 235- commits = import_result.commit_count, 236- "imported git commits into panproto-vcs" 237- ); 238- } 239- Err(e) => { 240- tracing::warn!( 241- %did, %repo, %refname, error = %e, 242- "panproto-vcs import failed (git mirror still updated)" 243- ); 244- } 245- } 246- 211+ import_tasks.push((new_oid.clone(), refname.clone())); 247212 response.push_str(&pkt_line(&format!("ok {refname}\n"))); 248213 } 249214 250215 drop(store_guard); 251216 217+ // 5. Import into panproto-vcs asynchronously. The push response is 218+ // sent immediately; the import runs in the background. This is 219+ // necessary because import_git_repo walks the entire commit 220+ // history (tracked upstream at panproto/panproto#26) and can 221+ // take minutes for repos with 100+ commits. 222+ if !import_tasks.is_empty() { 223+ let store_clone = state.store.clone(); 224+ let did_clone = did.clone(); 225+ let repo_clone = repo.clone(); 226+ tokio::task::spawn_blocking(move || { 227+ let store_guard = store_clone.blocking_lock(); 228+ let mirror = match store_guard.open_or_init_git_mirror(&did_clone, &repo_clone) { 229+ Ok(m) => m, 230+ Err(e) => { 231+ tracing::error!(error = %e, "background import: open mirror failed"); 232+ return; 233+ } 234+ }; 235+ let mut vcs_store = match store_guard.open_or_init(&did_clone, &repo_clone) { 236+ Ok(s) => s, 237+ Err(e) => { 238+ tracing::error!(error = %e, "background import: open vcs store failed"); 239+ return; 240+ } 241+ }; 242+ for (new_oid, refname) in &import_tasks { 243+ match panproto_git::import_git_repo(&mirror, &mut vcs_store, new_oid) { 244+ Ok(result) => { 245+ let _ = panproto_vcs::Store::set_ref(&mut vcs_store, refname, result.head_id); 246+ tracing::info!( 247+ did = %did_clone, repo = %repo_clone, %refname, 248+ commits = result.commit_count, 249+ "background: imported git commits into panproto-vcs" 250+ ); 251+ } 252+ Err(e) => { 253+ tracing::warn!( 254+ did = %did_clone, repo = %repo_clone, %refname, error = %e, 255+ "background: panproto-vcs import failed" 256+ ); 257+ } 258+ } 259+ } 260+ }); 261+ } 262+ 252263 let full_response = format!("{}{}0000", pkt_line("unpack ok\n"), response); 253264 254265 (