1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
//! The index, view and table catalog as recorded state, the way the
//! server keeps it: every change is recorded as one internal frame
//! carrying the whole catalog, `XINTERNAL.CATALOG <lineage> <version>
//! <index> <view> <table>`, through the write path (log, replicas, feed);
//! every snapshot and rewritten log carries the current one; a frame is
//! applied when it is newer than what the store holds, and a replica's
//! full sync takes its snapshot's frame the same way, except that a frame
//! from another lineage (another primary) replaces the catalog. A primary
//! gets its lineage when it opens. A server replica applies the frames an
//! embedded primary records, and the other way round.
use std::sync::{Arc, Mutex, PoisonError};
use kevy_index::{Catalog, TableCatalog, ViewCatalog};
use kevy_resp::Argv;
use crate::KevyResult;
use crate::ops_index::IndexReg;
use crate::ops_table::TableReg;
use crate::ops_view::ViewReg;
use crate::store::{Store, lock_write};
/// The three registries and where the catalog stands as recorded state,
/// shared by the store and every shard (the replica runner and the
/// snapshot writers reach it through a shard).
#[derive(Debug)]
pub(crate) struct CatalogRegs {
pub(crate) indexes: Arc<IndexReg>,
pub(crate) views: Arc<ViewReg>,
pub(crate) tables: Arc<TableReg>,
/// `(lineage, version)`; lineage 0 = nothing recorded yet.
at: Mutex<(u64, u64)>,
}
impl CatalogRegs {
pub(crate) fn new(tables: Arc<TableReg>) -> Self {
Self { indexes: Arc::default(), views: Arc::default(), tables, at: Mutex::new((0, 0)) }
}
fn texts(&self) -> [String; 3] {
[
self.indexes.catalog.read().unwrap_or_else(PoisonError::into_inner).1.to_sidecar(),
self.views.catalog.read().unwrap_or_else(PoisonError::into_inner).1.to_sidecar(),
self.tables.catalog.read().unwrap_or_else(PoisonError::into_inner).to_sidecar(),
]
}
fn frame(&self, at: (u64, u64)) -> Vec<Vec<u8>> {
let [index, view, table] = self.texts();
vec![
kevy_resp::ops_table::CATALOG.as_bytes().to_vec(),
at.0.to_string().into_bytes(),
at.1.to_string().into_bytes(),
index.into_bytes(),
view.into_bytes(),
table.into_bytes(),
]
}
#[cfg(feature = "persist")]
fn at(&self) -> (u64, u64) {
*self.at.lock().unwrap_or_else(PoisonError::into_inner)
}
#[cfg(feature = "persist")]
/// The frame a snapshot or a rewritten log keeps beside the keyspace:
/// one always, an empty catalog included, so a snapshot's lineage says
/// whose catalog it holds.
pub(crate) fn aux(&self) -> Argv {
Argv::from(self.frame(self.at()))
}
/// Give a primary that has recorded nothing its lineage, once its
/// restore is done (a restored frame may still bring one until then);
/// a replica takes its primary's.
pub(crate) fn mint(&self) {
mint(&mut self.at.lock().unwrap_or_else(PoisonError::into_inner));
}
/// Take a catalog frame when it is newer than what the store holds,
/// or, on a full sync, whenever it comes from another lineage, so a
/// full sync served from a snapshot older than the frames already
/// applied leaves the catalog as they did. A malformed frame is
/// skipped, the way a replay skips a frame it cannot apply. Only a 6.4
/// server sends a full sync with no frame: it replicates no catalog, so
/// the store holds none.
pub(crate) fn adopt(&self, frame: Option<&Argv>, full_sync: bool) {
let mut held = self.at.lock().unwrap_or_else(PoisonError::into_inner);
let Some(frame) = frame else {
if full_sync && held.0 != 0 {
self.install(Catalog::new(), ViewCatalog::new(), TableCatalog::new());
*held = (0, 0);
}
return;
};
let Some((at, icat, vcat, tcat)) = decode(frame) else { return };
if at > *held || full_sync && at.0 != held.0 {
self.install(icat, vcat, tcat);
*held = at;
}
}
/// Take a catalog frame a host feeds back from the log it keeps
/// itself ([`Store::apply_frame`]): when it is newer than what the
/// store holds, or whenever this store has recorded no catalog of its
/// own. The second case is the open-time restore, which for such a
/// host arrives after the open has already given the store its
/// lineage; a native open takes the same frame before that. A
/// malformed frame is skipped.
#[cfg(feature = "persist")]
pub(crate) fn adopt_fed(&self, frame: &Argv) {
let mut held = self.at.lock().unwrap_or_else(PoisonError::into_inner);
let Some((at, icat, vcat, tcat)) = decode(frame) else { return };
if held.1 == 0 || at > *held {
self.install(icat, vcat, tcat);
*held = at;
}
}
/// The frame a host's compacted image carries, once this store holds
/// a recorded catalog: `None` while nothing has been recorded, so an
/// image of a store that never declared an index is only its keys.
#[cfg(feature = "persist")]
pub(crate) fn fed_aux(&self) -> Option<Argv> {
let at = self.at();
(at.1 > 0).then(|| Argv::from(self.frame(at)))
}
/// Install each catalog that differs from the one held; the shards
/// rebuild what changed on their next touch.
fn install(&self, icat: Catalog, vcat: ViewCatalog, tcat: TableCatalog) {
let [index, view, table] = self.texts();
if index != icat.to_sidecar() {
let mut g = self.indexes.catalog.write().unwrap_or_else(PoisonError::into_inner);
*g = (g.0 + 1, icat);
}
if table != tcat.to_sidecar() {
*self.tables.catalog.write().unwrap_or_else(PoisonError::into_inner) = tcat;
self.tables.advise.lock().unwrap_or_else(PoisonError::into_inner).clear();
}
if view != vcat.to_sidecar() {
let mut g = self.views.catalog.write().unwrap_or_else(PoisonError::into_inner);
*g = (g.0 + 1, vcat);
}
}
/// Move to the next version, minting a lineage on the first.
fn next_version(&self) -> (u64, u64) {
let mut at = self.at.lock().unwrap_or_else(PoisonError::into_inner);
mint(&mut at);
at.1 += 1;
*at
}
}
/// Give a store that has recorded nothing its lineage.
fn mint(at: &mut (u64, u64)) {
if at.0 == 0 {
at.0 = kevy_store::now_unix_ms().max(1);
}
}
/// `(lineage, version)` and the three catalogs a frame carries.
fn decode(frame: &Argv) -> Option<((u64, u64), Catalog, ViewCatalog, TableCatalog)> {
let num = |i: usize| std::str::from_utf8(frame.get(i)?).ok()?.parse::<u64>().ok();
let text = |i: usize| std::str::from_utf8(frame.get(i)?).ok();
let (index, view, table) = (text(3)?, text(4)?, text(5)?);
let icat = if index.is_empty() { Catalog::new() } else { Catalog::from_sidecar(index)? };
let vcat = if view.is_empty() { ViewCatalog::new() } else { ViewCatalog::from_sidecar(view)? };
let tcat =
if table.is_empty() { TableCatalog::new() } else { TableCatalog::from_sidecar(table)? };
Some(((num(1)?, num(2)?), icat, vcat, tcat))
}
impl Store {
/// Run one catalog command: refused on a replica or a closed store,
/// and recorded once when it changed the catalog (a failure part way
/// through records what it left).
pub(crate) fn catalog_change<T>(&self, f: impl FnOnce() -> KevyResult<T>) -> KevyResult<T> {
crate::store::ensure_writable(self)?;
let before = self.guard.catalog.texts();
let out = f();
if self.guard.catalog.texts() != before {
self.record_catalog()?;
}
out
}
/// Record the catalog as it now stands, through shard 0's write path.
fn record_catalog(&self) -> KevyResult<()> {
let regs = &self.guard.catalog;
let frame = regs.frame(regs.next_version());
let parts: Vec<&[u8]> = frame.iter().map(Vec::as_slice).collect();
crate::store::commit_write(&mut lock_write(&self.shards[0]), &parts)
}
}
#[cfg(test)]
#[cfg(feature = "persist")]
#[path = "catalog_record_tests.rs"]
mod tests;
#[cfg(feature = "persist")]
mod sidecars {
use std::path::Path;
use kevy_index::{Catalog, TableCatalog, ViewCatalog};
use crate::KevyResult;
use crate::store::{Store, lock_write};
/// The files a 6.4 directory kept the catalog in.
const SIDECARS: [&str; 3] = ["index-catalog.meta", "view-catalog.meta", "table-catalog.meta"];
/// What one sidecar holds: empty when absent, `None` when present but
/// unreadable.
fn read<T: Default>(dir: &Path, name: &str, parse: impl Fn(&str) -> Option<T>) -> Option<T> {
match std::fs::read_to_string(dir.join(name)) {
Ok(text) => parse(&text),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Some(T::default()),
Err(_) => None,
}
}
impl Store {
/// Open-time settling of the files a 6.4 directory kept its
/// catalog in: superseded when the logs or snapshots held a
/// recorded change (a version above 0: every open gives a primary
/// a lineage), otherwise read once and recorded, and removed once
/// the record is on disk.
pub(crate) fn settle_sidecars(&self) -> KevyResult<()> {
let Some(dir) = crate::store_wire::disk_dir(&self.config).cloned() else {
return Ok(());
};
if self.guard.catalog.at().1 == 0 && !self.import_sidecars(&dir)? {
return Ok(());
}
for name in SIDECARS {
// absent is the usual case
drop(std::fs::remove_file(dir.join(name)));
}
Ok(())
}
/// Install and record what the sidecars in `dir` hold; `true` once
/// the record is on disk, or when there was nothing to import. A
/// replica takes its catalog from its primary, and a sidecar that
/// does not parse is left in place for someone to look at.
fn import_sidecars(&self, dir: &Path) -> KevyResult<bool> {
#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
if self.is_replica() {
return Ok(false);
}
let icat = read(dir, SIDECARS[0], Catalog::from_sidecar);
let vcat = read(dir, SIDECARS[1], ViewCatalog::from_sidecar);
let tcat = read(dir, SIDECARS[2], TableCatalog::from_sidecar);
let (Some(icat), Some(vcat), Some(tcat)) = (icat, vcat, tcat) else {
eprintln!(
"kevy: a catalog file in {} does not parse; it stays in place, not imported",
dir.display()
);
return Ok(false);
};
if icat.is_empty() && vcat.is_empty() && tcat.is_empty() {
return Ok(true);
}
self.guard.catalog.install(icat, vcat, tcat);
self.record_catalog()?;
match lock_write(&self.shards[0]).aof.as_mut() {
Some(aof) => {
aof.sync_now()?;
Ok(true)
}
None => Ok(false),
}
}
}
}