Skip to content

Commit 634cd75

Browse files
committed
Server-initiated mutations
Fixes #42
1 parent 649caaf commit 634cd75

File tree

11 files changed

+251
-141
lines changed

11 files changed

+251
-141
lines changed

lib/sqlsync-worker/sqlsync-wasm/src/utils.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ impl_from_error!(
9595
io::Error,
9696
sqlsync::error::Error,
9797
sqlsync::sqlite::Error,
98-
sqlsync::JournalError,
9998
sqlsync::replication::ReplicationError,
10099
sqlsync::JournalIdParseError,
101100
sqlsync::ReducerError,

lib/sqlsync/examples/end-to-end-local-net.rs

Lines changed: 95 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ use sqlsync::local::NoopSignal;
1616
use sqlsync::replication::ReplicationMsg;
1717
use sqlsync::replication::ReplicationProtocol;
1818
use sqlsync::JournalId;
19-
use sqlsync::Lsn;
2019
use sqlsync::MemoryJournalFactory;
2120
use sqlsync::Reducer;
2221

@@ -141,6 +140,8 @@ fn handle_client(
141140

142141
let mut num_steps = 0;
143142

143+
let mut remaining_direct_mutations = 5;
144+
144145
loop {
145146
let msg = receive_msg(&mut socket_reader)?;
146147
log::info!("server: received {:?}", msg);
@@ -157,6 +158,31 @@ fn handle_client(
157158
log::info!("server: stepping doc (steps: {})", num_steps);
158159
unlock!(|doc| doc.step()?);
159160

161+
// trigger a direct increment on the server side after every message
162+
if remaining_direct_mutations > 0 {
163+
remaining_direct_mutations -= 1;
164+
unlock!(|doc| {
165+
log::info!("server: running a direct mutation on the doc");
166+
doc.mutate_direct(|tx| {
167+
match tx.execute(
168+
"INSERT INTO counter (id, value) VALUES (1, 0)
169+
ON CONFLICT (id) DO UPDATE SET value = value + 1",
170+
[],
171+
) {
172+
Ok(_) => Ok::<_, anyhow::Error>(()),
173+
// ignore missing table error
174+
Err(rusqlite::Error::SqliteFailure(_, Some(msg)))
175+
if msg == "no such table: counter" =>
176+
{
177+
log::info!("server: skipping direct mutation");
178+
Ok(())
179+
}
180+
Err(err) => Err(err)?,
181+
}
182+
})?;
183+
});
184+
}
185+
160186
// sync back to the client if needed
161187
unlock!(|doc| {
162188
if let Some((msg, mut reader)) = protocol.sync(doc)? {
@@ -219,7 +245,16 @@ fn start_client(
219245
let total_mutations = 10 as usize;
220246
let mut remaining_mutations = total_mutations;
221247

248+
// the total number of sync attempts we will make
249+
let total_syncs = 100 as usize;
250+
let mut syncs = 0;
251+
222252
loop {
253+
syncs += 1;
254+
if syncs > total_syncs {
255+
panic!("client({}): too many syncs", timeline_id);
256+
}
257+
223258
let msg = receive_msg(&mut socket_reader)?;
224259
log::info!("client({}): received {:?}", timeline_id, msg);
225260

@@ -248,25 +283,31 @@ fn start_client(
248283
}
249284

250285
log::info!("client({}): QUERYING STATE", timeline_id);
251-
doc.query(|conn| {
252-
conn.query_row("select value from counter", [], |row| {
253-
let value: Option<i32> = row.get(0)?;
254-
log::info!(
255-
"client({}): counter value: {:?}",
256-
timeline_id,
257-
value
258-
);
259-
Ok(())
260-
})?;
261-
262-
Ok::<_, anyhow::Error>(())
286+
let current_value = doc.query(|conn| {
287+
let value = conn.query_row(
288+
"select value from counter where id = 0",
289+
[],
290+
|row| {
291+
let value: Option<usize> = row.get(0)?;
292+
log::info!(
293+
"client({}): counter value: {:?}",
294+
timeline_id,
295+
value
296+
);
297+
Ok(value)
298+
},
299+
)?;
300+
301+
Ok::<_, anyhow::Error>(value)
263302
})?;
264303

265-
if let Some(lsn) = doc.storage_lsn() {
266-
// once the storage has reached (total_mutations+1) * num_clients
267-
// then we have reached the end
268-
log::info!("client({}): storage lsn: {}", timeline_id, lsn);
269-
if lsn >= ((total_mutations * num_clients) + 1) as Lsn {
304+
if let Some(value) = current_value {
305+
log::info!(
306+
"client({}): storage lsn: {:?}",
307+
timeline_id,
308+
doc.storage_lsn()
309+
);
310+
if value == (total_mutations * num_clients) {
270311
break;
271312
}
272313
}
@@ -279,23 +320,47 @@ fn start_client(
279320

280321
// final query, value should be total_mutations * num_clients
281322
doc.query(|conn| {
282-
conn.query_row_and_then("select value from counter", [], |row| {
283-
let value: Option<usize> = row.get(0)?;
284-
log::info!(
285-
"client({}): FINAL counter value: {:?}",
286-
timeline_id,
287-
value
288-
);
289-
if value != Some(total_mutations * num_clients) {
290-
return Err(anyhow::anyhow!(
323+
conn.query_row_and_then(
324+
"select value from counter where id = 0",
325+
[],
326+
|row| {
327+
let value: Option<usize> = row.get(0)?;
328+
log::info!(
329+
"client({}): FINAL counter value: {:?}",
330+
timeline_id,
331+
value
332+
);
333+
if value != Some(total_mutations * num_clients) {
334+
return Err(anyhow::anyhow!(
291335
"client({}): counter value is incorrect: {:?}, expected {}",
292336
timeline_id,
293337
value,
294338
total_mutations * num_clients
295339
));
296-
}
297-
Ok(())
298-
})?;
340+
}
341+
Ok(())
342+
},
343+
)?;
344+
conn.query_row_and_then(
345+
"select value from counter where id = 1",
346+
[],
347+
|row| {
348+
let value: Option<usize> = row.get(0)?;
349+
log::info!(
350+
"client({}): FINAL server counter value: {:?}",
351+
timeline_id,
352+
value
353+
);
354+
if value.is_none() || value == Some(0) {
355+
return Err(anyhow::anyhow!(
356+
"client({}): server counter value is incorrect: {:?}, expected non-zero value",
357+
timeline_id,
358+
value,
359+
));
360+
}
361+
Ok(())
362+
},
363+
)?;
299364
Ok::<_, anyhow::Error>(())
300365
})?;
301366

lib/sqlsync/src/coordinator.rs

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
use std::collections::hash_map::Entry;
22
use std::collections::{HashMap, VecDeque};
3+
use std::convert::From;
34
use std::fmt::Debug;
45
use std::io;
56

6-
use crate::db::{open_with_vfs, ConnectionPair};
7+
use rusqlite::Transaction;
8+
9+
use crate::db::{open_with_vfs, run_in_tx, ConnectionPair};
710
use crate::error::Result;
811
use crate::reducer::Reducer;
9-
use crate::replication::{ReplicationDestination, ReplicationError, ReplicationSource};
12+
use crate::replication::{
13+
ReplicationDestination, ReplicationError, ReplicationSource,
14+
};
1015
use crate::timeline::{apply_timeline_range, run_timeline_migration};
16+
use crate::Lsn;
1117
use crate::{
1218
journal::{Journal, JournalFactory, JournalId},
1319
lsn::LsnRange,
1420
storage::Storage,
1521
};
16-
use crate::{JournalError, Lsn};
1722

1823
struct ReceiveQueueEntry {
1924
id: JournalId,
@@ -44,10 +49,11 @@ impl<J: Journal> CoordinatorDocument<J> {
4449
timeline_factory: J::Factory,
4550
reducer_wasm_bytes: &[u8],
4651
) -> Result<Self> {
47-
let (mut sqlite, storage) = open_with_vfs(storage)?;
52+
let (mut sqlite, mut storage) = open_with_vfs(storage)?;
4853

4954
// TODO: this feels awkward here
5055
run_timeline_migration(&mut sqlite.readwrite)?;
56+
storage.commit()?;
5157

5258
Ok(Self {
5359
reducer: Reducer::new(reducer_wasm_bytes)?,
@@ -62,10 +68,12 @@ impl<J: Journal> CoordinatorDocument<J> {
6268
fn get_or_create_timeline_mut(
6369
&mut self,
6470
id: JournalId,
65-
) -> std::result::Result<&mut J, JournalError> {
71+
) -> io::Result<&mut J> {
6672
match self.timelines.entry(id) {
6773
Entry::Occupied(entry) => Ok(entry.into_mut()),
68-
Entry::Vacant(entry) => Ok(entry.insert(self.timeline_factory.open(id)?)),
74+
Entry::Vacant(entry) => {
75+
Ok(entry.insert(self.timeline_factory.open(id)?))
76+
}
6977
}
7078
}
7179

@@ -89,12 +97,26 @@ impl<J: Journal> CoordinatorDocument<J> {
8997
}
9098
}
9199

100+
pub fn mutate_direct<F, E>(&mut self, f: F) -> Result<(), E>
101+
where
102+
F: FnOnce(&mut Transaction) -> Result<(), E>,
103+
E: From<rusqlite::Error> + From<io::Error>,
104+
{
105+
run_in_tx(&mut self.sqlite.readwrite, f)?;
106+
self.storage.commit()?;
107+
Ok(())
108+
}
109+
92110
pub fn step(&mut self) -> Result<()> {
93111
// check to see if we have anything in the receive queue
94112
let entry = self.timeline_receive_queue.pop_front();
95113

96114
if let Some(entry) = entry {
97-
log::debug!("applying range {} to timeline {}", entry.range, entry.id);
115+
log::debug!(
116+
"applying range {} to timeline {}",
117+
entry.range,
118+
entry.id
119+
);
98120

99121
// get the timeline
100122
let timeline = self
@@ -119,7 +141,9 @@ impl<J: Journal> CoordinatorDocument<J> {
119141
}
120142

121143
/// CoordinatorDocument knows how to replicate it's storage journal
122-
impl<J: Journal + ReplicationSource> ReplicationSource for CoordinatorDocument<J> {
144+
impl<J: Journal + ReplicationSource> ReplicationSource
145+
for CoordinatorDocument<J>
146+
{
123147
type Reader<'a> = <J as ReplicationSource>::Reader<'a>
124148
where
125149
Self: 'a;
@@ -132,14 +156,22 @@ impl<J: Journal + ReplicationSource> ReplicationSource for CoordinatorDocument<J
132156
self.storage.source_range()
133157
}
134158

135-
fn read_lsn<'a>(&'a self, lsn: crate::Lsn) -> io::Result<Option<Self::Reader<'a>>> {
159+
fn read_lsn<'a>(
160+
&'a self,
161+
lsn: crate::Lsn,
162+
) -> io::Result<Option<Self::Reader<'a>>> {
136163
self.storage.read_lsn(lsn)
137164
}
138165
}
139166

140167
/// CoordinatorDocument knows how to receive timeline journals from elsewhere
141-
impl<J: Journal + ReplicationDestination> ReplicationDestination for CoordinatorDocument<J> {
142-
fn range(&mut self, id: JournalId) -> std::result::Result<LsnRange, ReplicationError> {
168+
impl<J: Journal + ReplicationDestination> ReplicationDestination
169+
for CoordinatorDocument<J>
170+
{
171+
fn range(
172+
&mut self,
173+
id: JournalId,
174+
) -> std::result::Result<LsnRange, ReplicationError> {
143175
let timeline = self.get_or_create_timeline_mut(id)?;
144176
ReplicationDestination::range(timeline, id)
145177
}

lib/sqlsync/src/db.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use std::convert::From;
2+
13
use rusqlite::{
24
hooks::{AuthAction, AuthContext, Authorization},
3-
Connection, OpenFlags,
5+
Connection, OpenFlags, Transaction,
46
};
57
use sqlite_vfs::FilePtr;
68

@@ -13,11 +15,9 @@ pub struct ConnectionPair {
1315
pub readonly: Connection,
1416
}
1517

16-
type Result<T> = std::result::Result<T, rusqlite::Error>;
17-
1818
pub fn open_with_vfs<J: Journal>(
1919
journal: J,
20-
) -> Result<(ConnectionPair, Box<Storage<J>>)> {
20+
) -> rusqlite::Result<(ConnectionPair, Box<Storage<J>>)> {
2121
let mut storage = Box::new(Storage::new(journal));
2222
let storage_ptr = FilePtr::new(&mut storage);
2323

@@ -68,3 +68,14 @@ pub fn open_with_vfs<J: Journal>(
6868
storage,
6969
))
7070
}
71+
72+
pub fn run_in_tx<F, E>(sqlite: &mut Connection, f: F) -> Result<(), E>
73+
where
74+
F: FnOnce(&mut Transaction) -> Result<(), E>,
75+
E: From<rusqlite::Error>,
76+
{
77+
let mut txn = sqlite.transaction()?;
78+
f(&mut txn)?; // will cause a rollback on failure
79+
txn.commit()?;
80+
Ok(())
81+
}

lib/sqlsync/src/error.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1+
use std::io;
2+
13
use thiserror::Error;
24

35
use crate::{
4-
reducer::ReducerError, replication::ReplicationError, timeline::TimelineError, JournalError,
5-
JournalIdParseError,
6+
reducer::ReducerError, replication::ReplicationError,
7+
timeline::TimelineError, JournalIdParseError,
68
};
79

810
#[derive(Error, Debug)]
911
pub enum Error {
1012
#[error(transparent)]
1113
ReplicationError(#[from] ReplicationError),
1214

13-
#[error(transparent)]
14-
JournalError(#[from] JournalError),
15-
1615
#[error(transparent)]
1716
JournalIdParseError(#[from] JournalIdParseError),
1817

@@ -24,6 +23,9 @@ pub enum Error {
2423

2524
#[error(transparent)]
2625
SqliteError(#[from] rusqlite::Error),
26+
27+
#[error("io error: {0}")]
28+
IoError(#[from] io::Error),
2729
}
2830

29-
pub type Result<T> = std::result::Result<T, Error>;
31+
pub type Result<T, E = Error> = std::result::Result<T, E>;

0 commit comments

Comments
 (0)