use super::partial_authority_merge_receipt::{
PreparedAuthorityMergeReceipt, load_authority_merge_receipt,
};
use super::partial_merge_analysis::{
NativeMergeAnalysis, PartialMergeBudget, analyze_native_divergence,
};
use super::partial_merge_protocol::{PartialMergeReceipt, PartialMergeRequest};
use crate::LixError;
use crate::changelog::CommitId;
use crate::storage_adapter::{StorageAdapterRead, StoragePrecondition};
pub(crate) enum AuthorityMergePreparation {
AlreadyCommitted(PartialMergeReceipt),
Ready(AuthorityMergePlan),
}
pub(crate) struct AuthorityMergePlan {
repository_id: String,
account_id: String,
request: PartialMergeRequest,
native: NativeMergeAnalysis,
accepted_checkpoint: CommitId,
control_guards: Vec<StoragePrecondition>,
}
fn conflict(message: &str) -> LixError {
LixError::new("LIX_PARTIAL_MERGE_AUTHORITY_CHANGED", message)
}
fn id(value: &str) -> Result<CommitId, LixError> {
CommitId::parse_lix(value, "authority merge coordinate")
}
pub(crate) async fn prepare_authority_merge(
read: &(impl StorageAdapterRead + ?Sized),
repository: &str,
account: &str,
request: PartialMergeRequest,
budget: PartialMergeBudget,
) -> Result<AuthorityMergePreparation, LixError> {
request.validate()?;
if let Some(receipt) = load_authority_merge_receipt(read, repository, account, &request).await?
{
return Ok(AuthorityMergePreparation::AlreadyCommitted(receipt));
}
let identity = crate::gc::NativeUploadAttemptIdentity {
repository_id: repository.into(),
account_id: account.into(),
branch_id: request.branch_id.clone(),
attempt_id: request.attempt_id.clone(),
};
let digest = *blake3::hash(
&serde_json::to_vec(&request).map_err(|_| conflict("merge request encoding failed"))?,
)
.as_bytes();
crate::gc::require_native_upload_attempt(
read,
&identity,
digest,
id(&request.captured_local_head_commit_id)?,
crate::telemetry::unix_time_ms(),
)
.await?;
let ids = [
request.branch_id.clone(),
crate::GLOBAL_BRANCH_ID.to_owned(),
];
let observed = crate::branch::BranchHeadControlContext::new()
.reader(read)
.load_observed(&ids)
.await?;
let global = observed[1]
.control
.as_ref()
.ok_or_else(|| conflict("merge GLOBAL control is absent"))?;
if !super::partial_merge_analysis::catalog_contains(
read,
id(&request.global_head_commit_id)?,
global.head_commit_id,
budget.max_remote_graph_records,
)
.await?
{
return Err(conflict(
"authority GLOBAL no longer contains the captured catalog",
));
}
let selected = observed[0]
.control
.as_ref()
.ok_or_else(|| conflict("merge selected control is absent"))?;
let current_head = selected.head_commit_id;
let current_checkpoint = selected
.working_diff_checkpoint_commit_id
.ok_or_else(|| conflict("merge selected checkpoint is absent"))?;
let captured_remote = super::partial_merge_analysis::record(
read,
id(&request.expected_authority_head_commit_id)?,
false,
)
.await?;
if !super::partial_merge_analysis::incorporated(
read,
&captured_remote,
current_head,
&mut Default::default(),
budget.max_remote_graph_records,
)
.await?
{
return Err(conflict(
"authority no longer incorporates the captured remote frontier",
));
}
let mut accepted_checkpoint =
if request.captured_local_checkpoint_commit_id == request.checkpoint_commit_id {
current_checkpoint
} else {
id(&request.captured_local_checkpoint_commit_id)?
};
if request.captured_local_checkpoint_commit_id != request.checkpoint_commit_id {
let checkpoint = super::partial_merge_analysis::record(
read,
id(&request.captured_local_checkpoint_commit_id)?,
true,
)
.await?;
if !checkpoint.is_checkpoint
|| !super::partial_merge_analysis::bounded_ancestor(
read,
&checkpoint,
id(&request.captured_local_head_commit_id)?,
&mut Default::default(),
budget.max_remote_graph_records,
)
.await?
{
return Err(conflict(
"incoming checkpoint is not retained in the captured working head",
));
}
}
let native = analyze_native_divergence(
read,
id(&request.base_commit_id)?,
current_head,
id(&request.captured_local_head_commit_id)?,
account,
&request.branch_id,
&[
id(&request.checkpoint_commit_id)?,
id(&request.expected_authority_checkpoint_commit_id)?,
current_checkpoint,
],
id(&request.global_head_commit_id)?,
budget,
)
.await?;
if native.already_in_authority {
accepted_checkpoint = current_checkpoint;
}
let control_guards = ids
.iter()
.zip(observed)
.map(|(branch, value)| {
crate::branch::branch_head_control_precondition(branch, value.raw_token)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(AuthorityMergePreparation::Ready(AuthorityMergePlan {
repository_id: repository.into(),
account_id: account.into(),
request,
native,
accepted_checkpoint,
control_guards,
}))
}
impl AuthorityMergePlan {
pub(crate) fn account_id(&self) -> &str {
&self.account_id
}
pub(crate) fn branch_id(&self) -> &str {
&self.request.branch_id
}
pub(crate) fn accepted_checkpoint_commit_id(&self) -> Result<CommitId, LixError> {
Ok(self.accepted_checkpoint)
}
pub(crate) fn application(&self) -> Result<&crate::session::MergeAnalysis, LixError> {
self.native
.application
.as_ref()
.ok_or_else(|| conflict("native application omitted its validated row plan"))
}
pub(crate) fn into_receipt(
self,
merge: CommitId,
) -> Result<PreparedAuthorityMergeReceipt, LixError> {
PreparedAuthorityMergeReceipt::new(
&self.repository_id,
&self.account_id,
self.request,
merge,
self.control_guards,
)
}
}