├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── RELEASE.md ├── TODO.md ├── check.sh ├── perf.sh ├── rustfmt.toml └── src ├── batch.rs ├── batch_test.rs ├── bin └── perf.rs ├── entry.rs ├── entry_test.rs ├── files.rs ├── journal.rs ├── journal_test.rs ├── lib.rs ├── marker.rs ├── state.rs ├── util.rs ├── wral.rs ├── wral_test.rs └── writer.rs /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | Cargo.lock 3 | core 4 | test.out 5 | flamegraph.svg 6 | perf.out 7 | check.out 8 | perf.data 9 | perf.data.old 10 | target 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wral" 3 | version = "0.2.0" 4 | description = "Write ahead logging for durability" 5 | repository = "https://github.com/bnclabs/wral" 6 | documentation = "https://docs.rs/wral/" 7 | keywords = ["wal", "db", "database"] 8 | categories = ["concurrency", "database", "filesystem"] 9 | authors = ["prataprc "] 10 | license = "MIT" 11 | edition = "2018" 12 | readme = "README.md" 13 | 14 | [profile.release] 15 | debug = true 16 | 17 | [profile.bench] 18 | debug = true 19 | 20 | [[bin]] 21 | name = "perf" 22 | required-features = ["perf"] 23 | 24 | [dependencies] 25 | mkit = { path = "../../_archive/mkit", version = "0.4.0" } 26 | log = "0.4" 27 | arbitrary = { version = "0.4", features = ["derive"] } 28 | tempfile = "3" 29 | 30 | structopt = { version = "0.3.20", default-features = false, optional = true } 31 | rand = { version = "0.8.4", features = ["std_rng"], optional = true } 32 | 33 | [dev-dependencies] 34 | rand = { version = "0.8.4", features = ["std_rng"]} 35 | 36 | [features] 37 | perf = ["structopt", "rand"] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Package not ready for stable. 2 | 3 | build: 4 | # ... build ... 5 | cargo +nightly build 6 | # ... test ... 7 | cargo +nightly test --no-run 8 | # ... bench ... 9 | cargo +nightly bench --no-run 10 | # ... doc ... 11 | cargo +nightly doc 12 | # ... bins ... 13 | cargo +nightly build --release --bin perf --features=perf 14 | # ... meta commands ... 15 | cargo +nightly clippy --all-targets --all-features 16 | flamegraph: 17 | cargo flamegraph --features=perf --bin=perf -- --payload 100 --ops 10000 --threads 1 --size 100000000 18 | prepare: 19 | check.sh 20 | perf.sh 21 | clean: 22 | cargo clean 23 | rm -f check.out perf.out flamegraph.svg perf.data perf.data.old 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Documentation](https://docs.rs/wral/badge.svg?style=flat-square)](https://docs.rs/wral) 2 | 3 | _Write ahead logging for rust applications_. Write ahead logging is a 4 | crucial component for applications requiring data durability. Many times it 5 | is inefficient to flush and sync new data (or modifications to existing data) 6 | to on-disk data-structures, like an index. Write-ahead-logging facilitates 7 | by ingesting write operations by appending and syncing it to disk and allows 8 | applications to pre-process a batch of write-operations and write them to 9 | on-disk structures in the most efficient manner. 10 | 11 | Goals 12 | ----- 13 | 14 | * [x] Serialize write operations to an append only journal file. 15 | * [x] Generate monotonically increasing `sequence-number` and return the 16 | same to the application. 17 | * [x] Configurable limit for journal file, beyond which log files are rotate. 18 | * [x] Configurable fsync, for persistence guarantee. 19 | * [x] Iterate over all entries persisted in the log file in monotonically 20 | increasing `seqno` order. 21 | * [x] Range over a subset of entries, specified with `start-seqno` and 22 | `end-seqno`. 23 | * [x] `Wal` type is parameterized over a state type `S`. This is helpful for 24 | using `Wal` type to be used with consensus protocol like [Raft][raft]. 25 | * [x] Concurrent readers and writers into single log instance. 26 | 27 | **Concurrency** 28 | 29 | A single log-instance can be cloned and shared among multiple threads 30 | for concurrent writing and reading. All write operations are serialized. 31 | While read operations and write-operation are mutually exclusive, 32 | concurrent reads are allowed. 33 | 34 | Performance 35 | ----------- 36 | 37 | Single threaded write performance with different payload size and 38 | `fsync` enabled. 39 | 40 | payload | total-entries | elapsed-time | throughput 41 | --------|----------------|--------------|------------ 42 | 100 | 10000 | 31s | 300/s 43 | 1000 | 10000 | 31s | 300/s 44 | 10000 | 10000 | 31s | 300/s 45 | 10000 | 1000 | 3.1s | 300/s 46 | 47 | Multi-threaded write performance with constant payload size of 48 | 100-bytes per operation and `fsync` enabled. 49 | 50 | threads | total-entries | elapsed-time | throughput 51 | --------|----------------|--------------|------------ 52 | 1 | 10000 | 31s | 300/s 53 | 2 | 20000 | 60s | 300/s 54 | 4 | 40000 | 59s | 650/s 55 | 8 | 80000 | 54s | 1300/s 56 | 16 | 160000 | 50s | 3200/s 57 | 58 | Multi-threaded read performance with constant payload size of 59 | 100-bytes per operation and `fsync` enabled. 60 | 61 | threads | total-entries | elapsed-time | throughput 62 | --------|----------------|--------------|------------ 63 | 1 | 10000 | .15s | 66000/s 64 | 2 | 20000 | .28s | 71000/s 65 | 4 | 40000 | .38s | 105000/s 66 | 8 | 80000 | .62s | 130000/s 67 | 16 | 160000 | 1.10s | 150000/s 68 | 69 | Contribution 70 | ------------ 71 | 72 | * Simple workflow. Fork - Modify - Pull request. 73 | * Before creating a PR, 74 | * Run `make build` to confirm all versions of build is passing with 75 | 0 warnings and 0 errors. 76 | * Run `check.sh` with 0 warnings, 0 errors and all testcases passing. 77 | * Run `perf.sh` with 0 warnings, 0 errors and all testcases passing. 78 | * [Install][spellcheck] and run `cargo spellcheck` to remove common spelling mistakes. 79 | * [Developer certificate of origin][dco] is preferred. 80 | 81 | [spellcheck]: https://github.com/drahnr/cargo-spellcheck 82 | [dco]: https://developercertificate.org/ 83 | [raft]: https://raft.github.io 84 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | ----- 3 | 4 | * Cargo manifest, improvements. 5 | * performance metrics. 6 | * rustfmt fix max-width to 90 columns. 7 | * don't export `err_at!()` macro. 8 | * package maintanence. 9 | 10 | Refer to [release-checklist][release-checklist]. 11 | 12 | [release-checklist]: https://prataprc.github.io/rust-crates-release-checklist.html 13 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * review "use imports" and "crate imports". 2 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | export RUST_BACKTRACE=full 4 | export RUSTFLAGS=-g 5 | exec > check.out 6 | exec 2>&1 7 | 8 | set -o xtrace 9 | 10 | exec_prg() { 11 | for i in {0..5}; 12 | do 13 | date; time cargo +nightly test --release -- --nocapture || exit $? 14 | date; time cargo +nightly test -- --nocapture || exit $? 15 | date; time cargo +nightly bench -- --nocapture || exit $? 16 | # repeat this for stable 17 | date; time cargo test --release -- --nocapture || exit $? 18 | date; time cargo test -- --nocapture || exit $? 19 | date; time cargo bench -- --nocapture || exit $? 20 | done 21 | } 22 | 23 | exec_prg 24 | -------------------------------------------------------------------------------- /perf.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | exec > perf.out 4 | exec 2>&1 5 | 6 | set -o xtrace 7 | 8 | if [ -f ./target/release/perf ] then 9 | PERF=./target/release/perf 10 | else 11 | PERF=$HOME/.cargo/target/release/perf 12 | fi 13 | 14 | date; time cargo bench || exit $? 15 | 16 | # Single threaded, with 100-bytes, 1K, 10K, 1M payload, with fsync true. 17 | date; time cargo run --bin perf --features=perf -- --payload 100 --ops 10000 --threads 1 --size 100000000 18 | date; time cargo run --bin perf --features=perf -- --payload 1000 --ops 10000 --threads 1 --size 100000000 19 | date; time cargo run --bin perf --features=perf -- --payload 10000 --ops 10000 --threads 1 --size 100000000 20 | date; time cargo run --bin perf --features=perf -- --payload 100000 --ops 1000 --threads 1 --size 100000000 21 | 22 | # 100-byte payload, [1,2,4,8,16] thread write operations, fsync = true 23 | date; time cargo run --bin perf --features=perf -- --payload 100 --ops 10000 --threads 1 --size 100000000 24 | date; time cargo run --bin perf --features=perf -- --payload 100 --ops 10000 --threads 2 --size 100000000 25 | date; time cargo run --bin perf --features=perf -- --payload 100 --ops 10000 --threads 4 --size 100000000 26 | date; time cargo run --bin perf --features=perf -- --payload 100 --ops 10000 --threads 8 --size 100000000 27 | date; time cargo run --bin perf --features=perf -- --payload 100 --ops 10000 --threads 16 --size 100000000 28 | 29 | date; valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes $PERF --payload 100000 --ops 1000 --threads 1 --size 100000000 || exit $? 30 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 90 2 | array_width = 80 3 | attr_fn_like_width = 80 4 | chain_width = 80 5 | fn_call_width = 80 6 | single_line_if_else_max_width = 80 7 | struct_lit_width = 50 8 | -------------------------------------------------------------------------------- /src/batch.rs: -------------------------------------------------------------------------------- 1 | use arbitrary::{Arbitrary, Unstructured}; 2 | use mkit::{ 3 | self, 4 | cbor::{Cbor, FromCbor}, 5 | Cborize, 6 | }; 7 | 8 | use std::{ 9 | cmp, 10 | fmt::{self, Display}, 11 | fs, 12 | io::{self, Read, Seek}, 13 | ops, result, vec, 14 | }; 15 | 16 | use crate::{entry, state, util, Error, Result}; 17 | 18 | pub struct Worker { 19 | index: Vec, 20 | entries: Vec, 21 | state: S, 22 | } 23 | 24 | impl Worker { 25 | pub fn new(state: S) -> Worker { 26 | Worker { 27 | index: Vec::default(), 28 | entries: Vec::default(), 29 | state, 30 | } 31 | } 32 | 33 | pub fn add_entry(&mut self, entry: entry::Entry) -> Result<()> 34 | where 35 | S: state::State, 36 | { 37 | self.state.on_add_entry(&entry)?; 38 | self.entries.push(entry); 39 | Ok(()) 40 | } 41 | 42 | pub fn flush(&mut self, file: &mut fs::File) -> Result> 43 | where 44 | S: state::State, 45 | { 46 | let fpos = err_at!(IOError, file.metadata())?.len(); 47 | let batch = match self.entries.len() { 48 | 0 => return Ok(None), 49 | _ => Batch { 50 | first_seqno: self.entries.first().map(entry::Entry::to_seqno).unwrap(), 51 | last_seqno: self.entries.last().map(entry::Entry::to_seqno).unwrap(), 52 | state: util::encode_cbor(self.state.clone())?, 53 | entries: self.entries.drain(..).collect(), 54 | }, 55 | }; 56 | 57 | let first_seqno = batch.first_seqno; 58 | let last_seqno = batch.last_seqno; 59 | let length = { 60 | let data = util::encode_cbor(batch)?; 61 | util::sync_write(file, &data)?; 62 | data.len() 63 | }; 64 | 65 | let index = Index::new(fpos, length, first_seqno, last_seqno); 66 | self.index.push(index.clone()); 67 | 68 | Ok(Some(index)) 69 | } 70 | } 71 | 72 | impl Worker { 73 | pub fn to_last_seqno(&self) -> Option { 74 | match self.entries.len() { 75 | 0 => self.index.last().map(|index| index.last_seqno), 76 | _ => self.entries.last().map(entry::Entry::to_seqno), 77 | } 78 | } 79 | 80 | pub fn to_index(&self) -> Vec { 81 | self.index.clone() 82 | } 83 | 84 | pub fn to_entries(&self) -> Vec { 85 | self.entries.clone() 86 | } 87 | 88 | pub fn len_batches(&self) -> usize { 89 | self.index.len() 90 | } 91 | 92 | pub fn to_state(&self) -> S 93 | where 94 | S: Clone, 95 | { 96 | self.state.clone() 97 | } 98 | 99 | pub fn unwrap(self) -> (Vec, Vec, S) { 100 | (self.index, self.entries, self.state) 101 | } 102 | } 103 | 104 | /// Batch of entries on disk or in-memory. 105 | #[derive(Debug, Clone, Eq, PartialEq, Cborize)] 106 | pub struct Batch { 107 | // index-seqno of first entry in this batch. 108 | first_seqno: u64, 109 | // index-seqno of last entry in this batch. 110 | last_seqno: u64, 111 | // state as serialized bytes, shall be in cbor format. 112 | state: Vec, 113 | // list of entries in this batch. 114 | entries: Vec, 115 | } 116 | 117 | impl arbitrary::Arbitrary for Batch { 118 | fn arbitrary(u: &mut Unstructured) -> arbitrary::Result { 119 | let mut entries: Vec = u.arbitrary()?; 120 | entries.dedup_by(|a, b| a.to_seqno() == b.to_seqno()); 121 | entries.sort(); 122 | 123 | let first_seqno: u64 = entries.first().map(|e| e.to_seqno()).unwrap_or(0); 124 | let last_seqno: u64 = entries.last().map(|e| e.to_seqno()).unwrap_or(0); 125 | 126 | let batch = Batch { 127 | first_seqno, 128 | last_seqno, 129 | state: u.arbitrary()?, 130 | entries, 131 | }; 132 | Ok(batch) 133 | } 134 | } 135 | 136 | impl Display for Batch { 137 | fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { 138 | write!(f, "batch<{}..{}]>", self.first_seqno, self.last_seqno) 139 | } 140 | } 141 | 142 | impl PartialOrd for Batch { 143 | fn partial_cmp(&self, other: &Self) -> Option { 144 | Some(self.cmp(other)) 145 | } 146 | } 147 | 148 | impl Ord for Batch { 149 | fn cmp(&self, other: &Self) -> cmp::Ordering { 150 | self.first_seqno.cmp(&other.first_seqno) 151 | } 152 | } 153 | 154 | impl Batch { 155 | const ID: u32 = 0x0; 156 | 157 | pub fn from_index(index: Index, file: &mut fs::File) -> Result { 158 | err_at!(IOError, file.seek(io::SeekFrom::Start(index.fpos)))?; 159 | let mut buf = vec![0; index.length]; 160 | err_at!(IOError, file.read_exact(&mut buf))?; 161 | let (value, _) = Cbor::decode(&mut buf.as_slice())?; 162 | Ok(Batch::from_cbor(value)?) 163 | } 164 | 165 | #[inline] 166 | pub fn to_state(&self) -> Vec { 167 | self.state.to_vec() 168 | } 169 | 170 | #[inline] 171 | pub fn to_first_seqno(&self) -> u64 { 172 | self.first_seqno 173 | } 174 | 175 | #[inline] 176 | pub fn to_last_seqno(&self) -> u64 { 177 | self.last_seqno 178 | } 179 | 180 | pub fn into_iter( 181 | self, 182 | range: ops::RangeInclusive, 183 | ) -> vec::IntoIter { 184 | self.entries 185 | .into_iter() 186 | .filter(|e| range.contains(&e.to_seqno())) 187 | .collect::>() 188 | .into_iter() 189 | } 190 | } 191 | 192 | /// Index of batches on disk. 193 | #[derive(Debug, Clone, Eq, PartialEq, Arbitrary)] 194 | pub struct Index { 195 | // offset in file, where the batch starts. 196 | fpos: u64, 197 | // length from offset that spans the entire batch. 198 | length: usize, 199 | // first seqno in the batch. 200 | first_seqno: u64, 201 | // last seqno in the batch. 202 | last_seqno: u64, 203 | } 204 | 205 | impl Index { 206 | pub fn new(fpos: u64, length: usize, first_seqno: u64, last_seqno: u64) -> Index { 207 | Index { fpos, length, first_seqno, last_seqno } 208 | } 209 | 210 | #[inline] 211 | pub fn to_first_seqno(&self) -> u64 { 212 | self.first_seqno 213 | } 214 | 215 | #[inline] 216 | pub fn to_last_seqno(&self) -> u64 { 217 | self.last_seqno 218 | } 219 | } 220 | 221 | #[cfg(test)] 222 | #[path = "batch_test.rs"] 223 | mod batch_test; 224 | -------------------------------------------------------------------------------- /src/batch_test.rs: -------------------------------------------------------------------------------- 1 | use arbitrary::Unstructured; 2 | use mkit::cbor::IntoCbor; 3 | use rand::{prelude::random, rngs::StdRng, Rng, SeedableRng}; 4 | 5 | use super::*; 6 | 7 | #[test] 8 | fn test_index() { 9 | let seed: u64 = random(); 10 | println!("test_index {}", seed); 11 | let mut rng = StdRng::seed_from_u64(seed); 12 | 13 | let index: Index = { 14 | let bytes = rng.gen::<[u8; 32]>(); 15 | let mut uns = Unstructured::new(&bytes); 16 | uns.arbitrary().unwrap() 17 | }; 18 | assert_eq!(index.to_first_seqno(), index.first_seqno); 19 | assert_eq!(index.to_first_seqno(), index.first_seqno); 20 | 21 | let val = Index::new(index.fpos, index.length, index.first_seqno, index.last_seqno); 22 | assert_eq!(index, val); 23 | } 24 | 25 | #[test] 26 | fn test_batch() { 27 | let seed: u64 = random(); 28 | println!("test_batch {}", seed); 29 | let mut rng = StdRng::seed_from_u64(seed); 30 | 31 | let mut batches = vec![]; 32 | for _i in 0..1000 { 33 | let batch: Batch = { 34 | let bytes = rng.gen::<[u8; 32]>(); 35 | let mut uns = Unstructured::new(&bytes); 36 | uns.arbitrary().unwrap() 37 | }; 38 | batches.push(batch.clone()); 39 | 40 | assert_eq!(batch.to_state(), batch.state); 41 | assert_eq!(batch.to_first_seqno(), batch.first_seqno); 42 | assert_eq!(batch.to_last_seqno(), batch.last_seqno); 43 | assert_eq!( 44 | batch.clone().into_iter(0..=u64::MAX).collect::>(), 45 | batch.entries 46 | ); 47 | 48 | let cbor: Cbor = batch.clone().into_cbor().unwrap(); 49 | 50 | let mut buf: Vec = vec![]; 51 | let n = cbor.encode(&mut buf).unwrap(); 52 | let (val, m) = Cbor::decode(&mut buf.as_slice()).unwrap(); 53 | assert_eq!(n, m); 54 | assert_eq!(cbor, val); 55 | 56 | let rbatch = Batch::from_cbor(val).unwrap(); 57 | assert_eq!(batch, rbatch); 58 | } 59 | 60 | let mut batches: Vec = 61 | batches.into_iter().filter(|b| b.entries.is_empty()).collect(); 62 | batches.sort(); 63 | batches.dedup_by(|a, b| a.first_seqno == b.first_seqno); 64 | 65 | let mut seqno = 0; 66 | for batch in batches.into_iter() { 67 | assert!(seqno <= batch.first_seqno, "{} {}", seqno, batch.first_seqno); 68 | assert!(batch.first_seqno <= batch.last_seqno, "{}", batch); 69 | seqno = batch.first_seqno 70 | } 71 | } 72 | 73 | #[test] 74 | fn test_worker() { 75 | use crate::state; 76 | 77 | let seed: u64 = random(); 78 | println!("test_worker {}", seed); 79 | let mut rng = StdRng::seed_from_u64(seed); 80 | 81 | let mut file = { 82 | let ntf = tempfile::NamedTempFile::new().unwrap(); 83 | println!("test_worker temporary file created {:?}", ntf.path()); 84 | ntf.into_file() 85 | }; 86 | 87 | let mut worker = Worker::new(state::NoState); 88 | 89 | let mut index = vec![]; 90 | let mut all_entries = vec![]; 91 | for _i in 0..1000 { 92 | let mut entries = vec![]; 93 | let n = rng.gen::(); 94 | for _j in 0..n { 95 | let entry: entry::Entry = { 96 | let bytes = rng.gen::<[u8; 32]>(); 97 | let mut uns = Unstructured::new(&bytes); 98 | uns.arbitrary().unwrap() 99 | }; 100 | worker.add_entry(entry.clone()).unwrap(); 101 | entries.push(entry.clone()); 102 | all_entries.push(entry); 103 | } 104 | 105 | assert_eq!(entries, worker.to_entries()); 106 | if n > 0 { 107 | assert_eq!(entries.last().map(|e| e.to_seqno()), worker.to_last_seqno()) 108 | } 109 | 110 | if let Some(x) = worker.flush(&mut file).unwrap() { 111 | index.push(x) 112 | }; 113 | 114 | if n > 0 { 115 | assert_eq!(entries.last().map(|e| e.to_seqno()), worker.to_last_seqno()) 116 | } 117 | } 118 | 119 | assert_eq!(index, worker.to_index()); 120 | let entries = index 121 | .iter() 122 | .map(|x| { 123 | Batch::from_index(x.clone(), &mut file) 124 | .unwrap() 125 | .into_iter(0..=u64::MAX) 126 | .collect::>() 127 | }) 128 | .flatten() 129 | .collect::>(); 130 | assert_eq!(entries, all_entries) 131 | } 132 | -------------------------------------------------------------------------------- /src/bin/perf.rs: -------------------------------------------------------------------------------- 1 | use rand::prelude::random; 2 | use structopt::StructOpt; 3 | 4 | use std::time; 5 | 6 | use wral::{self}; 7 | 8 | // Command line options. 9 | #[derive(Clone, StructOpt)] 10 | pub struct Opt { 11 | #[structopt(long = "seed")] 12 | seed: Option, 13 | 14 | #[structopt(long = "name", default_value = "wral-perf")] 15 | name: String, 16 | 17 | #[structopt(long = "ops", default_value = "1000000")] // default 1M 18 | ops: usize, 19 | 20 | #[structopt(long = "payload", default_value = "32")] // default 32-bytes 21 | payload: usize, 22 | 23 | #[structopt(long = "threads", default_value = "8")] 24 | threads: usize, 25 | 26 | #[structopt(long = "size", default_value = "10000000")] // default 10M bytes 27 | journal_limit: usize, 28 | 29 | #[structopt(long = "nosync")] 30 | nosync: bool, 31 | } 32 | 33 | fn main() { 34 | let dir = tempfile::tempdir().unwrap(); 35 | let opts = Opt::from_args(); 36 | let seed = opts.seed.unwrap_or_else(random); 37 | 38 | let mut config = wral::Config::new(&opts.name, dir.path().as_os_str()); 39 | config.set_journal_limit(opts.journal_limit).set_fsync(!opts.nosync); 40 | println!("{:?}", config); 41 | 42 | let wal = wral::Wal::create(config, wral::NoState).unwrap(); 43 | 44 | let mut writers = vec![]; 45 | for id in 0..opts.threads { 46 | let wal = wal.clone(); 47 | let opts = opts.clone(); 48 | writers.push(std::thread::spawn(move || writer(id, wal, opts, seed))); 49 | } 50 | 51 | let mut entries: Vec> = vec![]; 52 | for handle in writers { 53 | entries.push(handle.join().unwrap()); 54 | } 55 | let mut entries: Vec = entries.into_iter().flatten().collect(); 56 | entries.sort_by_key(|a| a.to_seqno()); 57 | 58 | let n = entries.len() as u64; 59 | let sum = entries.iter().map(|e| e.to_seqno()).sum::(); 60 | assert_eq!(sum, (n * (n + 1)) / 2); 61 | 62 | let mut readers = vec![]; 63 | for id in 0..opts.threads { 64 | let wal = wal.clone(); 65 | let entries = entries.clone(); 66 | readers.push(std::thread::spawn(move || reader(id, wal, entries))); 67 | } 68 | 69 | for handle in readers { 70 | handle.join().unwrap(); 71 | } 72 | 73 | wal.close(true).unwrap(); 74 | } 75 | 76 | fn writer(id: usize, wal: wral::Wal, opts: Opt, _seed: u128) -> Vec { 77 | let start = time::Instant::now(); 78 | 79 | let mut entries = vec![]; 80 | let op = vec![0; opts.payload]; 81 | for _i in 0..opts.ops { 82 | let seqno = wal.add_op(&op).unwrap(); 83 | entries.push(wral::Entry::new(seqno, op.clone())); 84 | } 85 | 86 | println!("w-{:02} took {:?} to write {} ops", id, start.elapsed(), opts.ops); 87 | entries 88 | } 89 | 90 | fn reader(id: usize, wal: wral::Wal, entries: Vec) { 91 | let start = time::Instant::now(); 92 | let items: Vec = wal.iter().unwrap().map(|x| x.unwrap()).collect(); 93 | assert_eq!(items, entries); 94 | 95 | println!("r-{:02} took {:?} to iter {} ops", id, start.elapsed(), items.len()); 96 | } 97 | -------------------------------------------------------------------------------- /src/entry.rs: -------------------------------------------------------------------------------- 1 | use arbitrary::Arbitrary; 2 | use mkit::Cborize; 3 | 4 | use std::{ 5 | cmp, 6 | fmt::{self, Display}, 7 | result, 8 | }; 9 | 10 | /// Single Op-entry in Write-ahead-log. 11 | #[derive(Debug, Clone, Default, Cborize, Arbitrary)] 12 | pub struct Entry { 13 | // Index seqno for this entry. This will be monotonically 14 | // increasing number. 15 | seqno: u64, 16 | // Operation to be logged. 17 | op: Vec, 18 | } 19 | 20 | impl Eq for Entry {} 21 | 22 | impl PartialEq for Entry { 23 | fn eq(&self, other: &Self) -> bool { 24 | self.seqno.eq(&other.seqno) 25 | } 26 | } 27 | 28 | impl Display for Entry { 29 | fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { 30 | write!(f, "entry", self.seqno) 31 | } 32 | } 33 | 34 | impl PartialOrd for Entry { 35 | fn partial_cmp(&self, other: &Self) -> Option { 36 | Some(self.cmp(other)) 37 | } 38 | } 39 | 40 | impl Ord for Entry { 41 | fn cmp(&self, other: &Self) -> cmp::Ordering { 42 | self.seqno.cmp(&other.seqno) 43 | } 44 | } 45 | 46 | impl Entry { 47 | const ID: u32 = 0x0; 48 | 49 | #[inline] 50 | pub fn new(seqno: u64, op: Vec) -> Entry { 51 | Entry { seqno, op } 52 | } 53 | 54 | #[inline] 55 | pub fn to_seqno(&self) -> u64 { 56 | self.seqno 57 | } 58 | 59 | #[inline] 60 | pub fn unwrap(self) -> (u64, Vec) { 61 | (self.seqno, self.op) 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | #[path = "entry_test.rs"] 67 | mod entry_test; 68 | -------------------------------------------------------------------------------- /src/entry_test.rs: -------------------------------------------------------------------------------- 1 | use arbitrary::Unstructured; 2 | use mkit::{ 3 | self, 4 | {cbor::FromCbor, cbor::IntoCbor}, 5 | }; 6 | use rand::{prelude::random, rngs::StdRng, Rng, SeedableRng}; 7 | 8 | use super::*; 9 | 10 | #[test] 11 | fn test_entry() { 12 | use mkit::cbor::Cbor; 13 | 14 | let seed: u64 = random(); 15 | println!("test_entry {}", seed); 16 | let mut rng = StdRng::seed_from_u64(seed); 17 | 18 | let mut entries: Vec = (0..1000) 19 | .map(|_i| { 20 | let bytes = rng.gen::<[u8; 32]>(); 21 | let mut uns = Unstructured::new(&bytes); 22 | uns.arbitrary::().unwrap() 23 | }) 24 | .collect(); 25 | entries.sort(); 26 | entries.dedup_by(|a, b| a.seqno == b.seqno); 27 | 28 | for entry in entries.iter() { 29 | let entry = entry.clone(); 30 | assert_eq!(entry.to_seqno(), entry.seqno); 31 | let (seqno, op) = entry.clone().unwrap(); 32 | assert_eq!(entry, Entry::new(seqno, op)); 33 | 34 | let cbor: Cbor = entry.clone().into_cbor().unwrap(); 35 | let mut buf: Vec = vec![]; 36 | let n = cbor.encode(&mut buf).unwrap(); 37 | let (val, m) = Cbor::decode(&mut buf.as_slice()).unwrap(); 38 | assert_eq!(n, m); 39 | assert_eq!(cbor, val); 40 | 41 | let entr = Entry::from_cbor(val).unwrap(); 42 | assert_eq!(entr, entry); 43 | } 44 | 45 | let mut seqno = 0; 46 | for entry in entries.into_iter() { 47 | assert!(seqno < entry.seqno, "{} {}", seqno, entry.seqno); 48 | seqno = entry.seqno 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/files.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi, path}; 2 | 3 | use crate::Error; 4 | 5 | pub fn make_filename(name: String, num: usize) -> ffi::OsString { 6 | let file = format!("{}-journal-{:03}.dat", name, num); 7 | let file: &ffi::OsStr = file.as_ref(); 8 | file.to_os_string() 9 | } 10 | 11 | pub fn unwrap_filename(file: ffi::OsString) -> Option<(String, usize)> { 12 | let stem = { 13 | let fname = path::Path::new(path::Path::new(&file).file_name()?); 14 | match fname.extension()?.to_str()? { 15 | "dat" => Some(fname.file_stem()?.to_str()?.to_string()), 16 | _ => None, 17 | }? 18 | }; 19 | 20 | let mut parts: Vec<&str> = stem.split('-').collect(); 21 | 22 | let (name, parts) = match parts.len() { 23 | 3 => Some((parts.remove(0).to_string(), parts)), 24 | n if n > 3 => { 25 | let name: Vec<&str> = parts.drain(..n - 2).collect(); 26 | Some((name.join("-"), parts)) 27 | } 28 | _ => None, 29 | }?; 30 | 31 | match parts[..] { 32 | ["journal", num] => { 33 | let num: usize = err_at!(FailConvert, num.parse()).ok()?; 34 | Some((name, num)) 35 | } 36 | _ => None, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/journal.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, error}; 2 | use mkit::{ 3 | self, 4 | cbor::{Cbor, FromCbor}, 5 | }; 6 | 7 | use std::{ 8 | convert::TryFrom, 9 | ffi, 10 | fmt::{self, Display}, 11 | fs, ops, path, result, vec, 12 | }; 13 | 14 | use crate::{batch, entry, files, state, Error, Result}; 15 | 16 | pub struct Journal { 17 | name: String, 18 | num: usize, 19 | file_path: ffi::OsString, // dir/{name}-journal-{num}.dat 20 | inner: InnerJournal, 21 | } 22 | 23 | impl Display for Journal { 24 | fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { 25 | write!(f, "journal-{}-{}", self.name, self.num) 26 | } 27 | } 28 | 29 | enum InnerJournal { 30 | // Active journal, the latest journal, in the journal-set. A journal 31 | // set is managed by Shard. 32 | Working { 33 | worker: batch::Worker, 34 | file: fs::File, 35 | }, 36 | // All journals except lastest journal are archives, which means only 37 | // the metadata for each batch shall be stored. 38 | Archive { 39 | index: Vec, 40 | state: S, 41 | }, 42 | // Cold journals are colder than archives, that is, they are not 43 | // required by the application, may be as frozen-backup. 44 | Cold, 45 | } 46 | 47 | impl Journal { 48 | pub fn start( 49 | name: &str, 50 | dir: &ffi::OsStr, 51 | num: usize, 52 | state: S, 53 | ) -> Result> { 54 | let file_path: path::PathBuf = { 55 | let file: ffi::OsString = files::make_filename(name.to_string(), num); 56 | [dir, &file].iter().collect() 57 | }; 58 | 59 | fs::remove_file(&file_path).ok(); // cleanup a single journal file 60 | 61 | let file = { 62 | let mut opts = fs::OpenOptions::new(); 63 | err_at!(IOError, opts.append(true).create_new(true).open(&file_path))? 64 | }; 65 | debug!(target: "wral", "start_journal {:?}", file_path); 66 | 67 | Ok(Journal { 68 | name: name.to_string(), 69 | num, 70 | file_path: file_path.into_os_string(), 71 | inner: InnerJournal::Working { worker: batch::Worker::new(state), file }, 72 | }) 73 | } 74 | 75 | pub fn load(name: &str, file_path: &ffi::OsStr) -> Option<(Journal, S)> 76 | where 77 | S: Clone + FromCbor, 78 | { 79 | let os_file = path::Path::new(file_path); 80 | let (nm, num) = files::unwrap_filename(os_file.file_name()?.to_os_string())?; 81 | 82 | if nm != name { 83 | return None; 84 | } 85 | 86 | let mut file = 87 | err_at!(IOError, fs::OpenOptions::new().read(true).open(os_file)).ok()?; 88 | 89 | let mut state = vec![]; 90 | let mut index = vec![]; 91 | let mut fpos = 0_usize; 92 | let len = file.metadata().ok()?.len(); 93 | 94 | while u64::try_from(fpos).ok()? < len { 95 | let (val, n) = Cbor::decode(&mut file).ok()?; 96 | let batch = batch::Batch::from_cbor(val).ok()?; 97 | index.push(batch::Index::new( 98 | u64::try_from(fpos).ok()?, 99 | n, 100 | batch.to_first_seqno(), 101 | batch.to_last_seqno(), 102 | )); 103 | state = batch.to_state(); 104 | fpos += n 105 | } 106 | 107 | if index.is_empty() { 108 | return None; 109 | } 110 | 111 | let state: S = match Cbor::decode(&mut state.as_slice()) { 112 | Ok((state, _)) => match S::from_cbor(state) { 113 | Ok(state) => Some(state), 114 | Err(err) => { 115 | error!(target: "wral", "corrupted state-cbor {:?} {}", file_path, err); 116 | None 117 | } 118 | }, 119 | Err(err) => { 120 | error!(target: "wral", "corrupted state {:?} {}", file_path, err); 121 | None 122 | } 123 | }?; 124 | 125 | debug!(target: "wral", "load journal {:?}, loaded {} batches", file_path, index.len()); 126 | 127 | let journal = Journal { 128 | name: name.to_string(), 129 | num, 130 | file_path: file_path.to_os_string(), 131 | inner: InnerJournal::Archive { index, state: state.clone() }, 132 | }; 133 | 134 | Some((journal, state)) 135 | } 136 | 137 | pub fn load_cold(name: &str, file_path: &ffi::OsStr) -> Option> { 138 | let os_file = path::Path::new(file_path); 139 | let (nm, num) = files::unwrap_filename(os_file.file_name()?.to_os_string())?; 140 | 141 | if nm != name { 142 | return None; 143 | } 144 | 145 | let journal = Journal { 146 | name: name.to_string(), 147 | num, 148 | file_path: file_path.to_os_string(), 149 | inner: InnerJournal::Cold, 150 | }; 151 | Some(journal) 152 | } 153 | 154 | pub fn into_archive(mut self) -> (Self, Vec, S) 155 | where 156 | S: Clone, 157 | { 158 | let (inner, entries, state) = match self.inner { 159 | InnerJournal::Working { worker, .. } => { 160 | let (index, entries, state) = worker.unwrap(); 161 | let inner = InnerJournal::Archive { index, state: state.clone() }; 162 | (inner, entries, state) 163 | } 164 | _ => unreachable!(), 165 | }; 166 | self.inner = inner; 167 | (self, entries, state) 168 | } 169 | 170 | pub fn purge(self) -> Result<()> { 171 | debug!(target: "wral", "purging {:?} ...", self.file_path); 172 | err_at!(IOError, fs::remove_file(&self.file_path))?; 173 | Ok(()) 174 | } 175 | } 176 | 177 | impl Journal { 178 | pub fn add_entry(&mut self, entry: entry::Entry) -> Result<()> 179 | where 180 | S: state::State, 181 | { 182 | match &mut self.inner { 183 | InnerJournal::Working { worker, .. } => worker.add_entry(entry), 184 | InnerJournal::Archive { .. } => unreachable!(), 185 | InnerJournal::Cold => unreachable!(), 186 | } 187 | } 188 | 189 | pub fn flush(&mut self) -> Result<()> 190 | where 191 | S: state::State, 192 | { 193 | match &mut self.inner { 194 | InnerJournal::Working { worker, file } => { 195 | worker.flush(file)?; 196 | Ok(()) 197 | } 198 | InnerJournal::Archive { .. } => unreachable!(), 199 | InnerJournal::Cold { .. } => unreachable!(), 200 | } 201 | } 202 | } 203 | 204 | impl Journal { 205 | pub fn to_journal_number(&self) -> usize { 206 | self.num 207 | } 208 | 209 | pub fn len_batches(&self) -> usize { 210 | match &self.inner { 211 | InnerJournal::Working { worker, .. } => worker.len_batches(), 212 | InnerJournal::Archive { index, .. } => index.len(), 213 | InnerJournal::Cold { .. } => unreachable!(), 214 | } 215 | } 216 | 217 | pub fn to_last_seqno(&self) -> Option { 218 | match &self.inner { 219 | InnerJournal::Working { worker, .. } => worker.to_last_seqno(), 220 | InnerJournal::Archive { index, .. } if index.is_empty() => None, 221 | InnerJournal::Archive { index, .. } => { 222 | index.last().map(batch::Index::to_last_seqno) 223 | } 224 | _ => None, 225 | } 226 | } 227 | 228 | pub fn file_size(&self) -> Result { 229 | let n = match &self.inner { 230 | InnerJournal::Working { file, .. } => { 231 | let m = err_at!(IOError, file.metadata())?; 232 | err_at!(FailConvert, usize::try_from(m.len()))? 233 | } 234 | InnerJournal::Archive { .. } => unreachable!(), 235 | InnerJournal::Cold => unreachable!(), 236 | }; 237 | Ok(n) 238 | } 239 | 240 | pub fn to_state(&self) -> S 241 | where 242 | S: Clone, 243 | { 244 | match &self.inner { 245 | InnerJournal::Working { worker, .. } => worker.to_state(), 246 | InnerJournal::Archive { state, .. } => state.clone(), 247 | InnerJournal::Cold => unreachable!(), 248 | } 249 | } 250 | 251 | #[allow(dead_code)] 252 | pub fn to_file_path(&self) -> ffi::OsString { 253 | self.file_path.clone() 254 | } 255 | } 256 | 257 | pub struct RdJournal { 258 | range: ops::RangeInclusive, 259 | batch: vec::IntoIter, 260 | index: vec::IntoIter, 261 | entries: vec::IntoIter, 262 | file: fs::File, 263 | } 264 | 265 | impl RdJournal { 266 | pub fn from_journal( 267 | journal: &Journal, 268 | range: ops::RangeInclusive, 269 | ) -> Result { 270 | let (index, entries) = match &journal.inner { 271 | InnerJournal::Working { worker, .. } => { 272 | (worker.to_index(), worker.to_entries()) 273 | } 274 | InnerJournal::Archive { index, .. } => (index.to_vec(), vec![]), 275 | InnerJournal::Cold => unreachable!(), 276 | }; 277 | let batch: vec::IntoIter = vec![].into_iter(); 278 | let index = index 279 | .into_iter() 280 | .skip_while(|i| i.to_last_seqno() < *range.start()) 281 | .take_while(|i| i.to_first_seqno() <= *range.end()) 282 | .collect::>() 283 | .into_iter(); 284 | let entries = entries 285 | .into_iter() 286 | .filter(|e| range.contains(&e.to_seqno())) 287 | .collect::>() 288 | .into_iter(); 289 | 290 | let file = { 291 | let mut opts = fs::OpenOptions::new(); 292 | err_at!(IOError, opts.read(true).open(&journal.file_path))? 293 | }; 294 | 295 | Ok(RdJournal { range, batch, index, entries, file }) 296 | } 297 | } 298 | 299 | impl Iterator for RdJournal { 300 | type Item = Result; 301 | 302 | fn next(&mut self) -> Option { 303 | match self.batch.next() { 304 | Some(entry) => Some(Ok(entry)), 305 | None => match self.index.next() { 306 | Some(index) => match batch::Batch::from_index(index, &mut self.file) { 307 | Ok(batch) => { 308 | self.batch = batch.into_iter(self.range.clone()); 309 | self.next() 310 | } 311 | Err(err) => Some(Err(err)), 312 | }, 313 | None => self.entries.next().map(Ok), 314 | }, 315 | } 316 | } 317 | } 318 | 319 | #[cfg(test)] 320 | #[path = "journal_test.rs"] 321 | mod journal_test; 322 | -------------------------------------------------------------------------------- /src/journal_test.rs: -------------------------------------------------------------------------------- 1 | use arbitrary::Unstructured; 2 | use rand::{prelude::random, rngs::StdRng, Rng, SeedableRng}; 3 | 4 | use super::*; 5 | use crate::state; 6 | 7 | #[test] 8 | fn test_journal() { 9 | let seed: u64 = random(); 10 | println!("test_journal {}", seed); 11 | let mut rng = StdRng::seed_from_u64(seed); 12 | 13 | let name = "test_journal"; 14 | let dir = tempfile::tempdir().unwrap(); 15 | println!("test_journal {:?}", dir.path()); 16 | let mut jn = Journal::start(name, dir.path().as_ref(), 0, state::NoState).unwrap(); 17 | assert_eq!(jn.to_journal_number(), 0); 18 | assert_eq!(jn.len_batches(), 0); 19 | assert_eq!(jn.to_state(), state::NoState); 20 | 21 | let mut entries: Vec = (0..1_000_000) 22 | .map(|_i| { 23 | let bytes = rng.gen::<[u8; 32]>(); 24 | let mut uns = Unstructured::new(&bytes); 25 | uns.arbitrary::().unwrap() 26 | }) 27 | .collect(); 28 | entries.sort(); 29 | entries.dedup_by(|a, b| a.to_seqno() == b.to_seqno()); 30 | 31 | let mut n_batches = 0; 32 | let mut offset = 0; 33 | for _i in 0..1000 { 34 | let n = rng.gen::(); 35 | for _j in 0..n { 36 | let entry = entries[offset].clone(); 37 | jn.add_entry(entry.clone()).unwrap(); 38 | entries.push(entry); 39 | offset += 1; 40 | } 41 | 42 | assert_eq!(jn.to_last_seqno(), Some(entries[offset - 1].to_seqno())); 43 | 44 | jn.flush().unwrap(); 45 | if n > 0 { 46 | n_batches += 1; 47 | } 48 | 49 | assert_eq!(jn.to_last_seqno(), Some(entries[offset - 1].to_seqno())); 50 | } 51 | assert_eq!(n_batches, jn.len_batches()); 52 | 53 | let iter = RdJournal::from_journal(&jn, 0..=u64::MAX).unwrap(); 54 | let jn_entries: Vec = iter.map(|x| x.unwrap()).collect(); 55 | let entries = entries[..offset].to_vec(); 56 | assert_eq!(entries.len(), jn_entries.len()); 57 | assert_eq!(entries, jn_entries); 58 | 59 | { 60 | let (load_jn, _) = 61 | Journal::::load(name, &jn.to_file_path()).unwrap(); 62 | let iter = RdJournal::from_journal(&load_jn, 0..=u64::MAX).unwrap(); 63 | let jn_entries: Vec = iter.map(|x| x.unwrap()).collect(); 64 | let entries = entries[..offset].to_vec(); 65 | assert_eq!(entries.len(), jn_entries.len()); 66 | assert_eq!(entries, jn_entries); 67 | } 68 | 69 | jn.purge().unwrap(); 70 | dir.close().unwrap(); 71 | } 72 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Package implement Write-Ahead-Logging. 2 | //! 3 | //! Write-Ahead-Logging is implemented by [Wal] type, to get started create 4 | //! a configuration [Config] value. Subsequently, a fresh Wal instance can be 5 | //! created or existing Wal from disk can be loaded, using the configuration. 6 | //! Wal optionally takes a type parameter `S` for state, that can be used by 7 | //! application to persist storage state along with each batch. 8 | //! By default, `NoState` is used. 9 | //! 10 | //! Concurrent writers 11 | //! ------------------ 12 | //! 13 | //! [Wal] writes are batch-processed, where batching is automatically dictated 14 | //! by storage (disk, ssd) latency. Latency can get higher when `fsync` is 15 | //! enabled for every batch flush. With fsync enabled it is hard to reduce 16 | //! the latency, and to get better throughput applications can do concurrent 17 | //! writes. This is possible because [Wal] type can be cloned with underlying 18 | //! structure safely shared among all the clones. For example, 19 | //! 20 | //! ```ignore 21 | //! let wal = wral::Wal::create(config, wral::NoState).unwrap(); 22 | //! let mut writers = vec![]; 23 | //! for id in 0..n_threads { 24 | //! let wal = wal.clone(); 25 | //! writers.push(std::thread::spawn(move || writer(id, wal))); 26 | //! } 27 | //! ``` 28 | //! 29 | //! Application employing concurrent [Wal] must keep in mind that `seqno` 30 | //! generated for consecutive ops may not be monotonically increasing within 31 | //! the same thread, and must make sure to serialize operations across the 32 | //! writers through other means. 33 | //! 34 | //! Concurrent readers 35 | //! ------------------ 36 | //! 37 | //! It is possible for a [Wal] value and its clones to concurrently read the 38 | //! log journal (typically iterating over its entries). Remember that read 39 | //! operations shall block concurrent writes and vice-versa. But concurrent 40 | //! reads shall be allowed. 41 | 42 | use std::{error, fmt, result}; 43 | 44 | // Short form to compose Error values. 45 | // 46 | // Here are few possible ways: 47 | // 48 | // ```ignore 49 | // use crate::Error; 50 | // err_at!(ParseError, msg: format!("bad argument")); 51 | // ``` 52 | // 53 | // ```ignore 54 | // use crate::Error; 55 | // err_at!(ParseError, std::io::read(buf)); 56 | // ``` 57 | // 58 | // ```ignore 59 | // use crate::Error; 60 | // err_at!(ParseError, std::fs::read(file_path), format!("read failed")); 61 | // ``` 62 | // 63 | macro_rules! err_at { 64 | ($v:ident, msg: $($arg:expr),+) => {{ 65 | let prefix = format!("{}:{}", file!(), line!()); 66 | Err(Error::$v(prefix, format!($($arg),+))) 67 | }}; 68 | ($v:ident, $e:expr) => {{ 69 | match $e { 70 | Ok(val) => Ok(val), 71 | Err(err) => { 72 | let prefix = format!("{}:{}", file!(), line!()); 73 | Err(Error::$v(prefix, format!("{}", err))) 74 | } 75 | } 76 | }}; 77 | ($v:ident, $e:expr, $($arg:expr),+) => {{ 78 | match $e { 79 | Ok(val) => Ok(val), 80 | Err(err) => { 81 | let prefix = format!("{}:{}", file!(), line!()); 82 | let msg = format!($($arg),+); 83 | Err(Error::$v(prefix, format!("{} {}", err, msg))) 84 | } 85 | } 86 | }}; 87 | } 88 | 89 | mod batch; 90 | mod entry; 91 | mod files; 92 | mod journal; 93 | mod state; 94 | mod util; 95 | mod wral; 96 | mod writer; 97 | 98 | pub use crate::entry::Entry; 99 | pub use crate::state::{NoState, State}; 100 | pub use crate::wral::Config; 101 | pub use crate::wral::Wal; 102 | 103 | /// Type alias for Result return type, used by this package. 104 | pub type Result = result::Result; 105 | 106 | /// Error variants that can be returned by this package's API. 107 | /// 108 | /// Each variant carries a prefix, typically identifying the 109 | /// error location. 110 | pub enum Error { 111 | FailConvert(String, String), 112 | FailCbor(String, String), 113 | IOError(String, String), 114 | Fatal(String, String), 115 | Invalid(String, String), 116 | IPCFail(String, String), 117 | ThreadFail(String, String), 118 | } 119 | 120 | impl fmt::Display for Error { 121 | fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { 122 | use Error::*; 123 | 124 | match self { 125 | FailConvert(p, msg) => write!(f, "{} FailConvert: {}", p, msg), 126 | FailCbor(p, msg) => write!(f, "{} FailCbor: {}", p, msg), 127 | IOError(p, msg) => write!(f, "{} IOError: {}", p, msg), 128 | Fatal(p, msg) => write!(f, "{} Fatal: {}", p, msg), 129 | Invalid(p, msg) => write!(f, "{} Invalid: {}", p, msg), 130 | IPCFail(p, msg) => write!(f, "{} IPCFail: {}", p, msg), 131 | ThreadFail(p, msg) => write!(f, "{} ThreadFail: {}", p, msg), 132 | } 133 | } 134 | } 135 | 136 | impl fmt::Debug for Error { 137 | fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { 138 | write!(f, "{}", self) 139 | } 140 | } 141 | 142 | impl error::Error for Error {} 143 | 144 | impl From for Error { 145 | fn from(err: mkit::Error) -> Error { 146 | match err { 147 | mkit::Error::Fatal(p, m) => Error::Fatal(p, m), 148 | mkit::Error::FailConvert(p, m) => Error::FailConvert(p, m), 149 | mkit::Error::IOError(p, m) => Error::IOError(p, m), 150 | mkit::Error::FailCbor(p, m) => Error::FailCbor(p, m), 151 | mkit::Error::IPCFail(p, m) => Error::IPCFail(p, m), 152 | mkit::Error::ThreadFail(p, m) => Error::ThreadFail(p, m), 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/marker.rs: -------------------------------------------------------------------------------- 1 | lazy_static! { 2 | static ref DLOG_BATCH_MARKER: Vec = { 3 | let marker = "செய்வன திருந்தச் செய்"; 4 | marker.as_bytes().to_vec() 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use mkit::{ 2 | cbor::{FromCbor, IntoCbor}, 3 | Cborize, 4 | }; 5 | 6 | #[allow(unused_imports)] 7 | use crate::wral::Wal; 8 | use crate::{entry::Entry, Result}; 9 | 10 | /// Callback trait for updating application state in relation to [Wal] type. 11 | pub trait State: 'static + Clone + Sync + Send + IntoCbor + FromCbor + Default { 12 | fn on_add_entry(&mut self, new_entry: &Entry) -> Result<()>; 13 | } 14 | 15 | /// Default parameter, implementing [State] trait, for [Wal] type. 16 | #[derive(Clone, Eq, PartialEq, Debug, Cborize, Default)] 17 | pub struct NoState; 18 | 19 | impl NoState { 20 | const ID: u32 = 0x0; 21 | } 22 | 23 | impl State for NoState { 24 | fn on_add_entry(&mut self, _: &Entry) -> Result<()> { 25 | Ok(()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use mkit::cbor::IntoCbor; 2 | 3 | use std::{fs, io::Write}; 4 | 5 | use crate::{Error, Result}; 6 | 7 | pub fn encode_cbor(val: T) -> Result> 8 | where 9 | T: IntoCbor, 10 | { 11 | let mut data: Vec = vec![]; 12 | let n = val.into_cbor()?.encode(&mut data)?; 13 | if n != data.len() { 14 | err_at!(Fatal, msg: "cbor encoding len mistmatch {} {}", n, data.len()) 15 | } else { 16 | Ok(data) 17 | } 18 | } 19 | 20 | pub fn sync_write(file: &mut fs::File, data: &[u8]) -> Result { 21 | let n = err_at!(IOError, file.write(data))?; 22 | if n != data.len() { 23 | err_at!(IOError, msg: "partial write to file {} {}", n, data.len())? 24 | } 25 | err_at!(IOError, file.sync_all())?; 26 | Ok(n) 27 | } 28 | -------------------------------------------------------------------------------- /src/wral.rs: -------------------------------------------------------------------------------- 1 | //! Package implement WRite-Ahead-Logging. 2 | //! 3 | //! Entries are added to `Wal` journal. Journals automatically rotate 4 | //! and are numbered from ZERO. 5 | 6 | use arbitrary::{Arbitrary, Unstructured}; 7 | use log::debug; 8 | use mkit::{self, thread}; 9 | 10 | use std::{ 11 | ffi, fs, mem, ops, path, 12 | sync::{Arc, RwLock}, 13 | vec, 14 | }; 15 | 16 | use crate::{entry, journal, journal::Journal, state, writer, Error, Result}; 17 | 18 | /// Default journal file limit is set at 1GB. 19 | pub const JOURNAL_LIMIT: usize = 1024 * 1024 * 1024; 20 | /// Default channel buffer for writer thread. 21 | pub const SYNC_BUFFER: usize = 1024; 22 | 23 | /// Configuration for [Wal] type. 24 | #[derive(Debug, Clone)] 25 | pub struct Config { 26 | /// Uniquely name Wal instances. 27 | pub name: String, 28 | /// Directory in which wral journals are stored. 29 | pub dir: ffi::OsString, 30 | /// Define file-size limit for a single journal file, beyond with 31 | /// journal files are rotated. 32 | pub journal_limit: usize, 33 | /// Enable fsync for every flush. 34 | pub fsync: bool, 35 | } 36 | 37 | impl Arbitrary for Config { 38 | fn arbitrary(u: &mut Unstructured) -> arbitrary::Result { 39 | let name: String = u.arbitrary()?; 40 | let dir = tempfile::tempdir().unwrap().path().into(); 41 | 42 | let journal_limit = *u.choose(&[100, 1000, 10_000, 1_000_000])?; 43 | let fsync: bool = u.arbitrary()?; 44 | 45 | let config = Config { name, dir, journal_limit, fsync }; 46 | Ok(config) 47 | } 48 | } 49 | 50 | impl Config { 51 | pub fn new(name: &str, dir: &ffi::OsStr) -> Config { 52 | Config { 53 | name: name.to_string(), 54 | dir: dir.to_os_string(), 55 | journal_limit: JOURNAL_LIMIT, 56 | fsync: true, 57 | } 58 | } 59 | 60 | pub fn set_journal_limit(&mut self, journal_limit: usize) -> &mut Self { 61 | self.journal_limit = journal_limit; 62 | self 63 | } 64 | 65 | pub fn set_fsync(&mut self, fsync: bool) -> &mut Self { 66 | self.fsync = fsync; 67 | self 68 | } 69 | } 70 | 71 | /// Write ahead logging. 72 | pub struct Wal { 73 | config: Config, 74 | 75 | tx: thread::Tx, 76 | t: Arc>>>, 77 | w: Arc>>, 78 | } 79 | 80 | impl Clone for Wal { 81 | fn clone(&self) -> Wal { 82 | Wal { 83 | config: self.config.clone(), 84 | 85 | tx: self.tx.clone(), 86 | t: Arc::clone(&self.t), 87 | w: Arc::clone(&self.w), 88 | } 89 | } 90 | } 91 | 92 | impl Wal { 93 | /// Create a new Write-Ahead-Log instance, while create a new journal, 94 | /// older journals matching the `name` shall be purged. 95 | pub fn create(config: Config, state: S) -> Result> 96 | where 97 | S: state::State, 98 | { 99 | // try creating the directory, if it does not exist. 100 | fs::create_dir_all(&config.dir).ok(); 101 | 102 | // purge existing journals for this shard. 103 | for item in err_at!(IOError, fs::read_dir(&config.dir))? { 104 | let file_path: path::PathBuf = { 105 | let file_name = err_at!(IOError, item)?.file_name(); 106 | [config.dir.clone(), file_name.clone()].iter().collect() 107 | }; 108 | match Journal::::load_cold(&config.name, file_path.as_ref()) { 109 | Some(journal) => match journal.purge() { 110 | Ok(_) => (), 111 | Err(err) => { 112 | debug!(target: "wral", "failed to purge {:?}, {}", file_path, err) 113 | } 114 | }, 115 | None => continue, 116 | }; 117 | } 118 | 119 | let num = 0; 120 | let journal = Journal::start(&config.name, &config.dir, num, state)?; 121 | 122 | debug!(target: "wral", "{:?}/{} created", &config.dir, &config.name); 123 | 124 | let seqno = 1; 125 | let (w, t, tx) = writer::Writer::start(config.clone(), vec![], journal, seqno); 126 | 127 | let val = Wal { config, tx, t: Arc::new(RwLock::new(t)), w }; 128 | 129 | Ok(val) 130 | } 131 | 132 | /// Load an existing journal under `dir`, matching `name`. Files that 133 | /// don't match the journal file-name structure or journals with 134 | /// corrupted batch or corrupted state shall be ignored. 135 | /// 136 | /// Application state shall be loaded from the last batch of the 137 | /// last journal. 138 | pub fn load(config: Config) -> Result> 139 | where 140 | S: state::State, 141 | { 142 | let mut journals: Vec<(Journal, u64, S)> = vec![]; 143 | for item in err_at!(IOError, fs::read_dir(&config.dir))? { 144 | let file_path: path::PathBuf = { 145 | let file_name = err_at!(IOError, item)?.file_name(); 146 | [config.dir.clone(), file_name.clone()].iter().collect() 147 | }; 148 | match Journal::load(&config.name, file_path.as_ref()) { 149 | Some((journal, state)) => { 150 | let seqno = journal.to_last_seqno().unwrap(); 151 | journals.push((journal, seqno, state)); 152 | } 153 | None => debug!(target: "wral", "failed to load {:?}", file_path), 154 | }; 155 | } 156 | 157 | journals.sort_by(|(_, a, _), (_, b, _)| a.cmp(b)); 158 | 159 | let (mut seqno, num, state) = match journals.last() { 160 | Some((j, seqno, state)) => (*seqno, j.to_journal_number(), state.clone()), 161 | None => (0, 0, S::default()), 162 | }; 163 | seqno += 1; 164 | let num = num.saturating_add(1); 165 | let journal = Journal::start(&config.name, &config.dir, num, state)?; 166 | 167 | let n_batches: usize = journals.iter().map(|(j, _, _)| j.len_batches()).sum(); 168 | debug!( 169 | target: "wral", 170 | "{:?}/{} loaded with {} journals, {} batches", 171 | config.dir, config.name, journals.len(), n_batches 172 | ); 173 | 174 | let journals: Vec> = journals.into_iter().map(|(j, _, _)| j).collect(); 175 | let (w, t, tx) = writer::Writer::start(config.clone(), journals, journal, seqno); 176 | 177 | let val = Wal { config, tx, t: Arc::new(RwLock::new(t)), w }; 178 | 179 | Ok(val) 180 | } 181 | 182 | /// Close the [Wal] instance. To purge the instance pass `purge` as true. 183 | pub fn close(self, purge: bool) -> Result> { 184 | match Arc::try_unwrap(self.t) { 185 | Ok(t) => { 186 | mem::drop(self.tx); 187 | (err_at!(IPCFail, t.into_inner())?.join()?)?; 188 | 189 | match Arc::try_unwrap(self.w) { 190 | Ok(w) => { 191 | let w = err_at!(IPCFail, w.into_inner())?; 192 | Ok(Some(if purge { w.purge()? } else { w.close()? })) 193 | } 194 | Err(_) => Ok(None), // there are active clones 195 | } 196 | } 197 | Err(_) => Ok(None), // there are active clones 198 | } 199 | } 200 | } 201 | 202 | impl Wal { 203 | /// Add a operation to WAL, operations are pre-serialized and opaque to 204 | /// Wal instances. Return the sequence-number for this operation. 205 | pub fn add_op(&self, op: &[u8]) -> Result { 206 | let req = writer::Req::AddEntry { op: op.to_vec() }; 207 | let writer::Res::Seqno(seqno) = self.tx.request(req)?; 208 | Ok(seqno) 209 | } 210 | } 211 | 212 | impl Wal { 213 | /// Iterate over all entries in this Wal instance, entries can span 214 | /// across multiple journal files. Iteration will start from lowest 215 | /// sequence-number to highest. 216 | pub fn iter(&self) -> Result>> { 217 | self.range(..) 218 | } 219 | 220 | /// Iterate over entries whose sequence number fall within the 221 | /// specified `range`. 222 | pub fn range(&self, range: R) -> Result>> 223 | where 224 | R: ops::RangeBounds, 225 | { 226 | let journals = match Self::range_bound_to_range_inclusive(range) { 227 | Some(range) => { 228 | let rd = err_at!(Fatal, self.w.read())?; 229 | let mut journals = vec![]; 230 | for jn in rd.journals.iter() { 231 | journals.push(journal::RdJournal::from_journal(jn, range.clone())?); 232 | } 233 | journals.push(journal::RdJournal::from_journal(&rd.journal, range)?); 234 | journals 235 | } 236 | None => vec![], 237 | }; 238 | 239 | Ok(Iter { journal: None, journals: journals.into_iter() }) 240 | } 241 | 242 | fn range_bound_to_range_inclusive(range: R) -> Option> 243 | where 244 | R: ops::RangeBounds, 245 | { 246 | let start = match range.start_bound() { 247 | ops::Bound::Excluded(start) if *start < u64::MAX => Some(*start + 1), 248 | ops::Bound::Excluded(_) => None, 249 | ops::Bound::Included(start) => Some(*start), 250 | ops::Bound::Unbounded => Some(0), 251 | }?; 252 | let end = match range.end_bound() { 253 | ops::Bound::Excluded(0) => None, 254 | ops::Bound::Excluded(end) => Some(*end - 1), 255 | ops::Bound::Included(end) => Some(*end), 256 | ops::Bound::Unbounded => Some(u64::MAX), 257 | }?; 258 | Some(start..=end) 259 | } 260 | } 261 | 262 | struct Iter { 263 | journal: Option, 264 | journals: vec::IntoIter, 265 | } 266 | 267 | impl Iterator for Iter { 268 | type Item = Result; 269 | 270 | fn next(&mut self) -> Option { 271 | let mut journal = match self.journal.take() { 272 | Some(journal) => journal, 273 | None => self.journals.next()?, 274 | }; 275 | loop { 276 | match journal.next() { 277 | Some(item) => { 278 | self.journal = Some(journal); 279 | return Some(item); 280 | } 281 | None => match self.journals.next() { 282 | Some(j) => { 283 | journal = j; 284 | } 285 | None => { 286 | return None; 287 | } 288 | }, 289 | } 290 | } 291 | } 292 | } 293 | 294 | #[cfg(test)] 295 | #[path = "wral_test.rs"] 296 | mod wral_test; 297 | -------------------------------------------------------------------------------- /src/wral_test.rs: -------------------------------------------------------------------------------- 1 | use arbitrary::Unstructured; 2 | use rand::{prelude::random, rngs::StdRng, Rng, SeedableRng}; 3 | 4 | use super::*; 5 | 6 | #[test] 7 | fn test_wal() { 8 | let seed: u64 = random(); 9 | println!("test_wal {}", seed); 10 | let mut rng = StdRng::seed_from_u64(seed); 11 | 12 | let mut config: Config = { 13 | let bytes = rng.gen::<[u8; 32]>(); 14 | let mut uns = Unstructured::new(&bytes); 15 | uns.arbitrary().unwrap() 16 | }; 17 | config.name = "test-wal".to_string(); 18 | let dir = tempfile::tempdir().unwrap(); 19 | config.dir = dir.path().into(); 20 | 21 | println!("{:?}", config); 22 | let val = Wal::create(config, state::NoState).unwrap(); 23 | 24 | let n_threads = 1; 25 | 26 | let mut writers = vec![]; 27 | for id in 0..n_threads { 28 | let wal = val.clone(); 29 | writers 30 | .push(std::thread::spawn(move || writer(id, wal, 1000, seed + (id as u64)))); 31 | } 32 | 33 | let mut entries: Vec> = vec![]; 34 | for handle in writers { 35 | entries.push(handle.join().unwrap()); 36 | } 37 | let entries: Vec = entries.into_iter().flatten().collect(); 38 | 39 | let n = entries.len() as u64; 40 | let sum = entries.iter().map(|e| e.to_seqno()).sum::(); 41 | assert_eq!(sum, (n * (n + 1)) / 2); 42 | 43 | let mut readers = vec![]; 44 | for id in 0..n_threads { 45 | let wal = val.clone(); 46 | let entries = entries.clone(); 47 | readers.push(std::thread::spawn(move || { 48 | reader(id, wal, 10, seed + (id as u64), entries) 49 | })); 50 | } 51 | 52 | for handle in readers { 53 | handle.join().unwrap(); 54 | } 55 | 56 | val.close(true).unwrap(); 57 | } 58 | 59 | fn writer(_id: u128, wal: Wal, ops: usize, seed: u64) -> Vec { 60 | let mut rng = StdRng::seed_from_u64(seed); 61 | 62 | let mut entries = vec![]; 63 | for _i in 1..ops { 64 | let op: Vec = { 65 | let bytes = rng.gen::<[u8; 32]>(); 66 | let mut uns = Unstructured::new(&bytes); 67 | uns.arbitrary().unwrap() 68 | }; 69 | let seqno = wal.add_op(&op).unwrap(); 70 | entries.push(entry::Entry::new(seqno, op)); 71 | } 72 | 73 | entries 74 | } 75 | 76 | fn reader(_id: u128, wal: Wal, ops: usize, seed: u64, entries: Vec) { 77 | let mut rng = StdRng::seed_from_u64(seed); 78 | 79 | for _i in 0..ops { 80 | match rng.gen::() % 2 { 81 | 0 => { 82 | let items: Vec = 83 | wal.iter().unwrap().map(|x| x.unwrap()).collect(); 84 | assert_eq!(items, entries); 85 | } 86 | 1 => { 87 | let start = rng.gen::() % entries.len(); 88 | let end = start + (rng.gen::() % (entries.len() - start)); 89 | let (x, y) = (entries[start].to_seqno(), entries[end].to_seqno()); 90 | let items: Vec = 91 | wal.range(x..y).unwrap().map(|x| x.unwrap()).collect(); 92 | assert_eq!(items, entries[start..end]); 93 | } 94 | _ => unreachable!(), 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/writer.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | use mkit::{ 3 | cbor::{FromCbor, IntoCbor}, 4 | thread, 5 | }; 6 | 7 | use std::{ 8 | borrow::BorrowMut, 9 | mem, 10 | sync::{ 11 | atomic::{AtomicU64, Ordering::SeqCst}, 12 | Arc, RwLock, 13 | }, 14 | }; 15 | 16 | use crate::{entry, journal::Journal, state, wral, wral::Config, Error, Result}; 17 | 18 | #[derive(Debug)] 19 | pub enum Req { 20 | AddEntry { op: Vec }, 21 | } 22 | 23 | #[derive(Debug)] 24 | pub enum Res { 25 | Seqno(u64), 26 | } 27 | 28 | pub struct Writer { 29 | config: Config, 30 | seqno: Arc, 31 | pub journals: Vec>, 32 | pub journal: Journal, 33 | } 34 | 35 | type SpawnWriter = ( 36 | Arc>>, 37 | thread::Thread>, 38 | thread::Tx, 39 | ); 40 | 41 | impl Writer { 42 | pub(crate) fn start( 43 | config: Config, 44 | journals: Vec>, 45 | journal: Journal, 46 | seqno: u64, 47 | ) -> SpawnWriter 48 | where 49 | S: state::State, 50 | { 51 | let seqno = Arc::new(AtomicU64::new(seqno)); 52 | let w = Arc::new(RwLock::new(Writer { 53 | config: config.clone(), 54 | seqno: Arc::clone(&seqno), 55 | journals, 56 | journal, 57 | })); 58 | let name = format!("wral-writer-{}", config.name); 59 | let thread_w = Arc::clone(&w); 60 | let (t, tx) = thread::Thread::new_sync( 61 | &name, 62 | wral::SYNC_BUFFER, 63 | move |rx: thread::Rx| { 64 | || { 65 | let l = MainLoop { config, seqno, w: thread_w, rx }; 66 | l.run() 67 | } 68 | }, 69 | ); 70 | 71 | (w, t, tx) 72 | } 73 | 74 | pub fn close(&self) -> Result { 75 | let n_batches: usize = self.journals.iter().map(|j| j.len_batches()).sum(); 76 | let (n, m) = match self.journal.len_batches() { 77 | 0 => (self.journals.len(), n_batches), 78 | n => (self.journals.len() + 1, n_batches + n), 79 | }; 80 | let seqno = self.seqno.load(SeqCst); 81 | debug!( 82 | target: "wral", 83 | "{:?}/{} closed at seqno {}, with {} journals and {} batches", 84 | self.config.dir, self.config.name, seqno, m, n 85 | ); 86 | Ok(self.seqno.load(SeqCst).saturating_sub(1)) 87 | } 88 | 89 | pub fn purge(mut self) -> Result { 90 | self.close()?; 91 | 92 | for j in self.journals.drain(..) { 93 | j.purge()? 94 | } 95 | self.journal.purge()?; 96 | 97 | Ok(self.seqno.load(SeqCst).saturating_sub(1)) 98 | } 99 | } 100 | 101 | struct MainLoop { 102 | config: Config, 103 | seqno: Arc, 104 | w: Arc>>, 105 | rx: thread::Rx, 106 | } 107 | 108 | impl MainLoop 109 | where 110 | S: Clone + IntoCbor + FromCbor + state::State, 111 | { 112 | fn run(self) -> Result { 113 | use std::sync::mpsc::TryRecvError; 114 | 115 | // block for the first request. 116 | 'a: while let Ok(req) = self.rx.recv() { 117 | // then get as many outstanding requests as possible from 118 | // the channel. 119 | let mut reqs = vec![req]; 120 | loop { 121 | match self.rx.try_recv() { 122 | Ok(req) => reqs.push(req), 123 | Err(TryRecvError::Empty) => break, 124 | Err(TryRecvError::Disconnected) => break 'a, 125 | } 126 | } 127 | // and then start processing it in batch. 128 | let mut w = err_at!(Fatal, self.w.write())?; 129 | 130 | let mut items = vec![]; 131 | for req in reqs.into_iter() { 132 | match req { 133 | (Req::AddEntry { op }, tx) => { 134 | let seqno = self.seqno.fetch_add(1, SeqCst); 135 | w.journal.add_entry(entry::Entry::new(seqno, op))?; 136 | items.push((seqno, tx)) 137 | } 138 | } 139 | } 140 | w.journal.flush()?; 141 | 142 | for (seqno, tx) in items.into_iter() { 143 | if let Some(tx) = tx { 144 | err_at!(IPCFail, tx.send(Res::Seqno(seqno)))?; 145 | } 146 | } 147 | 148 | if w.journal.file_size()? > self.config.journal_limit { 149 | Self::rotate(w.borrow_mut())?; 150 | } 151 | } 152 | 153 | Ok(self.seqno.load(SeqCst).saturating_sub(1)) 154 | } 155 | } 156 | 157 | impl MainLoop 158 | where 159 | S: Clone, 160 | { 161 | fn rotate(w: &mut Writer) -> Result<()> { 162 | // new journal 163 | let journal = { 164 | let num = w.journal.to_journal_number().saturating_add(1); 165 | let state = w.journal.to_state(); 166 | Journal::start(&w.config.name, &w.config.dir, num, state)? 167 | }; 168 | // replace with current journal 169 | let journal = mem::replace(&mut w.journal, journal); 170 | let (journal, entries, _) = journal.into_archive(); 171 | if !entries.is_empty() { 172 | err_at!(Fatal, msg: "unflushed entries {}", entries.len())? 173 | } 174 | w.journals.push(journal); 175 | Ok(()) 176 | } 177 | } 178 | --------------------------------------------------------------------------------