mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
refactor sync server to use pluggable storage
..with a fixed implementation of the replica / server protocol logic. There isn't much logic yet, and there's a lot of boilerplate to take care of, so this looks a little lopsided, but I'm confident this is the right structure for this code's future.
This commit is contained in:
parent
7472749fee
commit
fb22b9686f
9 changed files with 319 additions and 259 deletions
|
@ -1,11 +1,13 @@
|
|||
use crate::api::{
|
||||
ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER,
|
||||
};
|
||||
use crate::server::{AddVersionResult, ClientId, VersionId};
|
||||
use actix_web::{
|
||||
error, http::StatusCode, post, web, HttpMessage, HttpRequest, HttpResponse, Result,
|
||||
failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER,
|
||||
VERSION_ID_HEADER,
|
||||
};
|
||||
use crate::server::{AddVersionResult, ClientId, HistorySegment, VersionId, NO_VERSION_ID};
|
||||
use crate::storage::{Client, StorageTxn};
|
||||
use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result};
|
||||
use failure::Fallible;
|
||||
use futures::StreamExt;
|
||||
use taskchampion::Uuid;
|
||||
|
||||
/// Max history segment size: 100MB
|
||||
const MAX_SIZE: usize = 100 * 1024 * 1024;
|
||||
|
@ -23,7 +25,7 @@ const MAX_SIZE: usize = 100 * 1024 * 1024;
|
|||
#[post("/client/{client_id}/add-version/{parent_version_id}")]
|
||||
pub(crate) async fn service(
|
||||
req: HttpRequest,
|
||||
data: web::Data<ServerState>,
|
||||
server_state: web::Data<ServerState>,
|
||||
web::Path((client_id, parent_version_id)): web::Path<(ClientId, VersionId)>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse> {
|
||||
|
@ -47,9 +49,18 @@ pub(crate) async fn service(
|
|||
return Err(error::ErrorBadRequest("Empty body"));
|
||||
}
|
||||
|
||||
let result = data
|
||||
.add_version(client_id, parent_version_id, body.to_vec())
|
||||
.map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?;
|
||||
// note that we do not open the transaction until the body has been read
|
||||
// completely, to avoid blocking other storage access while that data is
|
||||
// in transit.
|
||||
let mut txn = server_state.txn().map_err(failure_to_ise)?;
|
||||
|
||||
let client = txn
|
||||
.get_client(client_id)
|
||||
.map_err(failure_to_ise)?
|
||||
.ok_or_else(|| error::ErrorNotFound("no such client"))?;
|
||||
|
||||
let result = add_version(txn, client_id, client, parent_version_id, body.to_vec())
|
||||
.map_err(failure_to_ise)?;
|
||||
Ok(match result {
|
||||
AddVersionResult::Ok(version_id) => HttpResponse::Ok()
|
||||
.header(VERSION_ID_HEADER, version_id.to_string())
|
||||
|
@ -60,14 +71,37 @@ pub(crate) async fn service(
|
|||
})
|
||||
}
|
||||
|
||||
fn add_version<'a>(
|
||||
mut txn: Box<dyn StorageTxn + 'a>,
|
||||
client_id: ClientId,
|
||||
client: Client,
|
||||
parent_version_id: VersionId,
|
||||
history_segment: HistorySegment,
|
||||
) -> Fallible<AddVersionResult> {
|
||||
// check if this version is acceptable, under the protection of the transaction
|
||||
if client.latest_version_id != NO_VERSION_ID && parent_version_id != client.latest_version_id {
|
||||
return Ok(AddVersionResult::ExpectedParentVersion(
|
||||
client.latest_version_id,
|
||||
));
|
||||
}
|
||||
|
||||
// invent a version ID
|
||||
let version_id = Uuid::new_v4();
|
||||
|
||||
// update the DB
|
||||
txn.add_version(client_id, version_id, parent_version_id, history_segment)?;
|
||||
txn.set_client_latest_version_id(client_id, version_id)?;
|
||||
txn.commit()?;
|
||||
|
||||
Ok(AddVersionResult::Ok(version_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::api::ServerState;
|
||||
use crate::app_scope;
|
||||
use crate::server::SyncServer;
|
||||
use crate::test::TestServer;
|
||||
use actix_web::{test, App};
|
||||
use crate::storage::{InMemoryStorage, Storage};
|
||||
use actix_web::{http::StatusCode, test, App};
|
||||
use taskchampion::Uuid;
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -75,13 +109,15 @@ mod test {
|
|||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let server_box: Box<dyn SyncServer> = Box::new(TestServer {
|
||||
expected_client_id: client_id,
|
||||
av_expected_parent_version_id: parent_version_id,
|
||||
av_expected_history_segment: b"abcd".to_vec(),
|
||||
av_result: Some(AddVersionResult::Ok(version_id)),
|
||||
..Default::default()
|
||||
});
|
||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = server_box.txn().unwrap();
|
||||
txn.set_client_latest_version_id(client_id, Uuid::nil())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let server_state = ServerState::new(server_box);
|
||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
||||
|
||||
|
@ -96,10 +132,12 @@ mod test {
|
|||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get("X-Version-Id").unwrap(),
|
||||
&version_id.to_string()
|
||||
);
|
||||
|
||||
// the returned version ID is random, but let's check that it's not
|
||||
// the passed parent version ID, at least
|
||||
let new_version_id = resp.headers().get("X-Version-Id").unwrap();
|
||||
assert!(new_version_id != &version_id.to_string());
|
||||
|
||||
assert_eq!(resp.headers().get("X-Parent-Version-Id"), None);
|
||||
}
|
||||
|
||||
|
@ -108,13 +146,15 @@ mod test {
|
|||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let server_box: Box<dyn SyncServer> = Box::new(TestServer {
|
||||
expected_client_id: client_id,
|
||||
av_expected_parent_version_id: parent_version_id,
|
||||
av_expected_history_segment: b"abcd".to_vec(),
|
||||
av_result: Some(AddVersionResult::ExpectedParentVersion(version_id)),
|
||||
..Default::default()
|
||||
});
|
||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = server_box.txn().unwrap();
|
||||
txn.set_client_latest_version_id(client_id, version_id)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let server_state = ServerState::new(server_box);
|
||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
||||
|
||||
|
@ -140,9 +180,7 @@ mod test {
|
|||
async fn test_bad_content_type() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let server_box: Box<dyn SyncServer> = Box::new(TestServer {
|
||||
..Default::default()
|
||||
});
|
||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server_state = ServerState::new(server_box);
|
||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
||||
|
||||
|
@ -160,9 +198,7 @@ mod test {
|
|||
async fn test_empty_body() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let server_box: Box<dyn SyncServer> = Box::new(TestServer {
|
||||
..Default::default()
|
||||
});
|
||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server_state = ServerState::new(server_box);
|
||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use crate::api::{
|
||||
ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER,
|
||||
failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER,
|
||||
VERSION_ID_HEADER,
|
||||
};
|
||||
use crate::server::{ClientId, VersionId};
|
||||
use actix_web::{error, get, http::StatusCode, web, HttpResponse, Result};
|
||||
use crate::server::{ClientId, GetVersionResult, VersionId};
|
||||
use crate::storage::StorageTxn;
|
||||
use actix_web::{error, get, web, HttpResponse, Result};
|
||||
use failure::Fallible;
|
||||
|
||||
/// Get a child version.
|
||||
///
|
||||
|
@ -14,12 +17,16 @@ use actix_web::{error, get, http::StatusCode, web, HttpResponse, Result};
|
|||
/// Returns other 4xx or 5xx responses on other errors.
|
||||
#[get("/client/{client_id}/get-child-version/{parent_version_id}")]
|
||||
pub(crate) async fn service(
|
||||
data: web::Data<ServerState>,
|
||||
server_state: web::Data<ServerState>,
|
||||
web::Path((client_id, parent_version_id)): web::Path<(ClientId, VersionId)>,
|
||||
) -> Result<HttpResponse> {
|
||||
let result = data
|
||||
.get_child_version(client_id, parent_version_id)
|
||||
.map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?;
|
||||
let mut txn = server_state.txn().map_err(failure_to_ise)?;
|
||||
|
||||
txn.get_client(client_id)
|
||||
.map_err(failure_to_ise)?
|
||||
.ok_or_else(|| error::ErrorNotFound("no such client"))?;
|
||||
|
||||
let result = get_child_version(txn, client_id, parent_version_id).map_err(failure_to_ise)?;
|
||||
if let Some(result) = result {
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(HISTORY_SEGMENT_CONTENT_TYPE)
|
||||
|
@ -34,14 +41,26 @@ pub(crate) async fn service(
|
|||
}
|
||||
}
|
||||
|
||||
fn get_child_version<'a>(
|
||||
mut txn: Box<dyn StorageTxn + 'a>,
|
||||
client_id: ClientId,
|
||||
parent_version_id: VersionId,
|
||||
) -> Fallible<Option<GetVersionResult>> {
|
||||
Ok(txn
|
||||
.get_version_by_parent(client_id, parent_version_id)?
|
||||
.map(|version| GetVersionResult {
|
||||
version_id: version.version_id,
|
||||
parent_version_id: version.parent_version_id,
|
||||
history_segment: version.history_segment,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::api::ServerState;
|
||||
use crate::app_scope;
|
||||
use crate::server::{GetVersionResult, SyncServer};
|
||||
use crate::test::TestServer;
|
||||
use actix_web::{test, App};
|
||||
use crate::storage::{InMemoryStorage, Storage};
|
||||
use actix_web::{http::StatusCode, test, App};
|
||||
use taskchampion::Uuid;
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -49,16 +68,17 @@ mod test {
|
|||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let server_box: Box<dyn SyncServer> = Box::new(TestServer {
|
||||
expected_client_id: client_id,
|
||||
gcv_expected_parent_version_id: parent_version_id,
|
||||
gcv_result: Some(GetVersionResult {
|
||||
version_id: version_id,
|
||||
parent_version_id: parent_version_id,
|
||||
history_segment: b"abcd".to_vec(),
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = server_box.txn().unwrap();
|
||||
txn.set_client_latest_version_id(client_id, Uuid::new_v4())
|
||||
.unwrap();
|
||||
txn.add_version(client_id, version_id, parent_version_id, b"abcd".to_vec())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let server_state = ServerState::new(server_box);
|
||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
||||
|
||||
|
@ -88,15 +108,36 @@ mod test {
|
|||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_not_found() {
|
||||
async fn test_client_not_found() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let server_box: Box<dyn SyncServer> = Box::new(TestServer {
|
||||
expected_client_id: client_id,
|
||||
gcv_expected_parent_version_id: parent_version_id,
|
||||
gcv_result: None,
|
||||
..Default::default()
|
||||
});
|
||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server_state = ServerState::new(server_box);
|
||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
||||
|
||||
let uri = format!(
|
||||
"/client/{}/get-child-version/{}",
|
||||
client_id, parent_version_id
|
||||
);
|
||||
let req = test::TestRequest::get().uri(&uri).to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(resp.headers().get("X-Version-Id"), None);
|
||||
assert_eq!(resp.headers().get("X-Parent-Version-Id"), None);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_version_not_found() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// create the client, but not the version
|
||||
{
|
||||
let mut txn = server_box.txn().unwrap();
|
||||
txn.set_client_latest_version_id(client_id, Uuid::new_v4())
|
||||
.unwrap();
|
||||
}
|
||||
let server_state = ServerState::new(server_box);
|
||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::server::SyncServer;
|
||||
use actix_web::{web, Scope};
|
||||
use crate::storage::Storage;
|
||||
use actix_web::{error, http::StatusCode, web, Scope};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod add_version;
|
||||
|
@ -15,11 +15,16 @@ pub(crate) const VERSION_ID_HEADER: &str = "X-Version-Id";
|
|||
/// The header names for parent version ID
|
||||
pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id";
|
||||
|
||||
/// The type containing a reference to the SyncServer object in the Actix state.
|
||||
pub(crate) type ServerState = Arc<Box<dyn SyncServer>>;
|
||||
/// The type containing a reference to the Storage object in the Actix state.
|
||||
pub(crate) type ServerState = Arc<Box<dyn Storage>>;
|
||||
|
||||
pub(crate) fn api_scope() -> Scope {
|
||||
web::scope("")
|
||||
.service(get_child_version::service)
|
||||
.service(add_version::service)
|
||||
}
|
||||
|
||||
/// Convert a failure::Error to an Actix ISE
|
||||
fn failure_to_ise(err: failure::Error) -> impl actix_web::ResponseError {
|
||||
error::InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
use crate::storage::{InMemoryStorage, Storage};
|
||||
use actix_web::{get, web, App, HttpServer, Responder, Scope};
|
||||
use api::{api_scope, ServerState};
|
||||
use server::{InMemorySyncServer, SyncServer};
|
||||
|
||||
mod api;
|
||||
mod server;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
mod storage;
|
||||
|
||||
// TODO: use hawk to sign requests
|
||||
|
||||
|
@ -27,7 +25,7 @@ pub(crate) fn app_scope(server_state: ServerState) -> Scope {
|
|||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let server_box: Box<dyn SyncServer> = Box::new(InMemorySyncServer::new());
|
||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server_state = ServerState::new(server_box);
|
||||
|
||||
HttpServer::new(move || App::new().service(app_scope(server_state.clone())))
|
||||
|
@ -35,3 +33,22 @@ async fn main() -> std::io::Result<()> {
|
|||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::api::ServerState;
|
||||
use crate::storage::{InMemoryStorage, Storage};
|
||||
use actix_web::{test, App};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_index_get() {
|
||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server_state = ServerState::new(server_box);
|
||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
||||
|
||||
let req = test::TestRequest::get().uri("/").to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert!(resp.status().is_success());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
use super::{
|
||||
AddVersionResult, ClientId, GetVersionResult, HistorySegment, SyncServer, VersionId,
|
||||
NO_VERSION_ID,
|
||||
};
|
||||
use failure::Fallible;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, RwLock};
|
||||
use taskchampion::Uuid;
|
||||
|
||||
/// An in-memory server backend that can be useful for testing.
|
||||
pub(crate) struct InMemorySyncServer {
|
||||
clients: RwLock<HashMap<ClientId, Mutex<Client>>>,
|
||||
}
|
||||
|
||||
struct Version {
|
||||
version_id: VersionId,
|
||||
history_segment: HistorySegment,
|
||||
}
|
||||
|
||||
struct Client {
|
||||
latest_version_id: VersionId,
|
||||
// NOTE: indexed by parent_version_id!
|
||||
versions: HashMap<VersionId, Version>,
|
||||
}
|
||||
|
||||
impl InMemorySyncServer {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
clients: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncServer for InMemorySyncServer {
|
||||
fn get_child_version(
|
||||
&self,
|
||||
client_id: ClientId,
|
||||
parent_version_id: VersionId,
|
||||
) -> Fallible<Option<GetVersionResult>> {
|
||||
let clients = self.clients.read().expect("poisoned lock");
|
||||
if let Some(client) = clients.get(&client_id) {
|
||||
let client = client.lock().expect("poisoned lock");
|
||||
if let Some(version) = client.versions.get(&parent_version_id) {
|
||||
return Ok(Some(GetVersionResult {
|
||||
version_id: version.version_id,
|
||||
parent_version_id,
|
||||
history_segment: version.history_segment.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn add_version(
|
||||
&self,
|
||||
client_id: ClientId,
|
||||
parent_version_id: VersionId,
|
||||
history_segment: HistorySegment,
|
||||
) -> Fallible<AddVersionResult> {
|
||||
let mut clients = self.clients.write().expect("poisoned lock");
|
||||
if let Some(client) = clients.get_mut(&client_id) {
|
||||
let mut client = client.lock().expect("poisoned lock");
|
||||
if client.latest_version_id != NO_VERSION_ID {
|
||||
if parent_version_id != client.latest_version_id {
|
||||
return Ok(AddVersionResult::ExpectedParentVersion(
|
||||
client.latest_version_id,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// invent a new ID for this version
|
||||
let version_id = Uuid::new_v4();
|
||||
|
||||
client.versions.insert(
|
||||
parent_version_id,
|
||||
Version {
|
||||
version_id,
|
||||
history_segment,
|
||||
},
|
||||
);
|
||||
client.latest_version_id = version_id;
|
||||
|
||||
Ok(AddVersionResult::Ok(version_id))
|
||||
} else {
|
||||
// new client, so insert a client with just this new version
|
||||
|
||||
let latest_version_id = Uuid::new_v4();
|
||||
let mut versions = HashMap::new();
|
||||
versions.insert(
|
||||
parent_version_id,
|
||||
Version {
|
||||
version_id: latest_version_id,
|
||||
history_segment,
|
||||
},
|
||||
);
|
||||
|
||||
clients.insert(
|
||||
client_id,
|
||||
Mutex::new(Client {
|
||||
latest_version_id,
|
||||
versions,
|
||||
}),
|
||||
);
|
||||
|
||||
Ok(AddVersionResult::Ok(latest_version_id))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,5 @@
|
|||
use failure::Fallible;
|
||||
use taskchampion::Uuid;
|
||||
|
||||
mod inmemory;
|
||||
|
||||
pub(crate) use inmemory::InMemorySyncServer;
|
||||
|
||||
/// The distinguished value for "no version"
|
||||
pub const NO_VERSION_ID: VersionId = Uuid::nil();
|
||||
|
||||
|
@ -28,18 +23,3 @@ pub(crate) enum AddVersionResult {
|
|||
/// Rejected; expected a version with the given parent version
|
||||
ExpectedParentVersion(VersionId),
|
||||
}
|
||||
|
||||
pub(crate) trait SyncServer: Sync + Send {
|
||||
fn get_child_version(
|
||||
&self,
|
||||
client_id: ClientId,
|
||||
parent_version_id: VersionId,
|
||||
) -> Fallible<Option<GetVersionResult>>;
|
||||
|
||||
fn add_version(
|
||||
&self,
|
||||
client_id: ClientId,
|
||||
parent_version_id: VersionId,
|
||||
history_segment: HistorySegment,
|
||||
) -> Fallible<AddVersionResult>;
|
||||
}
|
||||
|
|
90
sync-server/src/storage/inmemory.rs
Normal file
90
sync-server/src/storage/inmemory.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use super::{Client, Storage, StorageTxn, Uuid, Version};
|
||||
use failure::Fallible;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
struct Inner {
|
||||
/// Clients, indexed by client_id
|
||||
clients: HashMap<Uuid, Client>,
|
||||
|
||||
/// Versions, indexed by (client_id, parent_version_id)
|
||||
versions: HashMap<(Uuid, Uuid), Version>,
|
||||
}
|
||||
|
||||
pub(crate) struct InMemoryStorage(Mutex<Inner>);
|
||||
|
||||
impl InMemoryStorage {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(Mutex::new(Inner {
|
||||
clients: HashMap::new(),
|
||||
versions: HashMap::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
struct InnerTxn<'a>(MutexGuard<'a, Inner>);
|
||||
|
||||
/// In-memory storage for testing and experimentation.
|
||||
///
|
||||
/// NOTE: this does not implement transaction rollback.
|
||||
impl Storage for InMemoryStorage {
|
||||
fn txn<'a>(&'a self) -> Fallible<Box<dyn StorageTxn + 'a>> {
|
||||
Ok(Box::new(InnerTxn(self.0.lock().expect("poisoned lock"))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StorageTxn for InnerTxn<'a> {
|
||||
fn get_client(&mut self, client_id: Uuid) -> Fallible<Option<Client>> {
|
||||
Ok(self.0.clients.get(&client_id).cloned())
|
||||
}
|
||||
|
||||
fn set_client_latest_version_id(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
latest_version_id: Uuid,
|
||||
) -> Fallible<()> {
|
||||
if let Some(client) = self.0.clients.get_mut(&client_id) {
|
||||
client.latest_version_id = latest_version_id;
|
||||
} else {
|
||||
self.0
|
||||
.clients
|
||||
.insert(client_id, Client { latest_version_id });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_version_by_parent(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
) -> Fallible<Option<Version>> {
|
||||
Ok(self
|
||||
.0
|
||||
.versions
|
||||
.get(&(client_id, parent_version_id))
|
||||
.cloned())
|
||||
}
|
||||
|
||||
fn add_version(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
history_segment: Vec<u8>,
|
||||
) -> Fallible<()> {
|
||||
// TODO: verify it doesn't exist (`.entry`?)
|
||||
let version = Version {
|
||||
version_id,
|
||||
parent_version_id,
|
||||
history_segment,
|
||||
};
|
||||
self.0
|
||||
.versions
|
||||
.insert((client_id, version.parent_version_id), version);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commit(&mut self) -> Fallible<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
56
sync-server/src/storage/mod.rs
Normal file
56
sync-server/src/storage/mod.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use failure::Fallible;
|
||||
use taskchampion::Uuid;
|
||||
|
||||
mod inmemory;
|
||||
pub(crate) use inmemory::InMemoryStorage;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Client {
|
||||
pub(crate) latest_version_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Version {
|
||||
pub(crate) version_id: Uuid,
|
||||
pub(crate) parent_version_id: Uuid,
|
||||
pub(crate) history_segment: Vec<u8>,
|
||||
}
|
||||
|
||||
pub(crate) trait StorageTxn {
|
||||
/// Get information about the given client
|
||||
fn get_client(&mut self, client_id: Uuid) -> Fallible<Option<Client>>;
|
||||
|
||||
/// Set the client's latest_version_id (creating the client if necessary)
|
||||
fn set_client_latest_version_id(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
latest_version_id: Uuid,
|
||||
) -> Fallible<()>;
|
||||
|
||||
/// Get a version, indexed by parent version id
|
||||
fn get_version_by_parent(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
) -> Fallible<Option<Version>>;
|
||||
|
||||
/// Add a version (that must not already exist)
|
||||
fn add_version(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
history_segment: Vec<u8>,
|
||||
) -> Fallible<()>;
|
||||
|
||||
/// Commit any changes made in the transaction. It is an error to call this more than
|
||||
/// once. It is safe to skip this call for read-only operations.
|
||||
fn commit(&mut self) -> Fallible<()>;
|
||||
}
|
||||
|
||||
/// A trait for objects able to act as storage. Most of the interesting behavior is in the
|
||||
/// [`crate::storage::StorageTxn`] trait.
|
||||
pub(crate) trait Storage: Send + Sync {
|
||||
/// Begin a transaction
|
||||
fn txn<'a>(&'a self) -> Fallible<Box<dyn StorageTxn + 'a>>;
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
use crate::api::ServerState;
|
||||
use crate::app_scope;
|
||||
use crate::server::{
|
||||
AddVersionResult, ClientId, GetVersionResult, HistorySegment, SyncServer, VersionId,
|
||||
};
|
||||
use actix_web::{test, App};
|
||||
use failure::Fallible;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TestServer {
|
||||
/// test server will panic if this is not given
|
||||
pub expected_client_id: ClientId,
|
||||
|
||||
pub gcv_expected_parent_version_id: VersionId,
|
||||
pub gcv_result: Option<GetVersionResult>,
|
||||
|
||||
pub av_expected_parent_version_id: VersionId,
|
||||
pub av_expected_history_segment: HistorySegment,
|
||||
pub av_result: Option<AddVersionResult>,
|
||||
}
|
||||
|
||||
impl SyncServer for TestServer {
|
||||
fn get_child_version(
|
||||
&self,
|
||||
client_id: ClientId,
|
||||
parent_version_id: VersionId,
|
||||
) -> Fallible<Option<GetVersionResult>> {
|
||||
assert_eq!(client_id, self.expected_client_id);
|
||||
assert_eq!(parent_version_id, self.gcv_expected_parent_version_id);
|
||||
Ok(self.gcv_result.clone())
|
||||
}
|
||||
|
||||
fn add_version(
|
||||
&self,
|
||||
client_id: ClientId,
|
||||
parent_version_id: VersionId,
|
||||
history_segment: HistorySegment,
|
||||
) -> Fallible<AddVersionResult> {
|
||||
assert_eq!(client_id, self.expected_client_id);
|
||||
assert_eq!(parent_version_id, self.av_expected_parent_version_id);
|
||||
assert_eq!(history_segment, self.av_expected_history_segment);
|
||||
Ok(self.av_result.clone().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_index_get() {
|
||||
let server_box: Box<dyn SyncServer> = Box::new(TestServer {
|
||||
..Default::default()
|
||||
});
|
||||
let server_state = ServerState::new(server_box);
|
||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
||||
|
||||
let req = test::TestRequest::get().uri("/").to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert!(resp.status().is_success());
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue