use crate::{
document::C4DocumentOwner,
error::{c4error_init, Error, Result},
ffi::{
c4doc_resolveConflict, c4doc_save, c4doc_selectNextLeafRevision, C4DocContentLevel,
C4RevisionFlags, FLSlice, FLSlice_Compare,
},
Database,
};
use log::{info, warn};
use std::{os::raw::c_uint, ptr};
pub fn resolve_conflict(db: &mut Database, doc_id: &str) -> Result<()> {
let mut retry_count = 0_u8;
const MAX_RETRY_COUNT: u8 = 10;
loop {
let doc = match db.do_internal_get_opt(doc_id, true, C4DocContentLevel::kDocGetAll)? {
Some(x) => x,
None => {
info!("doc {doc_id} no longer exists, no conflict to resolve");
return Ok(());
}
};
let ok = select_next_conflicting_revision(&doc)?;
if !ok {
info!("conflict in doc {doc_id} already resolved, nothing to do");
return Ok(());
}
let ok = default_resolve_conflict(db, doc_id, &doc)?;
let mut in_conflict = false;
if !ok {
retry_count += 1;
in_conflict = retry_count < MAX_RETRY_COUNT;
if in_conflict {
warn!(
"conflict resolution of doc '{}' conflicted with newer saved, retry {}",
doc_id, retry_count
);
}
}
if !in_conflict {
break;
}
}
Ok(())
}
fn default_resolve_conflict(
db: &mut Database,
doc_id: &str,
conflict: &C4DocumentOwner,
) -> Result<bool> {
let remote_doc = if (conflict.selected_revision().flags & C4RevisionFlags::kRevDeleted)
!= C4RevisionFlags(0)
{
None
} else {
Some(conflict)
};
let local_doc = db
.do_internal_get_opt(doc_id, true, C4DocContentLevel::kDocGetAll)?
.map(|doc| {
if (doc.selected_revision().flags & C4RevisionFlags::kRevDeleted) != C4RevisionFlags(0)
{
None
} else {
Some(doc)
}
})
.unwrap_or(None);
let resolved = default_conflict_resolver(local_doc.as_ref(), remote_doc);
let resolution = if resolved.map(|x| x.0.as_ptr()).unwrap_or(ptr::null_mut())
== remote_doc.map(|x| x.0.as_ptr()).unwrap_or(ptr::null_mut())
{
Resolution::UseRemote
} else {
Resolution::UseLocal
};
do_resolve_conflict(db, conflict, resolution, resolved)
}
fn default_conflict_resolver<'b>(
local_doc: Option<&'b C4DocumentOwner>,
remote_doc: Option<&'b C4DocumentOwner>,
) -> Option<&'b C4DocumentOwner> {
fn selected_rev_gen(doc: &C4DocumentOwner) -> (&[u8], c_uint) {
let rev_id: &[u8] = doc.selected_revision().revID.as_fl_slice().into();
(rev_id, C4DocumentOwner::generation(rev_id))
}
match (local_doc, remote_doc) {
(None, None) | (None, Some(_)) | (Some(_), None) => None,
(Some(local_doc), Some(remote_doc)) => {
let (remote_rev, remote_gen) = selected_rev_gen(remote_doc);
let (local_rev, local_gen) = selected_rev_gen(local_doc);
if remote_gen > local_gen {
Some(remote_doc)
} else if remote_gen < local_gen {
Some(local_doc)
} else if unsafe { FLSlice_Compare(local_rev.into(), remote_rev.into()) } > 0 {
Some(local_doc)
} else {
Some(remote_doc)
}
}
}
}
fn select_next_conflicting_revision(doc: &C4DocumentOwner) -> Result<bool> {
let mut c4err = c4error_init();
while unsafe { c4doc_selectNextLeafRevision(doc.0.as_ptr(), true, true, &mut c4err) } {
if (doc.selected_revision().flags & C4RevisionFlags::kRevIsConflict) != C4RevisionFlags(0) {
return Ok(true);
}
}
if c4err.code == 0 {
Ok(false)
} else {
Err(Error::C4Error(c4err))
}
}
#[derive(Clone, Copy, PartialEq)]
enum Resolution {
UseRemote,
UseLocal,
}
fn do_resolve_conflict(
db: &mut Database,
conflict_doc: &C4DocumentOwner,
resolution: Resolution,
resolved_doc: Option<&C4DocumentOwner>,
) -> Result<bool> {
let tx = db.transaction()?;
let winner = unsafe { conflict_doc.0.as_ref() }
.selectedRev
.revID
.as_fl_slice();
let loser = conflict_doc.revision_id();
let mut merge_flags = C4RevisionFlags(0);
let mut merge_body = FLSlice::default();
if resolution != Resolution::UseRemote {
if let Some(resolved_doc) = resolved_doc {
let body = resolved_doc.load_body()?;
merge_body = body.into();
} else {
merge_flags = C4RevisionFlags::kRevDeleted;
}
}
let mut c4err = c4error_init();
if !unsafe {
c4doc_resolveConflict(
conflict_doc.0.as_ptr(),
winner,
loser.into(),
merge_body,
merge_flags,
&mut c4err,
)
} {
return Err(c4err.into());
}
if !unsafe { c4doc_save(conflict_doc.0.as_ptr(), 0, &mut c4err) } {
return Err(c4err.into());
}
tx.commit()?;
Ok(true)
}