Skip to content
This repository was archived by the owner on Oct 19, 2026. It is now read-only.

Commit d0af03b

Browse files
authored
fix: batch save concurrent limit (#2044)
1 parent c00e9c3 commit d0af03b

9 files changed

Lines changed: 713 additions & 44 deletions

File tree

‎ceres/src/pack/import_repo.rs‎

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{
66
Arc, Mutex,
77
atomic::{AtomicUsize, Ordering},
88
},
9+
time::Instant,
910
};
1011

1112
use async_recursion::async_recursion;
@@ -47,6 +48,7 @@ pub struct ImportRepo {
4748
pub command_list: Mutex<Vec<RefCommand>>,
4849
pub unpack_redlock: Arc<RedLock>,
4950
pub git_object_cache: Arc<GitObjectCache>,
51+
pub receive_pack_extra_timings_ms: Mutex<Vec<(String, u128)>>,
5052
}
5153

5254
#[async_trait]
@@ -55,6 +57,19 @@ impl RepoHandler for ImportRepo {
5557
false
5658
}
5759

60+
fn save_entry_concurrency(&self) -> usize {
61+
self.storage.config().pack.save_entry_concurrency
62+
}
63+
64+
fn receive_pack_extra_timings_ms(&self) -> Vec<(String, u128)> {
65+
std::mem::take(
66+
&mut self
67+
.receive_pack_extra_timings_ms
68+
.lock()
69+
.expect("receive_pack_extra_timings_ms lock poisoned"),
70+
)
71+
}
72+
5873
fn sync_commands_after_unpack(&self, commands: &[RefCommand]) {
5974
*self
6075
.command_list
@@ -75,10 +90,32 @@ impl RepoHandler for ImportRepo {
7590
}
7691

7792
async fn finalize_receive_pack(&self) -> Result<(), MegaError> {
78-
let guard = self.unpack_redlock.clone().lock().await?;
93+
let t0 = Instant::now();
94+
let t_fp = Instant::now();
7995
self.traverses_tree_and_update_filepath().await?;
96+
self.receive_pack_extra_timings_ms
97+
.lock()
98+
.expect("receive_pack_extra_timings_ms lock poisoned")
99+
.push((
100+
"import_filepath_update_ms".to_string(),
101+
t_fp.elapsed().as_millis(),
102+
));
103+
104+
let t_attach = Instant::now();
80105
self.attach_to_monorepo_parent().await?;
81-
guard.unlock().await?;
106+
self.receive_pack_extra_timings_ms
107+
.lock()
108+
.expect("receive_pack_extra_timings_ms lock poisoned")
109+
.extend([
110+
(
111+
"import_attach_to_monorepo_parent_ms".to_string(),
112+
t_attach.elapsed().as_millis(),
113+
),
114+
(
115+
"import_finalize_total_ms".to_string(),
116+
t0.elapsed().as_millis(),
117+
),
118+
]);
82119
Ok(())
83120
}
84121

@@ -424,8 +461,18 @@ impl ImportRepo {
424461

425462
// Concurrent attaches need CAS on root mega_refs; retry when head moved.
426463
const MAX_ATTACH_ATTEMPTS: u32 = 64;
464+
let mut root_lock_wait_max_ms: u128 = 0;
465+
let mut root_lock_wait_sum_ms: u128 = 0;
427466

428467
for attempt in 0..MAX_ATTACH_ATTEMPTS {
468+
// Only the root mega_refs update needs cross-repo serialization.
469+
// Keep the lock scope as small as possible: just the root ref read + attach transaction.
470+
let t_lock = Instant::now();
471+
let guard = self.unpack_redlock.clone().lock().await?;
472+
let lock_wait_ms = t_lock.elapsed().as_millis();
473+
root_lock_wait_max_ms = root_lock_wait_max_ms.max(lock_wait_ms);
474+
root_lock_wait_sum_ms += lock_wait_ms;
475+
429476
let root_ref = storage
430477
.get_main_ref("/")
431478
.await?
@@ -479,6 +526,7 @@ impl ImportRepo {
479526
}
480527
}
481528

529+
let t_attach_txn = Instant::now();
482530
match storage
483531
.attach_to_monorepo_parent_in_txn(
484532
&txn,
@@ -492,10 +540,38 @@ impl ImportRepo {
492540
{
493541
Ok(()) => {
494542
txn.commit().await.map_err(MegaError::Db)?;
543+
let t_unlock = Instant::now();
544+
guard.unlock().await?;
545+
self.receive_pack_extra_timings_ms
546+
.lock()
547+
.expect("receive_pack_extra_timings_ms lock poisoned")
548+
.extend([
549+
(
550+
"import_attach_attempts_count".to_string(),
551+
(attempt + 1) as u128,
552+
),
553+
(
554+
"import_root_lock_wait_sum_ms".to_string(),
555+
root_lock_wait_sum_ms,
556+
),
557+
(
558+
"import_root_lock_wait_max_ms".to_string(),
559+
root_lock_wait_max_ms,
560+
),
561+
(
562+
"import_attach_txn_ms".to_string(),
563+
t_attach_txn.elapsed().as_millis(),
564+
),
565+
(
566+
"import_root_lock_unlock_ms".to_string(),
567+
t_unlock.elapsed().as_millis(),
568+
),
569+
]);
495570
return Ok(());
496571
}
497572
Err(MegaError::StaleMonorepoRootRef) if attempt + 1 < MAX_ATTACH_ATTEMPTS => {
498573
let _ = txn.rollback().await;
574+
let _ = guard.unlock().await;
499575
tracing::warn!(
500576
attempt = attempt,
501577
repo_path = %self.repo.repo_path,
@@ -505,6 +581,28 @@ impl ImportRepo {
505581
}
506582
Err(e) => {
507583
let _ = txn.rollback().await;
584+
let _ = guard.unlock().await;
585+
self.receive_pack_extra_timings_ms
586+
.lock()
587+
.expect("receive_pack_extra_timings_ms lock poisoned")
588+
.extend([
589+
(
590+
"import_attach_attempts_count".to_string(),
591+
(attempt + 1) as u128,
592+
),
593+
(
594+
"import_root_lock_wait_sum_ms".to_string(),
595+
root_lock_wait_sum_ms,
596+
),
597+
(
598+
"import_root_lock_wait_max_ms".to_string(),
599+
root_lock_wait_max_ms,
600+
),
601+
(
602+
"import_attach_txn_ms".to_string(),
603+
t_attach_txn.elapsed().as_millis(),
604+
),
605+
]);
508606
return Err(e);
509607
}
510608
}

‎ceres/src/pack/mod.rs‎

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::{
55
Arc,
66
atomic::{AtomicUsize, Ordering},
77
},
8+
time::Instant,
89
};
910

1011
use async_trait::async_trait;
@@ -41,6 +42,20 @@ pub mod monorepo;
4142
pub trait RepoHandler: Send + Sync + 'static {
4243
fn is_monorepo(&self) -> bool;
4344

45+
/// Concurrency limit for `save_entry` batches during receive-pack.
46+
/// `0` means unbounded.
47+
fn save_entry_concurrency(&self) -> usize {
48+
1
49+
}
50+
51+
/// Optional extra timing key-values to be included in the final receive-pack report.
52+
/// Implementations may return an empty vec.
53+
///
54+
/// Intended for repo-type-specific breakdown (e.g. import attach lock waits).
55+
fn receive_pack_extra_timings_ms(&self) -> Vec<(String, u128)> {
56+
Vec::new()
57+
}
58+
4459
/// Replace the handler's ref-command snapshot after unpack and the protocol status loop
4560
/// (`default_branch`, `status`, etc.). The handler is built from an early `commands.clone()`,
4661
/// so without this, metadata updated in `git_receive_pack_stream` would be stale at finalize.
@@ -53,33 +68,72 @@ pub trait RepoHandler: Send + Sync + 'static {
5368
mut rx: UnboundedReceiver<MetaAttached<Entry, EntryMeta>>,
5469
_rx_pack_id: UnboundedReceiver<ObjectHash>,
5570
) -> Result<(), MegaError> {
71+
let t0 = Instant::now();
5672
let mut entry_list = vec![];
57-
let semaphore = Arc::new(Semaphore::new(1)); //这里暂时改动
73+
let limit = self.save_entry_concurrency();
74+
let semaphore = if limit == 0 {
75+
None
76+
} else {
77+
Some(Arc::new(Semaphore::new(limit)))
78+
};
5879
let mut join_tasks = vec![];
5980

6081
//let temp_pack_id = Uuid::new_v4().to_string();
6182
let temp_pack_id = String::new();
6283

84+
let mut total_entries: usize = 0;
85+
let mut flush_batches: usize = 0;
6386
while let Some(mut entry) = rx.recv().await {
6487
self.check_entry(&entry.inner).await?;
6588
entry.meta.set_pack_id(temp_pack_id.clone());
6689
entry_list.push(entry);
90+
total_entries += 1;
6791
if entry_list.len() >= 1000 {
68-
let acquired = semaphore.clone().acquire_owned().await.unwrap();
92+
flush_batches += 1;
6993
let entries = std::mem::take(&mut entry_list);
7094
let shared = self.clone();
95+
let sem = semaphore.clone();
7196
let handle = tokio::spawn(async move {
72-
let _acquired = acquired;
73-
shared.save_entry(entries).await
97+
let _permit = match sem {
98+
Some(s) => Some(s.acquire_owned().await.expect("semaphore closed")),
99+
None => None,
100+
};
101+
let t = Instant::now();
102+
let len = entries.len();
103+
shared.save_entry(entries).await.map(|_| {
104+
tracing::debug!(
105+
service = "git-receive-pack",
106+
batch_entries = len,
107+
elapsed_ms = t.elapsed().as_millis(),
108+
"save_entry batch completed"
109+
);
110+
})
74111
});
75112
join_tasks.push(handle);
76113
}
77114
}
78115
// process left entries
79116
if !entry_list.is_empty() {
117+
flush_batches += 1;
80118
let handler = self.clone();
81119
let entries = std::mem::take(&mut entry_list);
82-
let handle = tokio::spawn(async move { handler.save_entry(entries).await });
120+
let sem = semaphore.clone();
121+
let handle = tokio::spawn(async move {
122+
let _permit = match sem {
123+
Some(s) => Some(s.acquire_owned().await.expect("semaphore closed")),
124+
None => None,
125+
};
126+
let t = Instant::now();
127+
let len = entries.len();
128+
handler.save_entry(entries).await.map(|_| {
129+
tracing::debug!(
130+
service = "git-receive-pack",
131+
batch_entries = len,
132+
elapsed_ms = t.elapsed().as_millis(),
133+
"save_entry batch completed"
134+
);
135+
})
136+
});
83137
join_tasks.push(handle);
84138
}
85139

@@ -120,6 +174,14 @@ pub trait RepoHandler: Send + Sync + 'static {
120174
// }
121175
// }
122176

177+
tracing::debug!(
178+
service = "git-receive-pack",
179+
total_entries = total_entries,
180+
flush_batches = flush_batches,
181+
save_entry_concurrency = limit,
182+
elapsed_ms = t0.elapsed().as_millis(),
183+
"receiver_handler done"
184+
);
123185
Ok(())
124186
}
125187

‎ceres/src/pack/monorepo.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ impl RepoHandler for MonoRepo {
7070
true
7171
}
7272

73+
fn save_entry_concurrency(&self) -> usize {
74+
self.storage.config().pack.save_entry_concurrency
75+
}
76+
7377
fn sync_commands_after_unpack(&self, commands: &[RefCommand]) {
7478
*self
7579
.command_list

‎ceres/src/protocol/mod.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ impl SmartSession {
187187

188188
let unpack_redlock = Arc::new(RedLock::new(
189189
state.git_object_cache.connection.clone(),
190-
"git:receive-pack:lock",
190+
// Serialize monorepo root mega_refs update across concurrent import attaches.
191+
// Filepath updates and per-repo work should not be blocked by this lock.
192+
"git:receive-pack:lock:monorepo-root".to_string(),
191193
30_000, // 30s TTL
192194
));
193195
Ok(Arc::new(ImportRepo {
@@ -196,6 +198,7 @@ impl SmartSession {
196198
repo,
197199
command_list: Mutex::new(commands),
198200
unpack_redlock,
201+
receive_pack_extra_timings_ms: Mutex::new(Vec::new()),
199202
}) as Arc<dyn RepoHandler>)
200203
} else {
201204
let mut res = MonoRepo {

0 commit comments

Comments
 (0)