P3: the books' format moves only with the owner's go-ahead (D-39)
Books an update could write a newer format for open on their old format and are asked at every open; anything needing a new feature is refused until the owner agrees with the passphrase, after a verified backup that includes the keys. New books start on the current format. Each projection schema has its own database file, so an older version still opens the books until the owner agrees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
62 files changed
+5309
-628
No files matched your search
@@ -41,6 +41,12 @@
|
||||
//! however long that takes.
|
||||
//! 11. The owner's close while restarting is refused (the restart is
|
||||
//! already under way; close again once it is done).
|
||||
//! 12. The owner's go-ahead for a format upgrade (consent_format_upgrade,
|
||||
//! its passphrase given) is handed to the open server's admin socket,
|
||||
//! which checks the passphrase and does it (plan 3.9, D-39); the host
|
||||
//! reads no books, and answers with what the server said, or that it
|
||||
//! may or may not have been done. Refused while the books open or
|
||||
//! close; the host keeps serving leases meanwhile.
|
||||
//! ```
|
||||
|
||||
use crate::knobs::{FLAG, Knobs};
|
||||
@@ -550,15 +556,17 @@ impl Host {
|
||||
},
|
||||
});
|
||||
}
|
||||
Event::Intent(intent, answer) => {
|
||||
let (msg, closes) = self.intent(intent);
|
||||
let _ = answer.send(msg);
|
||||
if let Some(reason) = closes {
|
||||
self.begin_close(reason);
|
||||
idle_at = None;
|
||||
if self.server_pid.is_none() {
|
||||
return self.finish(connections, Ending::Closed).await;
|
||||
}
|
||||
Event::Intent(Intent::ConsentFormatUpgrade { book, passphrase }, answer) => {
|
||||
self.consent(book, passphrase.unwrap_or_default(), answer);
|
||||
}
|
||||
Event::Intent(Intent::Close, answer) => {
|
||||
let _ = answer.send(HostMsg::Done {
|
||||
detail: Some("the books are closing".into()),
|
||||
});
|
||||
self.begin_close(Reason::ClosedByOwner);
|
||||
idle_at = None;
|
||||
if self.server_pid.is_none() {
|
||||
return self.finish(connections, Ending::Closed).await;
|
||||
}
|
||||
}
|
||||
Event::ServerReady => {
|
||||
@@ -682,20 +690,39 @@ impl Host {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An owner intent: the answer, and whether it closes the books.
|
||||
fn intent(&mut self, intent: Intent) -> (HostMsg, Option<Reason>) {
|
||||
match intent {
|
||||
Intent::Close => (
|
||||
HostMsg::Done {
|
||||
detail: Some("the books are closing".into()),
|
||||
},
|
||||
Some(Reason::ClosedByOwner),
|
||||
),
|
||||
Intent::ConsentFormatUpgrade { .. } => (
|
||||
HostMsg::Refused("no books are waiting for a format upgrade".into()),
|
||||
None,
|
||||
),
|
||||
/// The owner's go-ahead for a format upgrade (INVARIANT 12): handed to
|
||||
/// the server's admin socket, off the loop (a backup comes first, and
|
||||
/// may take a while); `answer` gets what came of it.
|
||||
fn consent(
|
||||
&self,
|
||||
book: String,
|
||||
passphrase: ledger_mcp::admin::Passphrase,
|
||||
answer: oneshot::Sender<HostMsg>,
|
||||
) {
|
||||
if !matches!(self.phase, Phase::Open) {
|
||||
let _ = answer.send(HostMsg::Refused(
|
||||
"the books are closing: nothing was done".into(),
|
||||
));
|
||||
return;
|
||||
}
|
||||
if !self.ready {
|
||||
let _ = answer.send(HostMsg::Refused(
|
||||
"the books are still opening: nothing was done; try again once they are open"
|
||||
.into(),
|
||||
));
|
||||
return;
|
||||
}
|
||||
let socket = self.run.join(ledger_mcp::local::ADMIN_SOCKET);
|
||||
tokio::spawn(async move {
|
||||
let msg = tokio::task::spawn_blocking(move || consented(&socket, book, passphrase))
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
HostMsg::Uncertain(
|
||||
"the books' host stopped asking the books server, so it may or may not have been done: `wanderbooks books list` says whether the books still wait for it".into(),
|
||||
)
|
||||
});
|
||||
let _ = answer.send(msg);
|
||||
});
|
||||
}
|
||||
|
||||
/// Tell every lease why it ends, and end it (INVARIANT 5).
|
||||
@@ -737,6 +764,30 @@ impl Host {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask the server on `socket` to upgrade the books `book` with the
|
||||
/// owner's passphrase (INVARIANT 12): what came of it, in the lease
|
||||
/// socket's words.
|
||||
fn consented(socket: &Path, book: String, passphrase: ledger_mcp::admin::Passphrase) -> HostMsg {
|
||||
use ledger_mcp::admin::{CallError, Request, Response};
|
||||
match ledger_mcp::admin::call(
|
||||
socket,
|
||||
Request::UpgradeFormat {
|
||||
id: book,
|
||||
passphrase,
|
||||
},
|
||||
) {
|
||||
Ok(Response::FormatUpgraded(u)) => HostMsg::Done {
|
||||
detail: Some(crate::commands::upgraded_lines(&u).join("\n")),
|
||||
},
|
||||
Ok(Response::Refused(why)) => HostMsg::Refused(why),
|
||||
Ok(_) => HostMsg::Uncertain(
|
||||
"the books server answered oddly, so it may or may not have been done: `wanderbooks books list` says whether the books still wait for it".into(),
|
||||
),
|
||||
Err(CallError::NotSent(why)) => HostMsg::Refused(format!("{why}: nothing was done")),
|
||||
Err(CallError::Unknown(why)) => HostMsg::Uncertain(why),
|
||||
}
|
||||
}
|
||||
|
||||
/// Take run/update.lock (exclusive), if it is free now.
|
||||
fn take_update_lock(run: &Path) -> Result<std::fs::File, String> {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
|
||||
Reference in new issue
Block a user