use std::io::Read;
use std::path::Path;
use std::process::ExitCode;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use super::{Client, Common, Ended, Reply, Text, finish, one_value, socket_of};
const USAGE: &str = "\
usage: haematite write (--socket <path> | --config <serve.json>) [--timeout-ms N] --branch <name>
[--expected <generation>.<sequence>] [--request-id <text>] <kind> <value | ->
<kind> is a change kind as the service names it (document.put, vector.put, documents.put_many,
vectors.put_many, edges.put_many, documents.delete_many and the rest); <value> is one JSON value, sent as the text given; - reads it from standard input.
branch.fork: --branch is the source, and the value names the new branch and the source version
it starts from: {\"name\":\"<new>\",\"from\":{\"generation\":\"<g>\",\"sequence\":\"<s>\"}}.
branch.discard: the value names the --branch itself, never another: {\"name\":\"<branch>\"}.
Without --expected, the change is applied to the branch as it stands when the command runs. A
caller who read a record and is changing it gives the version it read as --expected, so a change
made in between is refused rather than overwritten.
Without --request-id, one is made and printed as `haematite: request <id>` before sending. If the
outcome is not known, running the same command with that --request-id is safe to repeat.
Repeated after the change was applied, the same command answers with the same receipt; with
different content under the same --request-id it is refused.
--timeout-ms bounds connecting, writing and reading; the default is 30000.
Exit 0: applied, the receipt on standard output. Exit 1: could not ask; nothing was sent.
Exit 2: refused by the service, its code and message on standard error. Exit 3: the outcome
is not known; standard error gives the request id to repeat it with.";
const BRANCH_PAGE: u32 = 200;
struct Write {
target: super::Target,
timeout: Duration,
branch: String,
expected: Option<Box<RawValue>>,
request_id: Option<String>,
kind: String,
value: Text,
}
#[derive(Serialize)]
struct Apply<'a> {
request_id: &'a str,
branch: &'a str,
expected: &'a RawValue,
change: Change<'a>,
}
#[derive(Serialize)]
struct Change<'a> {
kind: &'a str,
value: &'a RawValue,
}
#[derive(Serialize)]
struct ReceiptQuery<'a> {
branch: &'a str,
request_id: &'a str,
}
#[derive(Serialize)]
struct BranchQuery<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<&'a str>,
limit: u32,
}
#[derive(Deserialize)]
struct BranchPage {
entries: Vec<Branch>,
next_after: Option<String>,
}
#[derive(Deserialize)]
struct Branch {
branch: String,
version: Box<RawValue>,
}
#[must_use]
pub fn run(args: impl Iterator<Item = String>) -> ExitCode {
finish(write(args))
}
fn write(args: impl Iterator<Item = String>) -> Result<Reply, Ended> {
let asked = parse(args).map_err(|why| Ended::Usage(format!("{why}\n{USAGE}")))?;
let value = one_value(asked.value, "value")?;
let socket = socket_of(&asked.target)?;
let ordinary = !matches!(asked.kind.as_str(), "branch.fork" | "branch.discard");
let replays = asked.request_id.is_some() && ordinary;
let request_id = asked.request_id.map_or_else(made_request_id, Ok)?;
let mut client = Client::connect(&socket, asked.timeout)?;
let branch = asked.branch.as_str();
let expected = match asked.expected {
Some(expected) => expected,
None => match origin(&mut client, branch, &request_id, replays)? {
Ok(origin) => origin,
Err(refused) => return Ok(refused),
},
};
let change = Change {
kind: &asked.kind,
value: &value,
};
let apply = Apply {
request_id: &request_id,
branch,
expected: &expected,
change,
};
let params = serde_json::value::to_raw_value(&apply)
.map_err(|error| Ended::NotAsked(format!("the request could not be written: {error}")))?;
match client.call("mutation.apply", Some(¶ms)) {
Err(Ended::Unknown(why)) => reconcile(&socket, asked.timeout, branch, &request_id, &why),
other => other,
}
}
fn parse(mut args: impl Iterator<Item = String>) -> Result<Write, String> {
let mut common = Common::default();
let mut branch = None;
let mut expected = None;
let mut request_id = None;
let mut rest = Vec::new();
while let Some(arg) = args.next() {
if common.take(&arg, &mut args)? {
continue;
}
match arg.as_str() {
"--branch" if branch.is_none() => branch = Some(value_of(&arg, &mut args)?),
"--expected" if expected.is_none() => {
expected = Some(version(&value_of(&arg, &mut args)?)?);
}
"--request-id" if request_id.is_none() => {
request_id = Some(value_of(&arg, &mut args)?);
}
flag if flag.starts_with("--") => {
return Err(format!("unknown or repeated flag {flag:?}"));
}
_ => rest.push(arg),
}
}
let (target, timeout) = common.finish()?;
let branch = branch.ok_or("name the branch with --branch")?;
let mut rest = rest.into_iter();
let kind = rest.next().ok_or("name the change kind")?;
let value = rest
.next()
.map(Text::of)
.ok_or("give the change's value, or -")?;
if let Some(extra) = rest.next() {
return Err(format!("unexpected argument {extra:?}"));
}
Ok(Write {
target,
timeout,
branch,
expected,
request_id,
kind,
value,
})
}
fn value_of(flag: &str, args: &mut impl Iterator<Item = String>) -> Result<String, String> {
args.next()
.filter(|value| !value.is_empty())
.ok_or_else(|| format!("{flag} needs a value"))
}
fn version(text: &str) -> Result<Box<RawValue>, String> {
let refused = || format!("--expected {text:?} is not <generation>.<sequence>");
let (generation, sequence) = text.split_once('.').ok_or_else(refused)?;
let generation: u64 = generation.parse().map_err(|_| refused())?;
let sequence: u64 = sequence.parse().map_err(|_| refused())?;
version_value(generation, sequence).map_err(|error| error.to_string())
}
fn version_value(generation: u64, sequence: u64) -> serde_json::Result<Box<RawValue>> {
RawValue::from_string(format!(
r#"{{"generation":"{generation}","sequence":"{sequence}"}}"#
))
}
fn origin(
client: &mut Client,
branch: &str,
request_id: &str,
replays: bool,
) -> Result<Result<Box<RawValue>, Reply>, Ended> {
if replays {
match earlier(client, branch, request_id)? {
Err(refused) => return Ok(Err(refused)),
Ok(Some(receipt)) => {
if let Some(origin) = applied_on(&receipt) {
return Ok(Ok(origin));
}
}
Ok(None) => {}
}
}
head(client, branch, request_id)
}
fn earlier(
client: &mut Client,
branch: &str,
request_id: &str,
) -> Result<Result<Option<Box<RawValue>>, Reply>, Ended> {
let unread = |why: &dyn std::fmt::Display| {
Ended::NotAsked(format!(
"an earlier receipt for {request_id} could not be read: {why}"
))
};
let query = ReceiptQuery { branch, request_id };
let params = serde_json::value::to_raw_value(&query).map_err(|error| unread(&error))?;
match client.call("mutation.receipt", Some(¶ms)) {
Ok(Reply::Answered(receipt)) if receipt.get() == "null" => Ok(Ok(None)),
Ok(Reply::Answered(receipt)) => Ok(Ok(Some(receipt))),
Ok(refused @ Reply::Refused(_)) => Ok(Err(refused)),
Err(ended) => Err(unread(&ended.why())),
}
}
#[derive(Deserialize)]
struct Applied {
version: AppliedVersion,
}
#[derive(Deserialize)]
struct AppliedVersion {
generation: String,
sequence: String,
}
fn applied_on(receipt: &RawValue) -> Option<Box<RawValue>> {
let applied: Applied = serde_json::from_str(receipt.get()).ok()?;
let generation: u64 = applied.version.generation.parse().ok()?;
let sequence: u64 = applied.version.sequence.parse().ok()?;
version_value(generation, sequence.checked_sub(1)?).ok()
}
fn made_request_id() -> Result<String, Ended> {
let mut bytes = [0_u8; 16];
std::fs::File::open("/dev/urandom")
.and_then(|mut source| source.read_exact(&mut bytes))
.map_err(|error| Ended::NotAsked(format!("no request id could be made: {error}")))?;
let made = format!("{:032x}", u128::from_be_bytes(bytes));
eprintln!("haematite: request {made}");
Ok(made)
}
fn head(
client: &mut Client,
branch: &str,
request_id: &str,
) -> Result<Result<Box<RawValue>, Reply>, Ended> {
let unread = |why: &dyn std::fmt::Display| {
Ended::NotAsked(format!("the head of {branch} could not be read: {why}"))
};
let limit = BRANCH_PAGE.min(client.max_page().get());
let mut after = None;
loop {
let query = BranchQuery {
after: after.as_deref(),
limit,
};
let params = serde_json::value::to_raw_value(&query).map_err(|error| unread(&error))?;
let page = match client.call("branches.list", Some(¶ms)) {
Ok(Reply::Answered(page)) => page,
Ok(refused @ Reply::Refused(_)) => return Ok(Err(refused)),
Err(ended) => return Err(unread(&ended.why())),
};
let page: BranchPage = serde_json::from_str(page.get()).map_err(|error| unread(&error))?;
if let Some(found) = page
.entries
.into_iter()
.find(|entry| entry.branch == branch)
{
return Ok(Ok(found.version));
}
let Some(next) = page.next_after else {
break;
};
after = Some(next);
}
let query = ReceiptQuery { branch, request_id };
let params = serde_json::value::to_raw_value(&query).map_err(|error| unread(&error))?;
match client.call("mutation.receipt", Some(¶ms)) {
Ok(refused @ Reply::Refused(_)) => Ok(Err(refused)),
Ok(Reply::Answered(receipt)) if receipt.get() == "null" => {
Err(unread(&format!("{branch} is not listed")))
}
Ok(Reply::Answered(_)) => Err(Ended::Unknown(format!(
"{branch} is not listed, and the service holds a receipt for request \
{request_id} on it, so a change under this id was applied; whether it was this \
change is confirmed by running the same command with the --expected first sent"
))),
Err(ended) => Err(unread(&ended.why())),
}
}
fn reconcile(
socket: &Path,
timeout: Duration,
branch: &str,
request_id: &str,
why: &str,
) -> Result<Reply, Ended> {
let unknown = |found: String| {
Ended::Unknown(format!(
"request {request_id}: {why}; {found}. Running the same command with --request-id \
{request_id} is safe to repeat"
))
};
let query = ReceiptQuery { branch, request_id };
let asked = serde_json::value::to_raw_value(&query)
.map_err(|error| Ended::NotAsked(error.to_string()))
.and_then(|params| {
Client::connect(socket, timeout)?.call("mutation.receipt", Some(¶ms))
});
match asked {
Ok(Reply::Answered(receipt)) if receipt.get() != "null" => Ok(Reply::Answered(receipt)),
Ok(Reply::Answered(_)) => Err(unknown("the service has no receipt for it yet".to_owned())),
Ok(Reply::Refused(refusal)) => Err(unknown(format!(
"its receipt was refused: {}: {}",
refusal.code, refusal.message
))),
Err(ended) => Err(unknown(format!(
"its receipt could not be asked: {}",
ended.why()
))),
}
}