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 c76a66099971869d6429f912636c010a1557a39d
Parent: 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-cospan
1 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     (
cospan · schematic version control on atproto built on AT Protocol