Skip to content

Commit 7f27ea4

Browse files
authored
fix: thirdparty repo import ref inconsistency (#2042)
1 parent ddc3ec3 commit 7f27ea4

45 files changed

Lines changed: 895 additions & 472 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 321 additions & 229 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ lettre = { version = "0.11.19", default-features = false, features = [
7474
"rustls-platform-verifier"
7575
] }
7676
#====
77-
sea-orm = "1.1.19"
78-
sea-orm-migration = "1.1.19"
77+
sea-orm = "1.1.20"
78+
sea-orm-migration = "1.1.20"
7979

8080
#====
8181
rand = "0.10.0"
@@ -84,10 +84,10 @@ smallvec = "1.15.1"
8484
bytes = "1.11.1"
8585
chrono = { version = "0.4.44", features = ["serde"] }
8686
hex = "0.4.3"
87-
sha1 = "0.10"
88-
sha2 = "0.10.9"
87+
sha1 = "0.11"
88+
sha2 = "0.11"
8989
rsa = "0.9.10"
90-
hmac = "0.12"
90+
hmac = "0.13"
9191

9292
idgenerator = "2.0.0"
9393
config = "0.15.22"

ceres/Cargo.toml

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,26 @@ saturn = { workspace = true }
2222
anyhow = { workspace = true }
2323
# TODO: Use 0.16.0 version for GPG Verification, consider to adapt the latest version in the future
2424
pgp = { version = "0.16.0" }
25-
tokio = { workspace = true, features = ["net", "process"] }
26-
tokio-stream = { workspace = true }
27-
axum = { workspace = true }
28-
reqwest = { workspace = true, features = ["json", "stream"] }
29-
tracing = { workspace = true }
30-
serde = { workspace = true }
31-
serde_json = { workspace = true }
25+
async-trait = { workspace = true }
26+
bytes = { workspace = true }
3227
chrono = { workspace = true }
3328
futures = { workspace = true }
34-
bytes = { workspace = true }
35-
async-trait = { workspace = true }
29+
hex = { workspace = true }
30+
rand = { workspace = true, features = ["thread_rng"] }
31+
serde = { workspace = true, features = ["derive"] }
32+
serde_json = { workspace = true }
33+
sha1 = { workspace = true }
34+
tracing = { workspace = true }
35+
uuid = { workspace = true, features = ["v4"] }
36+
reqwest = { workspace = true, features = ["json", "stream"] }
37+
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "process"] }
38+
tokio-stream = { workspace = true }
39+
axum = { workspace = true }
3640
async-recursion = { workspace = true }
37-
rand = { workspace = true }
3841
sysinfo = { workspace = true }
3942
utoipa = { workspace = true }
4043
regex = { workspace = true }
4144
tokio-util = { workspace = true }
42-
uuid = { workspace = true, features = ["v4"] }
43-
redis = { workspace = true }
44-
sha1 = { workspace = true }
45-
hex = { workspace = true }
4645
rkyv = { workspace = true }
4746

4847
[dev-dependencies]

ceres/src/api_service/admin_ops.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
1212
use common::errors::MegaError;
1313
use git_internal::internal::object::tree::Tree;
14-
use jupiter::utils::converter::FromMegaModel;
15-
use redis::AsyncCommands;
14+
use jupiter::{redis::AsyncCommands, utils::converter::FromMegaModel};
1615

1716
use crate::api_service::mono_api_service::MonoApiService;
1817

ceres/src/api_service/blob_ops.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use git_internal::{
1111
hash::ObjectHash,
1212
internal::object::{blob::Blob, tree::TreeItemMode},
1313
};
14-
use hex;
1514
use sha1::{Digest, Sha1};
1615

1716
use crate::{

ceres/src/api_service/cache.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use git_internal::{
88
tree::{ArchivedTree, Tree},
99
},
1010
};
11-
use redis::{AsyncCommands, aio::ConnectionManager};
11+
use jupiter::redis::{AsyncCommands, ConnectionManager};
1212
use rkyv::rancor::Error;
1313

1414
#[derive(Clone)]

ceres/src/api_service/commit_ops.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use git_internal::{
1717
tree::{TreeItem, TreeItemMode},
1818
},
1919
};
20-
use redis::AsyncCommands;
20+
use jupiter::redis::AsyncCommands;
2121
use serde::{Deserialize, Serialize};
2222

2323
use crate::{

ceres/src/api_service/mono_api_service.rs

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3008,9 +3008,6 @@ impl MonoApiService {
30083008
.await
30093009
.map_err(|e| MegaError::Other(format!("{e}")))?;
30103010

3011-
self.save_import_ref(&mega_path, &ref_name, &ref_hash)
3012-
.await?;
3013-
30143011
let repo_path_str = mega_path
30153012
.to_str()
30163013
.ok_or_else(|| MegaError::Other("Invalid UTF-8 in mega_path".to_string()))?
@@ -3054,30 +3051,6 @@ impl MonoApiService {
30543051
Ok(bytes)
30553052
}
30563053

3057-
async fn save_import_ref(
3058-
&self,
3059-
mega_path: &Path,
3060-
ref_name: &str,
3061-
ref_id: &str,
3062-
) -> Result<(), MegaError> {
3063-
let path = mega_path
3064-
.to_str()
3065-
.ok_or_else(|| MegaError::Other("Invalid UTF-8 in mega_path".to_string()))?;
3066-
3067-
let name = mega_path
3068-
.file_name()
3069-
.and_then(|n| n.to_str())
3070-
.ok_or_else(|| {
3071-
MegaError::Other("Failed to extract file name from mega_path".to_string())
3072-
})?;
3073-
3074-
self.storage
3075-
.git_db_storage()
3076-
.create_repo_and_save_ref(path, name, ref_name, ref_id)
3077-
.await?;
3078-
Ok(())
3079-
}
3080-
30813054
async fn traverse_tree(
30823055
&self,
30833056
root_tree: Tree,

ceres/src/pack/import_repo.rs

Lines changed: 127 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ use std::{
33
path::PathBuf,
44
str::FromStr,
55
sync::{
6-
Arc,
6+
Arc, Mutex,
77
atomic::{AtomicUsize, Ordering},
88
},
99
};
1010

1111
use async_recursion::async_recursion;
1212
use async_trait::async_trait;
1313
use callisto::sea_orm_active_enums::RefTypeEnum;
14-
use common::errors::MegaError;
14+
use common::{errors::MegaError, utils::ZERO_ID};
1515
use futures::{StreamExt, TryStreamExt};
1616
use git_internal::{
1717
errors::GitError,
@@ -44,7 +44,7 @@ use crate::{
4444
pub struct ImportRepo {
4545
pub storage: Storage,
4646
pub repo: Repo,
47-
pub command_list: Vec<RefCommand>,
47+
pub command_list: Mutex<Vec<RefCommand>>,
4848
pub unpack_redlock: Arc<RedLock>,
4949
pub git_object_cache: Arc<GitObjectCache>,
5050
}
@@ -55,6 +55,13 @@ impl RepoHandler for ImportRepo {
5555
false
5656
}
5757

58+
fn sync_commands_after_unpack(&self, commands: &[RefCommand]) {
59+
*self
60+
.command_list
61+
.lock()
62+
.expect("command_list lock poisoned") = commands.to_vec();
63+
}
64+
5865
async fn refs_with_head_hash(&self) -> (String, Vec<Refs>) {
5966
let result = self
6067
.storage
@@ -67,7 +74,7 @@ impl RepoHandler for ImportRepo {
6774
self.find_head_hash(refs)
6875
}
6976

70-
async fn post_receive_pack(&self) -> Result<(), MegaError> {
77+
async fn finalize_receive_pack(&self) -> Result<(), MegaError> {
7178
let guard = self.unpack_redlock.clone().lock().await?;
7279
self.traverses_tree_and_update_filepath().await?;
7380
self.attach_to_monorepo_parent().await?;
@@ -280,23 +287,27 @@ impl RepoHandler for ImportRepo {
280287
}
281288

282289
async fn update_refs(&self, refs: &RefCommand) -> Result<(), GitError> {
290+
if refs.ref_type != RefTypeEnum::Tag {
291+
// Branch `import_refs` rows are written in the same DB transaction as monorepo attach.
292+
return Ok(());
293+
}
283294
let storage = self.storage.git_db_storage();
284295
match refs.command_type {
285296
CommandType::Create => {
286297
storage
287298
.save_ref(self.repo.repo_id, refs.clone().into())
288299
.await
289-
.unwrap();
300+
.map_err(|e| GitError::CustomError(e.to_string()))?;
290301
}
291302
CommandType::Delete => storage
292303
.remove_ref(self.repo.repo_id, &refs.ref_name)
293304
.await
294-
.unwrap(),
305+
.map_err(|e| GitError::CustomError(e.to_string()))?,
295306
CommandType::Update => {
296307
storage
297308
.update_ref(self.repo.repo_id, &refs.ref_name, &refs.new_id)
298309
.await
299-
.unwrap();
310+
.map_err(|e| GitError::CustomError(e.to_string()))?;
300311
}
301312
}
302313
Ok(())
@@ -320,8 +331,22 @@ impl RepoHandler for ImportRepo {
320331
}
321332

322333
async fn traverses_tree_and_update_filepath(&self) -> Result<(), MegaError> {
323-
//let (current_head, refs) = self.head_hash().await;
324-
let (current_head, _refs) = self.refs_with_head_hash().await;
334+
// Prefer the branch tip from this receive-pack (same as `attach_to_monorepo_parent`).
335+
// DB `import_refs` is not updated until the attach transaction, so reading HEAD only
336+
// from the DB would still see the pre-push tip during finalize.
337+
let from_commands = {
338+
let cmds = self
339+
.command_list
340+
.lock()
341+
.expect("command_list lock poisoned");
342+
cmds.iter()
343+
.find(|c| c.ref_type == RefTypeEnum::Branch && c.new_id != ZERO_ID)
344+
.map(|c| c.new_id.clone())
345+
};
346+
let current_head = match from_commands {
347+
Some(h) => h,
348+
None => self.refs_with_head_hash().await.0,
349+
};
325350
let commit = Commit::from_git_model(
326351
self.storage
327352
.git_db_storage()
@@ -379,53 +404,115 @@ impl ImportRepo {
379404

380405
// attach import repo to monorepo parent tree
381406
pub(crate) async fn attach_to_monorepo_parent(&self) -> Result<(), MegaError> {
382-
// 1. find branch command
383-
let commit_id = match self
407+
// Snapshot commands without holding the mutex across await (Send + avoids deadlocks).
408+
let commands_snapshot: Vec<RefCommand> = self
384409
.command_list
410+
.lock()
411+
.expect("command_list lock poisoned")
412+
.clone();
413+
let commit_id = match commands_snapshot
385414
.iter()
386415
.find(|c| c.ref_type == RefTypeEnum::Branch)
387416
{
388417
Some(cmd) => cmd.new_id.clone(),
389418
None => return Ok(()),
390419
};
391420

392-
// 2. search and create tree
393421
let path = PathBuf::from(self.repo.repo_path.clone());
394-
395422
let mono_api_service: MonoApiService = self.into();
396423
let storage = self.storage.mono_storage();
397-
let save_trees = tree_ops::search_and_create_tree(&mono_api_service, &path).await?;
398424

399-
// 3. get root ref
400-
let root_ref = storage
401-
.get_main_ref("/")
402-
.await?
403-
.ok_or_else(|| MegaError::Other("root ref not found".to_string()))?;
425+
// Concurrent attaches need CAS on root mega_refs; retry when head moved.
426+
const MAX_ATTACH_ATTEMPTS: u32 = 64;
404427

405-
// 4. get latest commit
406-
let latest_commit: Commit = Commit::from_git_model(
407-
self.storage
408-
.git_db_storage()
409-
.get_commit_by_hash(self.repo.repo_id, &commit_id)
428+
for attempt in 0..MAX_ATTACH_ATTEMPTS {
429+
let root_ref = storage
430+
.get_main_ref("/")
410431
.await?
411-
.ok_or_else(|| MegaError::Other(format!("commit {} not found", commit_id)))?,
412-
);
432+
.ok_or_else(|| MegaError::Other("root ref not found".to_string()))?;
433+
let expected_commit = root_ref.ref_commit_hash.clone();
434+
let expected_tree = root_ref.ref_tree_hash.clone();
435+
let root_ref_id = root_ref.id;
413436

414-
// 5. generate commit
415-
let commit_msg = latest_commit.format_message();
416-
let new_commit = Commit::from_tree_id(
417-
save_trees
418-
.back()
419-
.ok_or_else(|| MegaError::Other("no tree generated".to_string()))?
420-
.id,
421-
vec![ObjectHash::from_str(&root_ref.ref_commit_hash).unwrap()],
422-
&format!("\n{commit_msg}"),
423-
);
437+
let save_trees = tree_ops::search_and_create_tree(&mono_api_service, &path).await?;
424438

425-
storage
426-
.attach_to_monorepo_parent_with_txn(root_ref, new_commit, save_trees.into())
427-
.await?;
428-
Ok(())
439+
let latest_commit: Commit = Commit::from_git_model(
440+
self.storage
441+
.git_db_storage()
442+
.get_commit_by_hash(self.repo.repo_id, &commit_id)
443+
.await?
444+
.ok_or_else(|| MegaError::Other(format!("commit {} not found", commit_id)))?,
445+
);
446+
447+
let commit_msg = latest_commit.format_message();
448+
let new_commit = Commit::from_tree_id(
449+
save_trees
450+
.back()
451+
.ok_or_else(|| MegaError::Other("no tree generated".to_string()))?
452+
.id,
453+
vec![ObjectHash::from_str(&expected_commit).unwrap()],
454+
&format!("\n{commit_msg}"),
455+
);
456+
457+
let txn = self.storage.begin_db_transaction().await?;
458+
let git_db = self.storage.git_db_storage();
459+
for cmd in &commands_snapshot {
460+
if cmd.ref_type != RefTypeEnum::Branch {
461+
continue;
462+
}
463+
match cmd.command_type {
464+
CommandType::Create => {
465+
git_db
466+
.save_ref_in_txn(self.repo.repo_id, cmd.clone().into(), &txn)
467+
.await?;
468+
}
469+
CommandType::Delete => {
470+
git_db
471+
.remove_ref_in_txn(self.repo.repo_id, &cmd.ref_name, &txn)
472+
.await?;
473+
}
474+
CommandType::Update => {
475+
git_db
476+
.update_ref_in_txn(self.repo.repo_id, &cmd.ref_name, &cmd.new_id, &txn)
477+
.await?;
478+
}
479+
}
480+
}
481+
482+
match storage
483+
.attach_to_monorepo_parent_in_txn(
484+
&txn,
485+
root_ref_id,
486+
&expected_commit,
487+
&expected_tree,
488+
new_commit,
489+
save_trees.into(),
490+
)
491+
.await
492+
{
493+
Ok(()) => {
494+
txn.commit().await.map_err(MegaError::Db)?;
495+
return Ok(());
496+
}
497+
Err(MegaError::StaleMonorepoRootRef) if attempt + 1 < MAX_ATTACH_ATTEMPTS => {
498+
let _ = txn.rollback().await;
499+
tracing::warn!(
500+
attempt = attempt,
501+
repo_path = %self.repo.repo_path,
502+
"attach_to_monorepo_parent: root ref moved, retrying"
503+
);
504+
tokio::task::yield_now().await;
505+
}
506+
Err(e) => {
507+
let _ = txn.rollback().await;
508+
return Err(e);
509+
}
510+
}
511+
}
512+
513+
Err(MegaError::Other(
514+
"attach_to_monorepo_parent: exceeded retry limit for concurrent root updates".into(),
515+
))
429516
}
430517
}
431518

0 commit comments

Comments
 (0)