mentat/tolstoy/src/syncer.rs

287 lines
9 KiB
Rust
Raw Normal View History

// 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;
use std::str::FromStr;
use std::collections::HashMap;
use futures::{future, Future, Stream};
use hyper;
use hyper::Client;
use hyper::{Method, Request, StatusCode, Error as HyperError};
use hyper::header::{ContentType};
use rusqlite;
use serde_cbor;
use serde_json;
use tokio_core::reactor::Core;
use uuid::Uuid;
use mentat_core::Entid;
use metadata::SyncMetadataClient;
use metadata::HeadTrackable;
use errors::{
ErrorKind,
Result,
};
use tx_processor::{
Processor,
TxReceiver,
TxPart,
};
use tx_mapper::TxMapper;
static API_VERSION: &str = "0.1";
static BASE_URL: &str = "https://mentat.dev.lcip.org/mentatsync/";
pub struct Syncer {}
struct UploadingTxReceiver<'c> {
pub tx_temp_uuids: HashMap<Entid, Uuid>,
pub is_done: bool,
remote_client: &'c RemoteClient,
remote_head: &'c Uuid,
rolling_temp_head: Option<Uuid>,
}
impl<'c> UploadingTxReceiver<'c> {
fn new(client: &'c RemoteClient, remote_head: &'c Uuid) -> UploadingTxReceiver<'c> {
UploadingTxReceiver {
tx_temp_uuids: HashMap::new(),
remote_client: client,
remote_head: remote_head,
rolling_temp_head: None,
is_done: false
}
}
}
impl<'c> TxReceiver for UploadingTxReceiver<'c> {
fn tx<T>(&mut self, tx_id: Entid, d: &mut T) -> Result<()>
where T: Iterator<Item=TxPart> {
// Yes, we generate a new UUID for a given Tx, even if we might
// already have one mapped locally. Pre-existing local mapping will
// be replaced if this sync succeeds entirely.
// If we're seeing this tx again, it implies that previous attempt
// to sync didn't update our local head. Something went wrong last time,
// and it's unwise to try to re-use these remote tx mappings.
// We just leave garbage txs to be GC'd on the server.
let tx_uuid = Uuid::new_v4();
self.tx_temp_uuids.insert(tx_id, tx_uuid);
let mut tx_chunks = vec![];
// TODO separate bits of network work should be combined into single 'future'
// Upload all chunks.
for datom in d {
let datom_uuid = Uuid::new_v4();
tx_chunks.push(datom_uuid);
self.remote_client.put_chunk(&datom_uuid, serde_cbor::to_vec(&datom)?)?
}
// Upload tx.
// NB: At this point, we may choose to update remote & local heads.
// Depending on how much we're uploading, and how unreliable our connection
// is, this might be a good thing to do to ensure we make at least some progress.
// Comes at a cost of possibly increasing racing against other clients.
match self.rolling_temp_head {
Some(parent) => {
self.remote_client.put_transaction(&tx_uuid, &parent, &tx_chunks)?;
self.rolling_temp_head = Some(tx_uuid.clone());
},
None => self.remote_client.put_transaction(&tx_uuid, self.remote_head, &tx_chunks)?
}
Ok(())
}
fn done(&mut self) -> Result<()> {
self.is_done = true;
Ok(())
}
}
impl Syncer {
pub fn flow(sqlite: &mut rusqlite::Connection, username: String) -> Result<()> {
// Sketch of an upload flow:
// get remote head
// compare with local head
// if the same:
// - upload any local chunks, transactions
// - move server remote head
// - move local remote head
// TODO configure this sync with some auth data
let remote_client = RemoteClient::new(BASE_URL.into(), username);
let mut db_tx = sqlite.transaction()?;
let remote_head = remote_client.get_head()?;
let locally_known_remote_head = SyncMetadataClient::remote_head(&db_tx)?;
// 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 much cheaper.
// Don't know how to download, merge, resolve conflicts, etc yet.
if locally_known_remote_head != remote_head {
bail!(ErrorKind::NotYetImplemented(
format!("Can't yet sync against changed server. Local head {:?}, remote head {:?}", locally_known_remote_head, remote_head)
));
}
// Local and remote heads agree.
// In theory, it should be safe to upload our stuff now.
let mut uploader = UploadingTxReceiver::new(&remote_client, &remote_head);
Processor::process(&db_tx, &mut uploader)?;
if !uploader.is_done {
bail!(ErrorKind::UploadingProcessorUnfinished);
}
// Last tx uuid uploaded by the tx receiver.
// It's going to be our new head.
if let Some(last_tx_uploaded) = uploader.rolling_temp_head {
// Upload remote head.
remote_client.put_head(&last_tx_uploaded)?;
// On succes:
// - persist local mappings from the receiver
// - update our local "remote head".
TxMapper::set_bulk(&mut db_tx, &uploader.tx_temp_uuids)?;
SyncMetadataClient::set_remote_head(&db_tx, &last_tx_uploaded)?;
// Commit everything: tx->uuid mappings and the new HEAD. We're synced!
db_tx.commit()?;
}
Ok(())
}
}
#[derive(Serialize)]
struct SerializedHead<'a> {
head: &'a Uuid
}
#[derive(Serialize)]
struct SerializedTransaction<'a> {
parent: &'a Uuid,
chunks: &'a Vec<Uuid>
}
struct RemoteClient {
base_uri: String,
user_id: String
}
impl RemoteClient {
fn new(base_uri: String, user_id: String) -> Self {
RemoteClient {
base_uri: base_uri,
user_id: user_id
}
}
fn bound_base_uri(&self) -> String {
// TODO escaping
format!("{}/{}/{}", self.base_uri, API_VERSION, self.user_id)
}
fn get_uuid(&self, uri: String) -> Result<Uuid> {
let mut core = Core::new()?;
let client = Client::new(&core.handle());
let uri = uri.parse()?;
let get = client.get(uri).and_then(|res| {
res.body().concat2()
});
let got = core.run(get)?;
Ok(Uuid::from_str(std::str::from_utf8(&got)?)?)
}
fn put<T>(&self, uri: String, payload: T, expected: StatusCode) -> Result<()>
where hyper::Body: std::convert::From<T>,
T: {
let mut core = Core::new()?;
let client = Client::new(&core.handle());
let uri = uri.parse()?;
let mut req = Request::new(Method::Put, uri);
req.headers_mut().set(ContentType::json());
req.set_body(payload);
let put = client.request(req).and_then(|res| {
let status_code = res.status();
if status_code != expected {
future::err(HyperError::Status)
} else {
// body will be empty...
future::ok(())
}
});
core.run(put)?;
Ok(())
}
fn put_transaction(&self, transaction_uuid: &Uuid, parent_uuid: &Uuid, chunks: &Vec<Uuid>) -> Result<()> {
// {"parent": uuid, "chunks": [chunk1, chunk2...]}
let transaction = SerializedTransaction {
parent: parent_uuid,
chunks: chunks
};
let uri = format!("{}/transactions/{}", self.bound_base_uri(), transaction_uuid);
let json = serde_json::to_string(&transaction)?;
self.put(uri, json, StatusCode::Created)
}
fn get_head(&self) -> Result<Uuid> {
let uri = format!("{}/head", self.bound_base_uri());
self.get_uuid(uri)
}
fn put_head(&self, uuid: &Uuid) -> Result<()> {
// {"head": uuid}
let head = SerializedHead {
head: uuid
};
let uri = format!("{}/head", self.bound_base_uri());
let json = serde_json::to_string(&head)?;
self.put(uri, json, StatusCode::NoContent)
}
fn put_chunk(&self, chunk_uuid: &Uuid, payload: Vec<u8>) -> Result<()> {
let uri = format!("{}/chunks/{}", self.bound_base_uri(), chunk_uuid);
self.put(uri, payload, StatusCode::Created)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_remote_client(uri: &str, user_id: &str) -> RemoteClient {
RemoteClient::new(uri.into(), user_id.into())
}
#[test]
fn test_remote_client_bound_uri() {
let remote_client = test_remote_client("https://example.com/api", "test-user");
assert_eq!("https://example.com/api/0.1/test-user", remote_client.bound_base_uri());
}
}