901 lines
35 KiB
Rust
901 lines
35 KiB
Rust
// Copyright 2018 Mozilla
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
// this file except in compliance with the License. You may obtain a copy of the
|
|
// License at http://www.apache.org/licenses/LICENSE-2.0
|
|
// Unless required by applicable law or agreed to in writing, software distributed
|
|
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations under the License.
|
|
|
|
use std::fmt;
|
|
|
|
use std::collections::HashSet;
|
|
|
|
use uuid::Uuid;
|
|
|
|
use core_traits::{Entid, KnownEntid, TypedValue};
|
|
|
|
use edn::entities::{EntityPlace, LookupRef, TxFunction};
|
|
use edn::PlainSymbol;
|
|
use mentat_db::{entids, timelines, PartitionMap, CORE_SCHEMA_VERSION};
|
|
use mentat_transaction::{InProgress, Queryable, TermBuilder};
|
|
|
|
use mentat_transaction::entity_builder::BuildTerms;
|
|
|
|
use mentat_transaction::query::{QueryInputs, Variable};
|
|
|
|
use crate::bootstrap::BootstrapHelper;
|
|
|
|
use public_traits::errors::Result;
|
|
|
|
use crate::metadata::{PartitionsTable, SyncMetadata};
|
|
use crate::schema::ensure_current_version;
|
|
use crate::tx_mapper::TxMapper;
|
|
use crate::tx_processor::{Processor, TxReceiver};
|
|
use crate::tx_uploader::TxUploader;
|
|
use crate::types::{GlobalTransactionLog, LocalTx, Tx, TxPart};
|
|
use tolstoy_traits::errors::TolstoyError;
|
|
|
|
use crate::logger::d;
|
|
use crate::syncer;
|
|
|
|
pub struct Syncer {}
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub enum SyncFollowup {
|
|
None,
|
|
FullSync,
|
|
}
|
|
|
|
impl fmt::Display for SyncFollowup {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
SyncFollowup::None => write!(f, "None"),
|
|
SyncFollowup::FullSync => write!(f, "Full sync"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub enum SyncReport {
|
|
IncompatibleRemoteBootstrap(i64, i64),
|
|
BadRemoteState(String),
|
|
NoChanges,
|
|
RemoteFastForward,
|
|
LocalFastForward,
|
|
Merge(SyncFollowup),
|
|
}
|
|
|
|
pub enum SyncResult {
|
|
Atomic(SyncReport),
|
|
NonAtomic(Vec<SyncReport>),
|
|
}
|
|
|
|
impl fmt::Display for SyncReport {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
SyncReport::IncompatibleRemoteBootstrap(local, remote) => write!(
|
|
f,
|
|
"Incompatible remote bootstrap transaction version. Local: {}, remote: {}.",
|
|
local, remote
|
|
),
|
|
SyncReport::BadRemoteState(err) => write!(f, "Bad remote state: {}", err),
|
|
SyncReport::NoChanges => write!(f, "Neither local nor remote have any new changes"),
|
|
SyncReport::RemoteFastForward => write!(f, "Fast-forwarded remote"),
|
|
SyncReport::LocalFastForward => write!(f, "Fast-forwarded local"),
|
|
SyncReport::Merge(follow_up) => write!(
|
|
f,
|
|
"Merged local and remote, requesting a follow-up: {}",
|
|
follow_up
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for SyncResult {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
SyncResult::Atomic(report) => write!(f, "Single atomic sync: {}", report),
|
|
SyncResult::NonAtomic(reports) => {
|
|
writeln!(f, "Series of atomic syncs ({})", reports.len())?;
|
|
for report in reports {
|
|
writeln!(f, "{}", report)?;
|
|
}
|
|
writeln!(f, "\\o/")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
enum SyncAction {
|
|
NoOp,
|
|
// TODO this is the same as remote fast-forward from local root.
|
|
// It's currently distinguished from remote fast-forward for a more
|
|
// path through the "first-sync against non-empty remote" flow.
|
|
PopulateRemote,
|
|
RemoteFastForward,
|
|
LocalFastForward,
|
|
// Generic name since we might merge, or rebase, or do something else.
|
|
CombineChanges,
|
|
}
|
|
|
|
/// Represents remote state relative to previous sync.
|
|
/// On first sync, it's always "Changed" unless remote is "Empty".
|
|
pub enum RemoteDataState {
|
|
Empty,
|
|
Changed,
|
|
Unchanged,
|
|
}
|
|
|
|
/// Remote state is expressed in terms of what "remote head" actually is,
|
|
/// and what we think it is.
|
|
impl<'a> From<(&'a Uuid, &'a Uuid)> for RemoteDataState {
|
|
fn from((known_remote_head, actual_remote_head): (&Uuid, &Uuid)) -> RemoteDataState {
|
|
if *actual_remote_head == Uuid::nil() {
|
|
RemoteDataState::Empty
|
|
} else if actual_remote_head != known_remote_head {
|
|
RemoteDataState::Changed
|
|
} else {
|
|
RemoteDataState::Unchanged
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents local state relative to previous sync.
|
|
/// On first sync it's always "Changed".
|
|
/// Local client can't be empty: there's always at least a bootstrap transaction.
|
|
pub enum LocalDataState {
|
|
Changed,
|
|
Unchanged,
|
|
}
|
|
|
|
/// Local state is expressed in terms of presence of a "mapping" for the local head.
|
|
/// Presence of a mapping means that we've uploaded our local head,
|
|
/// indicating that there's no local changes.
|
|
/// Absence of a mapping indicates that local head hasn't been uploaded
|
|
/// and that we have local changes.
|
|
impl From<Option<Uuid>> for LocalDataState {
|
|
fn from(mapped_local_head: Option<Uuid>) -> LocalDataState {
|
|
match mapped_local_head {
|
|
Some(_) => LocalDataState::Unchanged,
|
|
None => LocalDataState::Changed,
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO rename this thing.
|
|
pub struct LocalTxSet {
|
|
txs: Vec<LocalTx>,
|
|
}
|
|
|
|
impl LocalTxSet {
|
|
pub fn new() -> LocalTxSet {
|
|
LocalTxSet { txs: vec![] }
|
|
}
|
|
}
|
|
|
|
impl Default for syncer::LocalTxSet {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl TxReceiver<Vec<LocalTx>> for LocalTxSet {
|
|
fn tx<T>(&mut self, tx_id: Entid, datoms: &mut T) -> Result<()>
|
|
where
|
|
T: Iterator<Item = TxPart>,
|
|
{
|
|
self.txs.push(LocalTx {
|
|
tx: tx_id,
|
|
parts: datoms.collect(),
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
fn done(self) -> Vec<LocalTx> {
|
|
self.txs
|
|
}
|
|
}
|
|
|
|
impl Syncer {
|
|
/// Produces a SyncAction based on local and remote states.
|
|
fn what_do(remote_state: RemoteDataState, local_state: LocalDataState) -> SyncAction {
|
|
match remote_state {
|
|
RemoteDataState::Empty => SyncAction::PopulateRemote,
|
|
|
|
RemoteDataState::Changed => match local_state {
|
|
LocalDataState::Changed => SyncAction::CombineChanges,
|
|
|
|
LocalDataState::Unchanged => SyncAction::LocalFastForward,
|
|
},
|
|
|
|
RemoteDataState::Unchanged => match local_state {
|
|
LocalDataState::Changed => SyncAction::RemoteFastForward,
|
|
|
|
LocalDataState::Unchanged => SyncAction::NoOp,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Upload local txs: (from_tx, HEAD]. Remote head is necessary here because we need to specify
|
|
/// "parent" for each transaction we'll upload; remote head will be first transaction's parent.
|
|
fn fast_forward_remote<R>(
|
|
db_tx: &mut rusqlite::Transaction<'_>,
|
|
from_tx: Option<Entid>,
|
|
remote_client: &mut R,
|
|
remote_head: &Uuid,
|
|
) -> Result<()>
|
|
where
|
|
R: GlobalTransactionLog,
|
|
{
|
|
// TODO consider moving head manipulations into uploader?
|
|
|
|
let report;
|
|
|
|
// Scope to avoid double-borrowing mutable remote_client.
|
|
{
|
|
// Prepare an uploader.
|
|
let uploader = TxUploader::new(
|
|
remote_client,
|
|
remote_head,
|
|
SyncMetadata::get_partitions(db_tx, PartitionsTable::Tolstoy)?,
|
|
);
|
|
// Walk the local transactions in the database and upload them.
|
|
report = Processor::process(db_tx, from_tx, uploader)?;
|
|
}
|
|
|
|
if let Some(last_tx_uploaded) = report.head {
|
|
// Upload remote head.
|
|
remote_client.set_head(&last_tx_uploaded)?;
|
|
|
|
// On success:
|
|
// - persist local mappings from the receiver
|
|
// - update our local "remote head".
|
|
TxMapper::set_lg_mappings(
|
|
db_tx,
|
|
report
|
|
.temp_uuids
|
|
.iter()
|
|
.map(|v| (*v.0, v.1).into())
|
|
.collect(),
|
|
)?;
|
|
|
|
SyncMetadata::set_remote_head(db_tx, &last_tx_uploaded)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn local_tx_for_uuid(db_tx: &rusqlite::Transaction<'_>, uuid: &Uuid) -> Result<Entid> {
|
|
match TxMapper::get_tx_for_uuid(db_tx, uuid)? {
|
|
Some(t) => Ok(t),
|
|
None => bail!(TolstoyError::TxIncorrectlyMapped(0)),
|
|
}
|
|
}
|
|
|
|
fn remote_parts_to_builder(builder: &mut TermBuilder, parts: Vec<TxPart>) -> Result<()> {
|
|
for part in parts {
|
|
let e: EntityPlace<TypedValue>;
|
|
let a = KnownEntid(part.a);
|
|
let v = part.v;
|
|
|
|
// Instead of providing a 'txInstant' datom directly, we map it
|
|
// into a (transaction-tx) style assertion.
|
|
// Transactor knows how to pick out a txInstant value out of these
|
|
// assertions and use that value for the generated transaction's txInstant.
|
|
if part.a == entids::DB_TX_INSTANT {
|
|
e = EntityPlace::TxFunction(TxFunction {
|
|
op: PlainSymbol("transaction-tx".to_string()),
|
|
});
|
|
} else {
|
|
e = KnownEntid(part.e).into();
|
|
}
|
|
|
|
if part.added {
|
|
builder.add(e, a, v)?;
|
|
} else {
|
|
builder.retract(e, a, v)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// In context of a "transaction to be applied", a PartitionMap supplied here
|
|
/// represents what a PartitionMap will be once this transaction is applied.
|
|
/// This works well for regular assertions: entids are supplied, and we need
|
|
/// them to be allocated in the user partition space.
|
|
/// However, we need to decrement 'tx' partition's index, so that the transactor's
|
|
/// allocated tx will match what came off the wire.
|
|
/// N.B.: this depends on absence of holes in the 'tx' partition!
|
|
fn rewind_tx_partition_by_one(partition_map: &mut PartitionMap) -> Result<()> {
|
|
if let Some(tx_part) = partition_map.get_mut(":db.part/tx") {
|
|
assert_eq!(false, tx_part.allow_excision); // Sanity check.
|
|
|
|
let next_entid = tx_part.next_entid() - 1;
|
|
tx_part.set_next_entid(next_entid);
|
|
Ok(())
|
|
} else {
|
|
bail!(TolstoyError::BadRemoteState(
|
|
"Missing tx partition in an incoming transaction".to_string()
|
|
));
|
|
}
|
|
}
|
|
|
|
fn fast_forward_local<'a, 'c>(
|
|
in_progress: &mut InProgress<'a, 'c>,
|
|
txs: Vec<Tx>,
|
|
) -> Result<SyncReport> {
|
|
let mut last_tx = None;
|
|
|
|
for tx in txs {
|
|
let mut builder = TermBuilder::new();
|
|
|
|
// TODO both here and in the merge scenario we're doing the same thing with the partition maps
|
|
// and with the txInstant datom rewriting.
|
|
// Figure out how to combine these operations into a resuable primitive(s).
|
|
// See notes in 'merge' for why we're doing this stuff.
|
|
let mut partition_map = match tx.parts[0].partitions.clone() {
|
|
Some(parts) => parts,
|
|
None => {
|
|
return Ok(SyncReport::BadRemoteState(
|
|
"Missing partition map in incoming transaction".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
// Make space in the provided tx partition for the transaction we're about to create.
|
|
// See function's notes for details.
|
|
Syncer::rewind_tx_partition_by_one(&mut partition_map)?;
|
|
Syncer::remote_parts_to_builder(&mut builder, tx.parts)?;
|
|
|
|
// Allocate space for the incoming entids.
|
|
in_progress.partition_map = partition_map;
|
|
let report = in_progress.transact_builder(builder)?;
|
|
last_tx = Some((report.tx_id, tx.tx));
|
|
}
|
|
|
|
// We've just transacted a new tx, and generated a new tx entid. Map it to the corresponding
|
|
// incoming tx uuid, advance our "locally known remote head".
|
|
if let Some((entid, uuid)) = last_tx {
|
|
SyncMetadata::set_remote_head_and_map(
|
|
&mut in_progress.transaction,
|
|
(entid, &uuid).into(),
|
|
)?;
|
|
}
|
|
|
|
Ok(SyncReport::LocalFastForward)
|
|
}
|
|
|
|
fn merge(
|
|
ip: &mut InProgress<'_, '_>,
|
|
incoming_txs: Vec<Tx>,
|
|
mut local_txs_to_merge: Vec<LocalTx>,
|
|
) -> Result<SyncReport> {
|
|
d(&"Rewinding local transactions.".to_string());
|
|
|
|
// 1) Rewind local to shared root.
|
|
local_txs_to_merge.sort(); // TODO sort at the interface level?
|
|
|
|
let (new_schema, new_partition_map) = timelines::move_from_main_timeline(
|
|
&ip.transaction,
|
|
&ip.schema,
|
|
ip.partition_map.clone(),
|
|
local_txs_to_merge[0].tx..,
|
|
// A poor man's parent reference. This might be brittle, although
|
|
// excisions are prohibited in the 'tx' partition, so this should hold...
|
|
local_txs_to_merge[0].tx - 1,
|
|
)?;
|
|
if let Some(schema) = new_schema {
|
|
ip.schema = schema
|
|
};
|
|
ip.partition_map = new_partition_map;
|
|
|
|
// 2) Transact incoming.
|
|
// 2.1) Prepare remote tx tuples (TermBuilder, PartitionMap, Uuid), which represent
|
|
// a remote transaction, its global identifier and partitions after it's applied.
|
|
d(&"Transacting incoming...".to_string());
|
|
let mut builders = vec![];
|
|
for remote_tx in incoming_txs {
|
|
let mut builder = TermBuilder::new();
|
|
|
|
let partition_map = match remote_tx.parts[0].partitions.clone() {
|
|
Some(parts) => parts,
|
|
None => {
|
|
return Ok(SyncReport::BadRemoteState(
|
|
"Missing partition map in incoming transaction".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
Syncer::remote_parts_to_builder(&mut builder, remote_tx.parts)?;
|
|
|
|
builders.push((builder, partition_map, remote_tx.tx));
|
|
}
|
|
|
|
let mut remote_report = None;
|
|
for (builder, mut partition_map, remote_tx) in builders {
|
|
// Make space in the provided tx partition for the transaction we're about to create.
|
|
// See function's notes for details.
|
|
Syncer::rewind_tx_partition_by_one(&mut partition_map)?;
|
|
|
|
// This allocates our incoming entids in each builder,
|
|
// letting us just use KnownEntid in the builders.
|
|
ip.partition_map = partition_map;
|
|
remote_report = Some((ip.transact_builder(builder)?.tx_id, remote_tx));
|
|
}
|
|
|
|
d(&"Transacting local on top of incoming...".to_string());
|
|
// 3) Rebase local transactions on top of remote.
|
|
let mut clean_rebase = true;
|
|
for local_tx in local_txs_to_merge {
|
|
let mut builder = TermBuilder::new();
|
|
|
|
// This is the beginnings of entity merging.
|
|
|
|
// An entid might be already known to the Schema, or it
|
|
// might be allocated in this transaction.
|
|
// In the former case, refer to it verbatim.
|
|
// In the latter case, rewrite it as a tempid, and let the transactor allocate it.
|
|
let mut entids_that_will_allocate = HashSet::new();
|
|
|
|
// We currently support "strict schema merging": we'll smush attribute definitions,
|
|
// but only if they're the same.
|
|
// e.g. prohibited would be defining different cardinality for the same attribute.
|
|
// Defining new attributes is allowed if:
|
|
// - attribute is defined either on local or remote,
|
|
// - attribute is defined on both local and remote in the same way.
|
|
// Modifying an attribute is currently not supported (requires higher order schema migrations).
|
|
// Note that "same" local and remote attributes might have different entids in the
|
|
// two sets of transactions.
|
|
|
|
// Set of entities that may alter "installed" attribute.
|
|
// Since this is a rebase of local on top of remote, an "installed"
|
|
// attribute might be one that was present in the root, or one that was
|
|
// defined by remote.
|
|
let mut might_alter_installed_attributes = HashSet::new();
|
|
|
|
// Set of entities that describe a new attribute, not present in the root
|
|
// or on the remote.
|
|
let mut will_not_alter_installed_attribute = HashSet::new();
|
|
|
|
// Note that at this point, remote and local have flipped - we're transacting
|
|
// local on top of incoming (which are already in the schema).
|
|
|
|
// Go through local datoms, and classify any schema-altering entids into
|
|
// one of the two sets above.
|
|
for part in &local_tx.parts {
|
|
// If we have an ident definition locally, check if remote
|
|
// already defined this ident. If it did, we'll need to ensure
|
|
// both local and remote are defining it in the same way.
|
|
if part.a == entids::DB_IDENT {
|
|
match part.v {
|
|
TypedValue::Keyword(ref local_kw) => {
|
|
// Remote did not define this ident. Make a note of it,
|
|
// so that we'll know to ignore its attribute datoms.
|
|
if !ip.schema.ident_map.contains_key(local_kw) {
|
|
will_not_alter_installed_attribute.insert(part.e);
|
|
|
|
// Otherwise, we'll need to ensure we have the same attribute definition
|
|
// for it.
|
|
} else {
|
|
might_alter_installed_attributes.insert(part.e);
|
|
}
|
|
}
|
|
_ => panic!("programming error: wrong value type for a local ident"),
|
|
}
|
|
} else if entids::is_a_schema_attribute(part.a)
|
|
&& !will_not_alter_installed_attribute.contains(&part.e)
|
|
{
|
|
might_alter_installed_attributes.insert(part.e);
|
|
}
|
|
}
|
|
|
|
for part in &local_tx.parts {
|
|
match part.a {
|
|
// We'll be ignoring this datom later on (to be generated by the transactor).
|
|
// During a merge we're concerned with entities in the "user" partition,
|
|
// while this falls into the "tx" partition.
|
|
// We have preserved the original txInstant value on the alternate timeline.
|
|
entids::DB_TX_INSTANT => continue,
|
|
|
|
// 'e's will be replaced with tempids, letting transactor handle everything.
|
|
// Non-unique entities are "duplicated". Unique entities are upserted.
|
|
_ => {
|
|
// Retractions never allocated tempids in the transactor.
|
|
if part.added {
|
|
entids_that_will_allocate.insert(part.e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// :db/ident is a db.unique/identity attribute, which means transactor will upsert
|
|
// attribute assertions. E.g. if a new attribute was defined on local and not on remote,
|
|
// it will be inserted. If both local and remote defined the same attribute
|
|
// with different entids, we'll converge and use remote's entid.
|
|
|
|
// Same follows for other types of db.unique/identity attributes.
|
|
// If user-defined attribute is db.unique/identity, we'll "smush" local and remote
|
|
// assertions against it.
|
|
// For example, {:p/name "Grisha"} assertion on local and
|
|
// {:p/name "Grisha"} assertion on remote will result in a single entity.
|
|
|
|
// If user-defined attribute is not unique, however, no smushing will be performed.
|
|
// The above example will result in two entities.
|
|
|
|
for part in local_tx.parts {
|
|
// Skip the "tx instant" datom: it will be generated by our transactor.
|
|
// We don't care about preserving exact state of these datoms: they're already
|
|
// stashed away on the timeline we've created above.
|
|
if part.a == entids::DB_TX_INSTANT {
|
|
continue;
|
|
}
|
|
|
|
let e: EntityPlace<TypedValue>;
|
|
let a = KnownEntid(part.a);
|
|
let v = part.v;
|
|
|
|
// Rewrite entids if they will allocate (see entity merging notes above).
|
|
if entids_that_will_allocate.contains(&part.e) {
|
|
e = builder.named_tempid(format!("{}", part.e)).into();
|
|
// Otherwise, refer to existing entities.
|
|
} else {
|
|
e = KnownEntid(part.e).into();
|
|
}
|
|
|
|
// TODO we need to do the same rewriting for part.v if it's a Ref.
|
|
|
|
// N.b.: attribute can't refer to an unallocated entity, so it's always a KnownEntid.
|
|
// To illustrate, this is not a valid transaction, and will fail ("no entid found for ident: :person/name"):
|
|
// [
|
|
// {:db/ident :person/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one}
|
|
// {:person/name "Grisha"}
|
|
// ]
|
|
// One would need to split that transaction into two,
|
|
// at which point :person/name will refer to an allocated entity.
|
|
|
|
match part.added {
|
|
true => builder.add(e, a, v)?,
|
|
false => {
|
|
if entids_that_will_allocate.contains(&part.e) {
|
|
builder.retract(e, a, v)?;
|
|
continue;
|
|
}
|
|
|
|
// TODO handle tempids in ValuePlace, as well.
|
|
|
|
// Retractions with non-upserting tempids are not currently supported.
|
|
// We work around this by using a lookup-ref instead of the entity tempid.
|
|
// However:
|
|
// - lookup-ref can only be used for attributes which are :db/unique,
|
|
// - a lookup-ref must resolve. If it doesn't, our transaction will fail.
|
|
// And so:
|
|
// - we skip retractions of non-unique attributes,
|
|
// - we "pre-run" a lookup-ref to ensure it will resolve,
|
|
// and skip the retraction otherwise.
|
|
match ip.schema.attribute_map.get(&part.a) {
|
|
Some(attributes) => {
|
|
// A lookup-ref using a non-unique attribute will fail.
|
|
// Skip this retraction, since we can't make sense of it.
|
|
if attributes.unique.is_none() {
|
|
continue;
|
|
}
|
|
}
|
|
None => panic!(
|
|
"programming error: missing attribute map for a known attribute"
|
|
),
|
|
}
|
|
|
|
// TODO prepare a query and re-use it for all retractions of this type
|
|
let pre_lookup = ip.q_once(
|
|
"[:find ?e . :in ?a ?v :where [?e ?a ?v]]",
|
|
QueryInputs::with_value_sequence(vec![
|
|
(Variable::from_valid_name("?a"), a.into()),
|
|
(Variable::from_valid_name("?v"), v.clone()),
|
|
]),
|
|
)?;
|
|
|
|
if pre_lookup.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// TODO just use the value from the query instead of doing _another_ lookup-ref!
|
|
|
|
builder.retract(
|
|
EntityPlace::LookupRef(LookupRef {
|
|
a: a.into(),
|
|
v: v.clone(),
|
|
}),
|
|
a,
|
|
v,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// After all these checks, our builder might be empty: short-circuit.
|
|
if builder.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
d(&"Savepoint before transacting a local tx...".to_string());
|
|
ip.savepoint("speculative_local")?;
|
|
|
|
d(&format!(
|
|
"Transacting builder filled with local txs... {:?}",
|
|
builder
|
|
));
|
|
|
|
let report = ip.transact_builder(builder)?;
|
|
|
|
// Let's check that we didn't modify any schema attributes.
|
|
// Our current attribute map in the schema isn't rich enough to allow
|
|
// for this check: it's missing a notion of "attribute absence" - we can't
|
|
// distinguish between a missing attribute and a default value.
|
|
// Instead, we simply query the database, checking if transaction produced
|
|
// any schema-altering datoms.
|
|
for e in might_alter_installed_attributes.iter() {
|
|
if let Some(resolved_e) = report.tempids.get(&format!("{}", e)) {
|
|
if SyncMetadata::has_entity_assertions_in_tx(
|
|
&ip.transaction,
|
|
*resolved_e,
|
|
report.tx_id,
|
|
)? {
|
|
bail!(TolstoyError::NotYetImplemented(
|
|
"Can't sync with schema alterations yet.".to_string()
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
if !SyncMetadata::is_tx_empty(&ip.transaction, report.tx_id)? {
|
|
d(&format!("tx {} is not a no-op", report.tx_id));
|
|
clean_rebase = false;
|
|
ip.release_savepoint("speculative_local")?;
|
|
} else {
|
|
d(&format!(
|
|
"Applied tx {} as a no-op. Rolling back the savepoint (empty tx clean-up).",
|
|
report.tx_id
|
|
));
|
|
ip.rollback_savepoint("speculative_local")?;
|
|
}
|
|
}
|
|
|
|
// TODO
|
|
// At this point, we've rebased local transactions on top of remote.
|
|
// This would be a good point to create a "merge commit" and upload our loosing timeline.
|
|
|
|
// Since we don't upload during a merge (instead, we request a follow-up sync),
|
|
// set the locally known remote HEAD to what we received from the 'remote'.
|
|
if let Some((entid, uuid)) = remote_report {
|
|
SyncMetadata::set_remote_head_and_map(&mut ip.transaction, (entid, &uuid).into())?;
|
|
}
|
|
|
|
// If necessary, request a full sync as a follow-up to fast-forward remote.
|
|
if clean_rebase {
|
|
Ok(SyncReport::Merge(SyncFollowup::None))
|
|
} else {
|
|
Ok(SyncReport::Merge(SyncFollowup::FullSync))
|
|
}
|
|
}
|
|
|
|
fn first_sync_against_non_empty<R>(
|
|
ip: &mut InProgress<'_, '_>,
|
|
remote_client: &R,
|
|
local_metadata: &SyncMetadata,
|
|
) -> Result<SyncReport>
|
|
where
|
|
R: GlobalTransactionLog,
|
|
{
|
|
d(&"remote non-empty on first sync, adopting remote state.".to_string());
|
|
|
|
// 1) Download remote transactions.
|
|
let incoming_txs = remote_client.transactions_after(&Uuid::nil())?;
|
|
if incoming_txs.is_empty() {
|
|
return Ok(SyncReport::BadRemoteState(
|
|
"Remote specified non-root HEAD but gave no transactions".to_string(),
|
|
));
|
|
}
|
|
|
|
// 2) Process remote bootstrap.
|
|
let remote_bootstrap = &incoming_txs[0];
|
|
let local_bootstrap = local_metadata.root;
|
|
let bootstrap_helper = BootstrapHelper::new(remote_bootstrap);
|
|
|
|
if !bootstrap_helper.is_compatible()? {
|
|
return Ok(SyncReport::IncompatibleRemoteBootstrap(
|
|
CORE_SCHEMA_VERSION as i64,
|
|
bootstrap_helper.core_schema_version()?,
|
|
));
|
|
}
|
|
|
|
d(&format!(
|
|
"mapping incoming bootstrap tx uuid to local bootstrap entid: {} -> {}",
|
|
remote_bootstrap.tx, local_bootstrap
|
|
));
|
|
|
|
// Map incoming bootstrap tx uuid to local bootstrap entid.
|
|
// If there's more work to do, we'll move the head again.
|
|
SyncMetadata::set_remote_head_and_map(
|
|
&mut ip.transaction,
|
|
(local_bootstrap, &remote_bootstrap.tx).into(),
|
|
)?;
|
|
|
|
// 3) Determine new local and remote data states, now that bootstrap has been dealt with.
|
|
let remote_state = if incoming_txs.len() > 1 {
|
|
RemoteDataState::Changed
|
|
} else {
|
|
RemoteDataState::Unchanged
|
|
};
|
|
|
|
let local_state = if local_metadata.root != local_metadata.head {
|
|
LocalDataState::Changed
|
|
} else {
|
|
LocalDataState::Unchanged
|
|
};
|
|
|
|
// 4) The rest of this flow isn't that special anymore.
|
|
// Since we've "merged" with the remote bootstrap, the "no-op" and
|
|
// "local fast-forward" cases are reported as merges.
|
|
match Syncer::what_do(remote_state, local_state) {
|
|
SyncAction::NoOp => Ok(SyncReport::Merge(SyncFollowup::None)),
|
|
|
|
SyncAction::PopulateRemote => {
|
|
// This is a programming error.
|
|
bail!(TolstoyError::UnexpectedState(
|
|
"Remote state can't be empty on first sync against non-empty remote"
|
|
.to_string()
|
|
))
|
|
}
|
|
|
|
SyncAction::RemoteFastForward => {
|
|
bail!(TolstoyError::NotYetImplemented("TODO fast-forward remote on first sync when remote is just bootstrap and local has more".to_string()))
|
|
}
|
|
|
|
SyncAction::LocalFastForward => {
|
|
Syncer::fast_forward_local(ip, incoming_txs[1..].to_vec())?;
|
|
Ok(SyncReport::Merge(SyncFollowup::None))
|
|
}
|
|
|
|
SyncAction::CombineChanges => {
|
|
let local_txs = Processor::process(
|
|
&ip.transaction,
|
|
Some(local_metadata.root),
|
|
LocalTxSet::new(),
|
|
)?;
|
|
Syncer::merge(ip, incoming_txs[1..].to_vec(), local_txs)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn sync<R>(ip: &mut InProgress<'_, '_>, remote_client: &mut R) -> Result<SyncReport>
|
|
where
|
|
R: GlobalTransactionLog,
|
|
{
|
|
d(&"sync flowing".to_string());
|
|
|
|
ensure_current_version(&mut ip.transaction)?;
|
|
|
|
let remote_head = remote_client.head()?;
|
|
d(&format!("remote head {:?}", remote_head));
|
|
|
|
let locally_known_remote_head = SyncMetadata::remote_head(&ip.transaction)?;
|
|
d(&format!("local head {:?}", locally_known_remote_head));
|
|
|
|
let (root, head) = SyncMetadata::root_and_head_tx(&ip.transaction)?;
|
|
let local_metadata = SyncMetadata::new(root, head);
|
|
|
|
// impl From ... vs ::new() calls to constuct "state" objects?
|
|
let local_state = TxMapper::get(&ip.transaction, local_metadata.head)?.into();
|
|
let remote_state = (&locally_known_remote_head, &remote_head).into();
|
|
|
|
// Currently, first sync against a non-empty remote is special.
|
|
if locally_known_remote_head == Uuid::nil() && remote_head != Uuid::nil() {
|
|
return Syncer::first_sync_against_non_empty(ip, remote_client, &local_metadata);
|
|
}
|
|
|
|
match Syncer::what_do(remote_state, local_state) {
|
|
SyncAction::NoOp => {
|
|
d(&"local HEAD did not move. Nothing to do!".to_string());
|
|
Ok(SyncReport::NoChanges)
|
|
}
|
|
|
|
SyncAction::PopulateRemote => {
|
|
d(&"empty remote!".to_string());
|
|
Syncer::fast_forward_remote(
|
|
&mut ip.transaction,
|
|
None,
|
|
remote_client,
|
|
&remote_head,
|
|
)?;
|
|
Ok(SyncReport::RemoteFastForward)
|
|
}
|
|
|
|
SyncAction::RemoteFastForward => {
|
|
d(&"local HEAD moved.".to_string());
|
|
let upload_from_tx =
|
|
Syncer::local_tx_for_uuid(&ip.transaction, &locally_known_remote_head)?;
|
|
|
|
d(&"Fast-forwarding the remote.".to_string());
|
|
|
|
// TODO it's possible that we've successfully advanced remote head previously,
|
|
// but failed to advance our own local head. If that's the case, and we can recognize it,
|
|
// our sync becomes just bumping our local head. AFAICT below would currently fail.
|
|
Syncer::fast_forward_remote(
|
|
&mut ip.transaction,
|
|
Some(upload_from_tx),
|
|
remote_client,
|
|
&remote_head,
|
|
)?;
|
|
Ok(SyncReport::RemoteFastForward)
|
|
}
|
|
|
|
SyncAction::LocalFastForward => {
|
|
d(&"fast-forwarding local store.".to_string());
|
|
Syncer::fast_forward_local(
|
|
ip,
|
|
remote_client.transactions_after(&locally_known_remote_head)?,
|
|
)?;
|
|
Ok(SyncReport::LocalFastForward)
|
|
}
|
|
|
|
SyncAction::CombineChanges => {
|
|
d(&"combining changes from local and remote stores.".to_string());
|
|
// Get the starting point for out local set of txs to merge.
|
|
let combine_local_from_tx =
|
|
Syncer::local_tx_for_uuid(&ip.transaction, &locally_known_remote_head)?;
|
|
let local_txs = Processor::process(
|
|
&ip.transaction,
|
|
Some(combine_local_from_tx),
|
|
LocalTxSet::new(),
|
|
)?;
|
|
// Merge!
|
|
Syncer::merge(
|
|
ip,
|
|
// Remote txs to merge...
|
|
remote_client.transactions_after(&locally_known_remote_head)?,
|
|
// ... with the local txs.
|
|
local_txs,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_what_do() {
|
|
assert_eq!(
|
|
SyncAction::PopulateRemote,
|
|
Syncer::what_do(RemoteDataState::Empty, LocalDataState::Unchanged)
|
|
);
|
|
assert_eq!(
|
|
SyncAction::PopulateRemote,
|
|
Syncer::what_do(RemoteDataState::Empty, LocalDataState::Changed)
|
|
);
|
|
|
|
assert_eq!(
|
|
SyncAction::NoOp,
|
|
Syncer::what_do(RemoteDataState::Unchanged, LocalDataState::Unchanged)
|
|
);
|
|
assert_eq!(
|
|
SyncAction::RemoteFastForward,
|
|
Syncer::what_do(RemoteDataState::Unchanged, LocalDataState::Changed)
|
|
);
|
|
|
|
assert_eq!(
|
|
SyncAction::LocalFastForward,
|
|
Syncer::what_do(RemoteDataState::Changed, LocalDataState::Unchanged)
|
|
);
|
|
assert_eq!(
|
|
SyncAction::CombineChanges,
|
|
Syncer::what_do(RemoteDataState::Changed, LocalDataState::Changed)
|
|
);
|
|
}
|
|
}
|