1use std::io::Read;
8use std::path::Path;
9use std::process::ExitCode;
10use std::time::Duration;
11
12use serde::{Deserialize, Serialize};
13use serde_json::value::RawValue;
14
15use super::{Client, Common, Ended, Reply, Text, finish, one_value, socket_of};
16
17const USAGE: &str = "\
18usage: haematite write (--socket <path> | --config <serve.json>) [--timeout-ms N] --branch <name>
19 [--expected <generation>.<sequence>] [--request-id <text>] <kind> <value | ->
20<kind> is a change kind as the service names it (document.put, vector.put, documents.put_many,
21vectors.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.
22branch.fork: --branch is the source, and the value names the new branch and the source version
23it starts from: {\"name\":\"<new>\",\"from\":{\"generation\":\"<g>\",\"sequence\":\"<s>\"}}.
24branch.discard: the value names the --branch itself, never another: {\"name\":\"<branch>\"}.
25Without --expected, the change is applied to the branch as it stands when the command runs. A
26caller who read a record and is changing it gives the version it read as --expected, so a change
27made in between is refused rather than overwritten.
28Without --request-id, one is made and printed as `haematite: request <id>` before sending. If the
29outcome is not known, running the same command with that --request-id is safe to repeat.
30Repeated after the change was applied, the same command answers with the same receipt; with
31different content under the same --request-id it is refused.
32--timeout-ms bounds connecting, writing and reading; the default is 30000.
33Exit 0: applied, the receipt on standard output. Exit 1: could not ask; nothing was sent.
34Exit 2: refused by the service, its code and message on standard error. Exit 3: the outcome
35is not known; standard error gives the request id to repeat it with.";
36
37const BRANCH_PAGE: u32 = 200;
40
41struct Write {
42 target: super::Target,
43 timeout: Duration,
44 branch: String,
45 expected: Option<Box<RawValue>>,
46 request_id: Option<String>,
47 kind: String,
48 value: Text,
49}
50
51#[derive(Serialize)]
52struct Apply<'a> {
53 request_id: &'a str,
54 branch: &'a str,
55 expected: &'a RawValue,
56 change: Change<'a>,
57}
58
59#[derive(Serialize)]
60struct Change<'a> {
61 kind: &'a str,
62 value: &'a RawValue,
63}
64
65#[derive(Serialize)]
66struct ReceiptQuery<'a> {
67 branch: &'a str,
68 request_id: &'a str,
69}
70
71#[derive(Serialize)]
72struct BranchQuery<'a> {
73 #[serde(skip_serializing_if = "Option::is_none")]
74 after: Option<&'a str>,
75 limit: u32,
76}
77
78#[derive(Deserialize)]
79struct BranchPage {
80 entries: Vec<Branch>,
81 next_after: Option<String>,
82}
83
84#[derive(Deserialize)]
85struct Branch {
86 branch: String,
87 version: Box<RawValue>,
88}
89
90#[must_use]
92pub fn run(args: impl Iterator<Item = String>) -> ExitCode {
93 finish(write(args))
94}
95
96fn write(args: impl Iterator<Item = String>) -> Result<Reply, Ended> {
97 let asked = parse(args).map_err(|why| Ended::Usage(format!("{why}\n{USAGE}")))?;
98 let value = one_value(asked.value, "value")?;
99 let socket = socket_of(&asked.target)?;
100 let ordinary = !matches!(asked.kind.as_str(), "branch.fork" | "branch.discard");
102 let replays = asked.request_id.is_some() && ordinary;
103 let request_id = asked.request_id.map_or_else(made_request_id, Ok)?;
104 let mut client = Client::connect(&socket, asked.timeout)?;
105 let branch = asked.branch.as_str();
106 let expected = match asked.expected {
107 Some(expected) => expected,
108 None => match origin(&mut client, branch, &request_id, replays)? {
109 Ok(origin) => origin,
110 Err(refused) => return Ok(refused),
111 },
112 };
113 let change = Change {
114 kind: &asked.kind,
115 value: &value,
116 };
117 let apply = Apply {
118 request_id: &request_id,
119 branch,
120 expected: &expected,
121 change,
122 };
123 let params = serde_json::value::to_raw_value(&apply)
124 .map_err(|error| Ended::NotAsked(format!("the request could not be written: {error}")))?;
125 match client.call("mutation.apply", Some(¶ms)) {
126 Err(Ended::Unknown(why)) => reconcile(&socket, asked.timeout, branch, &request_id, &why),
127 other => other,
128 }
129}
130
131fn parse(mut args: impl Iterator<Item = String>) -> Result<Write, String> {
132 let mut common = Common::default();
133 let mut branch = None;
134 let mut expected = None;
135 let mut request_id = None;
136 let mut rest = Vec::new();
137 while let Some(arg) = args.next() {
138 if common.take(&arg, &mut args)? {
139 continue;
140 }
141 match arg.as_str() {
142 "--branch" if branch.is_none() => branch = Some(value_of(&arg, &mut args)?),
143 "--expected" if expected.is_none() => {
144 expected = Some(version(&value_of(&arg, &mut args)?)?);
145 }
146 "--request-id" if request_id.is_none() => {
147 request_id = Some(value_of(&arg, &mut args)?);
148 }
149 flag if flag.starts_with("--") => {
150 return Err(format!("unknown or repeated flag {flag:?}"));
151 }
152 _ => rest.push(arg),
153 }
154 }
155 let (target, timeout) = common.finish()?;
156 let branch = branch.ok_or("name the branch with --branch")?;
157 let mut rest = rest.into_iter();
158 let kind = rest.next().ok_or("name the change kind")?;
159 let value = rest
160 .next()
161 .map(Text::of)
162 .ok_or("give the change's value, or -")?;
163 if let Some(extra) = rest.next() {
164 return Err(format!("unexpected argument {extra:?}"));
165 }
166 Ok(Write {
167 target,
168 timeout,
169 branch,
170 expected,
171 request_id,
172 kind,
173 value,
174 })
175}
176
177fn value_of(flag: &str, args: &mut impl Iterator<Item = String>) -> Result<String, String> {
178 args.next()
179 .filter(|value| !value.is_empty())
180 .ok_or_else(|| format!("{flag} needs a value"))
181}
182
183fn version(text: &str) -> Result<Box<RawValue>, String> {
185 let refused = || format!("--expected {text:?} is not <generation>.<sequence>");
186 let (generation, sequence) = text.split_once('.').ok_or_else(refused)?;
187 let generation: u64 = generation.parse().map_err(|_| refused())?;
188 let sequence: u64 = sequence.parse().map_err(|_| refused())?;
189 version_value(generation, sequence).map_err(|error| error.to_string())
190}
191
192fn version_value(generation: u64, sequence: u64) -> serde_json::Result<Box<RawValue>> {
193 RawValue::from_string(format!(
194 r#"{{"generation":"{generation}","sequence":"{sequence}"}}"#
195 ))
196}
197
198fn origin(
207 client: &mut Client,
208 branch: &str,
209 request_id: &str,
210 replays: bool,
211) -> Result<Result<Box<RawValue>, Reply>, Ended> {
212 if replays {
213 match earlier(client, branch, request_id)? {
214 Err(refused) => return Ok(Err(refused)),
215 Ok(Some(receipt)) => {
216 if let Some(origin) = applied_on(&receipt) {
217 return Ok(Ok(origin));
218 }
219 }
220 Ok(None) => {}
221 }
222 }
223 head(client, branch, request_id)
224}
225
226fn earlier(
229 client: &mut Client,
230 branch: &str,
231 request_id: &str,
232) -> Result<Result<Option<Box<RawValue>>, Reply>, Ended> {
233 let unread = |why: &dyn std::fmt::Display| {
234 Ended::NotAsked(format!(
235 "an earlier receipt for {request_id} could not be read: {why}"
236 ))
237 };
238 let query = ReceiptQuery { branch, request_id };
239 let params = serde_json::value::to_raw_value(&query).map_err(|error| unread(&error))?;
240 match client.call("mutation.receipt", Some(¶ms)) {
241 Ok(Reply::Answered(receipt)) if receipt.get() == "null" => Ok(Ok(None)),
242 Ok(Reply::Answered(receipt)) => Ok(Ok(Some(receipt))),
243 Ok(refused @ Reply::Refused(_)) => Ok(Err(refused)),
244 Err(ended) => Err(unread(&ended.why())),
245 }
246}
247
248#[derive(Deserialize)]
249struct Applied {
250 version: AppliedVersion,
251}
252
253#[derive(Deserialize)]
254struct AppliedVersion {
255 generation: String,
256 sequence: String,
257}
258
259fn applied_on(receipt: &RawValue) -> Option<Box<RawValue>> {
267 let applied: Applied = serde_json::from_str(receipt.get()).ok()?;
268 let generation: u64 = applied.version.generation.parse().ok()?;
269 let sequence: u64 = applied.version.sequence.parse().ok()?;
270 version_value(generation, sequence.checked_sub(1)?).ok()
271}
272
273fn made_request_id() -> Result<String, Ended> {
276 let mut bytes = [0_u8; 16];
277 std::fs::File::open("/dev/urandom")
278 .and_then(|mut source| source.read_exact(&mut bytes))
279 .map_err(|error| Ended::NotAsked(format!("no request id could be made: {error}")))?;
280 let made = format!("{:032x}", u128::from_be_bytes(bytes));
281 eprintln!("haematite: request {made}");
282 Ok(made)
283}
284
285fn head(
289 client: &mut Client,
290 branch: &str,
291 request_id: &str,
292) -> Result<Result<Box<RawValue>, Reply>, Ended> {
293 let unread = |why: &dyn std::fmt::Display| {
294 Ended::NotAsked(format!("the head of {branch} could not be read: {why}"))
295 };
296 let limit = BRANCH_PAGE.min(client.max_page().get());
297 let mut after = None;
298 loop {
299 let query = BranchQuery {
300 after: after.as_deref(),
301 limit,
302 };
303 let params = serde_json::value::to_raw_value(&query).map_err(|error| unread(&error))?;
304 let page = match client.call("branches.list", Some(¶ms)) {
305 Ok(Reply::Answered(page)) => page,
306 Ok(refused @ Reply::Refused(_)) => return Ok(Err(refused)),
307 Err(ended) => return Err(unread(&ended.why())),
308 };
309 let page: BranchPage = serde_json::from_str(page.get()).map_err(|error| unread(&error))?;
310 if let Some(found) = page
311 .entries
312 .into_iter()
313 .find(|entry| entry.branch == branch)
314 {
315 return Ok(Ok(found.version));
316 }
317 let Some(next) = page.next_after else {
318 break;
319 };
320 after = Some(next);
321 }
322 let query = ReceiptQuery { branch, request_id };
323 let params = serde_json::value::to_raw_value(&query).map_err(|error| unread(&error))?;
324 match client.call("mutation.receipt", Some(¶ms)) {
325 Ok(refused @ Reply::Refused(_)) => Ok(Err(refused)),
326 Ok(Reply::Answered(receipt)) if receipt.get() == "null" => {
327 Err(unread(&format!("{branch} is not listed")))
328 }
329 Ok(Reply::Answered(_)) => Err(Ended::Unknown(format!(
332 "{branch} is not listed, and the service holds a receipt for request \
333 {request_id} on it, so a change under this id was applied; whether it was this \
334 change is confirmed by running the same command with the --expected first sent"
335 ))),
336 Err(ended) => Err(unread(&ended.why())),
337 }
338}
339
340fn reconcile(
344 socket: &Path,
345 timeout: Duration,
346 branch: &str,
347 request_id: &str,
348 why: &str,
349) -> Result<Reply, Ended> {
350 let unknown = |found: String| {
351 Ended::Unknown(format!(
352 "request {request_id}: {why}; {found}. Running the same command with --request-id \
353 {request_id} is safe to repeat"
354 ))
355 };
356 let query = ReceiptQuery { branch, request_id };
357 let asked = serde_json::value::to_raw_value(&query)
358 .map_err(|error| Ended::NotAsked(error.to_string()))
359 .and_then(|params| {
360 Client::connect(socket, timeout)?.call("mutation.receipt", Some(¶ms))
361 });
362 match asked {
363 Ok(Reply::Answered(receipt)) if receipt.get() != "null" => Ok(Reply::Answered(receipt)),
364 Ok(Reply::Answered(_)) => Err(unknown("the service has no receipt for it yet".to_owned())),
365 Ok(Reply::Refused(refusal)) => Err(unknown(format!(
366 "its receipt was refused: {}: {}",
367 refusal.code, refusal.message
368 ))),
369 Err(ended) => Err(unknown(format!(
370 "its receipt could not be asked: {}",
371 ended.why()
372 ))),
373 }
374}