mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-09-14 04:45:07 +00:00
fix(http): keep long push finalization alive
The readiness fix incb7e5ae0deliberately withholds receive-pack's terminal flush until process_newly_available_git_data has promoted events, copied any required objects, aligned owner repositories, and notified subscribers. Complex multi-owner finalization can itself exceed ngit's 15-second per-recv I/O timeout even though the server is still making healthy progress. This is distinct from the large-pack failure behindf4828c63: that timeout occurred inside git-receive-pack while Git resolved deltas and checked connectivity. Streaming Git stdout continues to cover that phase. This change covers the post-Git GRASP finalization phase introduced by the corrected completion boundary. When the client negotiated side-band-64k and a terminal flush is being held, send a valid band-2 progress pkt-line every five seconds. Stop and join the keepalive task before releasing the flush so no progress can race past the protocol boundary. Do not invent packets for non-sideband clients. The regression blocks purgatory promotion past the keepalive interval and proves progress arrives while the announcement is unavailable, then verifies the promoted event is queryable before the final flush. Unit coverage pins the pkt-line framing and band identifier. Depends-on:cb7e5ae0f9Original-streaming-fix:f4828c6393Timeout-issue: 497c8ae1554037142e366f9ba363ba898fc16e90198fded195b9a45adb6f38c7
This commit is contained in:
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Sideband-aware Git clients now receive periodic progress while GRASP performs post-push purgatory promotion and cross-owner repository alignment, preventing the client I/O timeout from expiring during unusually complex finalization.
|
||||
- Smart HTTP pushes now expose the terminal receive-pack flush only after GRASP has finished promoting the matching repository announcement and state from purgatory. Git progress remains streamed while large packs are resolved and checked, but an immediately following clone or proposal push can now rely on a completed push being queryable on the relay.
|
||||
- Batch the one-time deletion-request lifecycle migration so large production databases do not remain unavailable while LMDB commits every historical request in separate transactions.
|
||||
- Prevent invitation syncing from dropping a source relay while its initial repository history is still being downloaded.
|
||||
|
||||
+105
-3
@@ -10,8 +10,10 @@ use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::MissedTickBehavior;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use super::protocol::{GitService, PktLine};
|
||||
@@ -27,6 +29,8 @@ use crate::purgatory::Purgatory;
|
||||
|
||||
pub(crate) const STREAM_CHANNEL_DEPTH: usize = 8;
|
||||
const STREAM_CHUNK_SIZE: usize = 8 * 1024;
|
||||
const POST_PUSH_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const POST_PUSH_KEEPALIVE_MESSAGE: &[u8] = b"GRASP is finalizing the push\n";
|
||||
|
||||
/// Handle GET /info/refs?service=git-{upload,receive}-pack
|
||||
///
|
||||
@@ -389,6 +393,59 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode progress that keeps a sideband-aware receive-pack client alive.
|
||||
///
|
||||
/// Post-push purgatory promotion can copy objects and align several owner
|
||||
/// repositories. The terminal flush remains withheld until that work finishes,
|
||||
/// so send a valid band-2 pkt-line during a long finalization window instead of
|
||||
/// leaving libgit2 with no response bytes until its per-recv timeout expires.
|
||||
fn receive_pack_keepalive_pktline() -> Vec<u8> {
|
||||
let mut payload = Vec::with_capacity(1 + POST_PUSH_KEEPALIVE_MESSAGE.len());
|
||||
payload.push(0x02); // band 2 = progress
|
||||
payload.extend_from_slice(POST_PUSH_KEEPALIVE_MESSAGE);
|
||||
PktLine::data(payload).encode()
|
||||
}
|
||||
|
||||
struct ReceivePackKeepalive {
|
||||
task: Option<tokio::task::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl ReceivePackKeepalive {
|
||||
fn spawn(tx: mpsc::Sender<Result<Frame<Bytes>, io::Error>>, period: Duration) -> Self {
|
||||
let task = tokio::spawn(async move {
|
||||
let mut ticker = tokio::time::interval(period);
|
||||
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
ticker.tick().await;
|
||||
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
if send_body_bytes(&tx, receive_pack_keepalive_pktline())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
Self { task: Some(task) }
|
||||
}
|
||||
|
||||
async fn stop(mut self) {
|
||||
if let Some(task) = self.task.take() {
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ReceivePackKeepalive {
|
||||
fn drop(&mut self) {
|
||||
if let Some(task) = self.task.take() {
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain Git stderr for later logging or Git protocol error synthesis.
|
||||
///
|
||||
/// Streaming handlers run this concurrently with stdout pumping so the child
|
||||
@@ -795,7 +852,22 @@ async fn stream_receive_pack_output<S, E>(
|
||||
// that client-visible success boundary so push completion implies local
|
||||
// refs/HEAD and promoted events are ready. Follow-up failures remain
|
||||
// internal/log-only rather than client-visible push rejections.
|
||||
match process_newly_available_git_data(
|
||||
//
|
||||
// A complex promotion may itself run longer than the client's receive
|
||||
// timeout. Only clients that negotiated side-band-64k can safely receive
|
||||
// invented progress pkt-lines, so keep those clients alive while leaving
|
||||
// the response for non-sideband clients byte-for-byte unchanged.
|
||||
let keepalive_task =
|
||||
if terminal_flush.is_some() && client_negotiated_sideband_64k(&request_body) {
|
||||
Some(ReceivePackKeepalive::spawn(
|
||||
tx.clone(),
|
||||
POST_PUSH_KEEPALIVE_INTERVAL,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let processing_result = process_newly_available_git_data(
|
||||
&repo_path,
|
||||
&new_oids,
|
||||
&database,
|
||||
@@ -804,8 +876,15 @@ async fn stream_receive_pack_output<S, E>(
|
||||
std::path::Path::new(&git_data_path),
|
||||
promotion_hooks.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
|
||||
// Stop and join the sender before releasing the flush so no keepalive can
|
||||
// race behind the terminal protocol boundary.
|
||||
if let Some(task) = keepalive_task {
|
||||
task.stop().await;
|
||||
}
|
||||
|
||||
match processing_result {
|
||||
Ok(result) => {
|
||||
if result.released_any() {
|
||||
info!(
|
||||
@@ -1054,6 +1133,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn post_push_keepalive_is_sideband_progress() {
|
||||
use tokio::time::timeout;
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<Result<Frame<Bytes>, io::Error>>(STREAM_CHANNEL_DEPTH);
|
||||
let keepalive = ReceivePackKeepalive::spawn(tx, Duration::from_millis(10));
|
||||
|
||||
let frame = timeout(Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("keepalive should arrive before timeout")
|
||||
.expect("keepalive channel should remain open")
|
||||
.expect("keepalive should not be an HTTP body error");
|
||||
let data = frame.into_data().expect("keepalive should contain data");
|
||||
|
||||
assert_eq!(data.as_ref(), receive_pack_keepalive_pktline());
|
||||
assert_eq!(
|
||||
data[4], 0x02,
|
||||
"keepalive pkt-line payload must use sideband band 2"
|
||||
);
|
||||
|
||||
keepalive.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn receive_pack_err_without_body_is_not_wrapped() {
|
||||
// No request body → conservative path: do not wrap.
|
||||
|
||||
@@ -238,6 +238,27 @@ async fn receive_pack_terminal_flush_waits_for_purgatory_promotion() {
|
||||
"receive-pack terminal flush must remain hidden while promotion is blocked"
|
||||
);
|
||||
|
||||
let keepalive = timeout(Duration::from_secs(6), body.frame())
|
||||
.await
|
||||
.expect("sideband keepalive should arrive during blocked promotion")
|
||||
.expect("body should remain open while promotion is blocked")
|
||||
.expect("keepalive should not be an HTTP body error");
|
||||
streamed.extend_from_slice(
|
||||
&keepalive
|
||||
.into_data()
|
||||
.expect("keepalive frame should contain data"),
|
||||
);
|
||||
assert!(
|
||||
streamed
|
||||
.windows(b"GRASP is finalizing the push\n".len())
|
||||
.any(|window| window == b"GRASP is finalizing the push\n"),
|
||||
"blocked post-push processing should emit sideband progress"
|
||||
);
|
||||
assert!(
|
||||
!streamed.ends_with(b"0000"),
|
||||
"keepalive must not expose the terminal flush"
|
||||
);
|
||||
|
||||
release.add_permits(1);
|
||||
|
||||
loop {
|
||||
@@ -255,9 +276,13 @@ async fn receive_pack_terminal_flush_waits_for_purgatory_promotion() {
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
streamed, b"first-progress\nsecond-progress\n0000",
|
||||
"terminal flush should be the final client-visible bytes"
|
||||
assert!(
|
||||
streamed.starts_with(b"first-progress\nsecond-progress\n"),
|
||||
"git progress should remain at the start of the response"
|
||||
);
|
||||
assert!(
|
||||
streamed.ends_with(b"0000"),
|
||||
"terminal flush should remain the final client-visible bytes"
|
||||
);
|
||||
let after_save = database
|
||||
.query(Filter::new().id(announcement.id))
|
||||
|
||||
Reference in New Issue
Block a user