@mark1russell7/harness 0.14.4 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- //! client HOST PORT --mode idle|inflight|stream [--keepalive] [--user-timeout-ms N] [--go PATH]
2
- //! [--request TEXT] [--count N] [--interval-ms N] [--sample-ms N] [--max-s N]
1
+ //! client HOST PORT --mode idle|inflight|stream|schedule [--keepalive] [--user-timeout-ms N] [--go PATH]
2
+ //! [--request TEXT] [--count N] [--interval-ms N] [--schedule PATH] [--sample-ms N] [--max-s N]
3
3
  //!
4
4
  //! A cell's TCP client: one connection, every event a JSON line stamped on CLOCK_MONOTONIC. It
5
5
  //! sets no per-socket TCP timer unless the cell asks, so the namespace's sysctls are what gets
@@ -19,9 +19,15 @@
19
19
  //! whose outcome is one (loss, reordering). A reply is whole at its CRLF, or for a bulk string
20
20
  //! after its length: so `GET big` keeps a long reply's many segments in flight, and the replied
21
21
  //! line stamps its last byte. It ends with a done line.
22
+ //! schedule: then wait for --go, and send the request at each time of the file --schedule, a file
23
+ //! of the format rigor-schedule/1. A send does not wait for a reply, thus a stall hides no arrival
24
+ //! (coordinated omission). The requests share the one connection, and the replies come in their
25
+ //! order. The sent and replied lines name their arrival (i). It ends with a done line when each
26
+ //! arrival has its reply. An error, an end or the deadline leaves the rest without a reply.
22
27
 
28
+ use harness_native::{arrivals_of, emit, emit_at};
23
29
  use prober::{errno_name, monotonic_ns, quantity};
24
- use serde_json::{json, Value};
30
+ use serde_json::json;
25
31
  use std::error::Error;
26
32
  use std::io::{ErrorKind, Read, Write};
27
33
  use std::net::TcpStream;
@@ -32,18 +38,6 @@ use std::time::Duration;
32
38
  /// a Redis inline command. Any line-based server echoes it or gives an error, and that is a reply too
33
39
  const REQUEST: &[u8] = b"PING\r\n";
34
40
 
35
- fn emit(event: &str, fields: Value) {
36
- emit_at(event, monotonic_ns(), fields);
37
- }
38
-
39
- fn emit_at(event: &str, t: u64, fields: Value) {
40
- let mut line = json!({ "ev": event, "t": t });
41
- if let (Some(line), Value::Object(fields)) = (line.as_object_mut(), fields) {
42
- line.extend(fields);
43
- }
44
- println!("{line}");
45
- }
46
-
47
41
  /// Sends the request, stamped before the write: on a bridge, the kernel can take a reset to the
48
42
  /// request before write() returns, and a stamp after it comes later than the reset's own.
49
43
  fn send(stream: &mut TcpStream, request: &[u8]) -> std::io::Result<()> {
@@ -110,17 +104,129 @@ struct Reply {
110
104
  }
111
105
 
112
106
  impl Reply {
113
- /// Takes the bytes of one read, and says whether the reply is whole.
114
- fn take(&mut self, bytes: &[u8]) -> bool {
115
- self.got += bytes.len();
116
- if self.length.is_none() {
107
+ /// Takes the bytes of this reply from the start of `bytes`, and says how many it took. The rest
108
+ /// is the next reply of a pipeline.
109
+ fn consume(&mut self, bytes: &[u8]) -> usize {
110
+ let mut used = 0;
111
+ while self.length.is_none() && used < bytes.len() {
117
112
  // a bulk string's first line is its length: 64 bytes hold any
118
- let room = 64usize.saturating_sub(self.head.len());
119
- self.head.extend_from_slice(&bytes[..bytes.len().min(room)]);
113
+ if self.head.len() < 64 {
114
+ self.head.push(bytes[used]);
115
+ }
116
+ used += 1;
120
117
  self.length = reply_length(&self.head);
121
118
  }
119
+ self.got += used;
120
+ if let Some(length) = self.length {
121
+ let more = length.saturating_sub(self.got).min(bytes.len() - used);
122
+ self.got += more;
123
+ used += more;
124
+ }
125
+ used
126
+ }
127
+
128
+ fn whole(&self) -> bool {
122
129
  matches!(self.length, Some(length) if self.got >= length)
123
130
  }
131
+
132
+ /// Takes the bytes of one read, and says whether the reply is whole.
133
+ fn take(&mut self, bytes: &[u8]) -> bool {
134
+ self.consume(bytes);
135
+ self.whole()
136
+ }
137
+ }
138
+
139
+ /// Waits up to `ns` for the socket, and says if it has something to read and if it takes a write.
140
+ /// A wait in ms is too coarse for a schedule, thus this one is ppoll().
141
+ fn wait_for(fd: i32, writing: bool, ns: u64) -> std::io::Result<(bool, bool)> {
142
+ let events = libc::POLLIN | libc::POLLRDHUP | if writing { libc::POLLOUT } else { 0 };
143
+ let mut wanted = libc::pollfd { fd, events, revents: 0 };
144
+ let time = libc::timespec { tv_sec: (ns / 1_000_000_000) as libc::time_t, tv_nsec: (ns % 1_000_000_000) as libc::c_long };
145
+ // SAFETY: one pollfd and one timespec on this stack, and no signal mask
146
+ match unsafe { libc::ppoll(&mut wanted, 1, &time, std::ptr::null()) } {
147
+ -1 if std::io::Error::last_os_error().kind() == ErrorKind::Interrupted => Ok((false, false)),
148
+ -1 => Err(std::io::Error::last_os_error()),
149
+ _ => Ok((wanted.revents & !libc::POLLOUT != 0, wanted.revents & libc::POLLOUT != 0)),
150
+ }
151
+ }
152
+
153
+ /// Sends each arrival of `plan` at its time after the file --go appears, whatever the replies do.
154
+ /// The stamp of a send comes before the write that ends it. Thus a full socket shows as the lag of
155
+ /// each arrival behind it.
156
+ fn open_loop(stream: &mut TcpStream, plan: &[u64], args: &Args, deadline: u64) -> Result<(), Box<dyn Error>> {
157
+ let fd = stream.as_raw_fd();
158
+ let tick = args.sample.as_nanos() as u64;
159
+ while !Path::new(&args.go).exists() {
160
+ if monotonic_ns() > deadline {
161
+ emit("deadline", json!({}));
162
+ return Ok(());
163
+ }
164
+ std::thread::sleep(args.sample);
165
+ }
166
+ let start = monotonic_ns();
167
+ emit_at("scheduled", start, json!({ "arrivals": plan.len() }));
168
+ let mut buf = [0u8; 65536];
169
+ let (mut next, mut written, mut answered, mut blocked) = (0usize, 0usize, 0usize, false);
170
+ let mut reply = Reply::default();
171
+ loop {
172
+ while !blocked && next < plan.len() && monotonic_ns() >= start + plan[next] {
173
+ let t = monotonic_ns();
174
+ match stream.write(&args.request[written..]) {
175
+ Ok(n) => {
176
+ written += n;
177
+ if written == args.request.len() {
178
+ emit_at("sent", t, json!({ "i": next }));
179
+ next += 1;
180
+ written = 0;
181
+ }
182
+ }
183
+ Err(error) if error.kind() == ErrorKind::WouldBlock => blocked = true,
184
+ Err(error) if error.kind() == ErrorKind::Interrupted => {}
185
+ Err(error) => {
186
+ emit("error", json!({ "errno": errno_name(error.raw_os_error().unwrap_or(0)) }));
187
+ return Ok(());
188
+ }
189
+ }
190
+ }
191
+ if answered == plan.len() {
192
+ emit("done", json!({ "requests": next }));
193
+ return Ok(());
194
+ }
195
+ let now = monotonic_ns();
196
+ if now > deadline {
197
+ emit("deadline", json!({}));
198
+ return Ok(());
199
+ }
200
+ let due = plan.get(next).map_or(tick, |at| (start + at).saturating_sub(now));
201
+ let (can_read, can_write) = wait_for(fd, blocked, due.min(tick))?;
202
+ blocked = blocked && !can_write;
203
+ if !can_read {
204
+ continue;
205
+ }
206
+ match stream.read(&mut buf) {
207
+ Ok(0) => {
208
+ emit("eof", json!({}));
209
+ return Ok(());
210
+ }
211
+ Ok(n) => {
212
+ let t = monotonic_ns();
213
+ let mut at = 0;
214
+ while at < n {
215
+ at += reply.consume(&buf[at..n]);
216
+ if reply.whole() {
217
+ emit_at("replied", t, json!({ "i": answered }));
218
+ answered += 1;
219
+ reply = Reply::default();
220
+ }
221
+ }
222
+ }
223
+ Err(error) if error.kind() == ErrorKind::WouldBlock || error.kind() == ErrorKind::Interrupted => {}
224
+ Err(error) => {
225
+ emit("error", json!({ "errno": errno_name(error.raw_os_error().unwrap_or(0)) }));
226
+ return Ok(());
227
+ }
228
+ }
229
+ }
124
230
  }
125
231
 
126
232
  /// what in TCP_INFO is worth a line when it changes: the timers' own state, never the ages
@@ -150,14 +256,19 @@ struct Args {
150
256
  max: Duration,
151
257
  /// the request sent at --go, with its line ending
152
258
  request: Vec<u8>,
259
+ /// in schedule mode, the file of the arrivals
260
+ schedule: Option<String>,
153
261
  }
154
262
 
155
263
  fn args() -> Result<Args, Box<dyn Error>> {
156
264
  let raw: Vec<String> = std::env::args().skip(1).collect();
157
265
  let value = |flag: &str| raw.iter().position(|arg| arg == flag).and_then(|at| raw.get(at + 1)).cloned();
158
- let mode = value("--mode").ok_or("--mode idle|inflight|stream is required")?;
159
- if mode != "idle" && mode != "inflight" && mode != "stream" {
160
- return Err(format!("--mode is idle, inflight or stream, not {mode}").into());
266
+ let mode = value("--mode").ok_or("--mode idle|inflight|stream|schedule is required")?;
267
+ if !["idle", "inflight", "stream", "schedule"].contains(&mode.as_str()) {
268
+ return Err(format!("--mode is idle, inflight, stream or schedule, not {mode}").into());
269
+ }
270
+ if mode == "schedule" && value("--schedule").is_none() {
271
+ return Err("--mode schedule needs --schedule".into());
161
272
  }
162
273
  let count: u32 = value("--count").map(|v| v.parse()).transpose()?.unwrap_or(100);
163
274
  Ok(Args {
@@ -172,17 +283,20 @@ fn args() -> Result<Args, Box<dyn Error>> {
172
283
  sample: Duration::from_micros((value("--sample-ms").map(|v| v.parse::<f64>()).transpose()?.unwrap_or(2.0) * 1000.0) as u64),
173
284
  max: Duration::from_secs_f64(value("--max-s").map(|v| v.parse::<f64>()).transpose()?.unwrap_or(60.0)),
174
285
  request: value("--request").map_or_else(|| REQUEST.to_vec(), |r| format!("{r}\r\n").into_bytes()),
286
+ schedule: (mode == "schedule").then(|| value("--schedule")).flatten(),
175
287
  })
176
288
  }
177
289
 
178
290
  fn run() -> Result<(), Box<dyn Error>> {
179
291
  let args = args()?;
180
292
  let boot_id = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")?.trim().to_string();
181
- let mode = match (args.inflight, args.stream) {
182
- (_, Some(_)) => "stream",
183
- (true, None) => "inflight",
184
- (false, None) => "idle",
293
+ let mode = match (args.inflight, args.stream, &args.schedule) {
294
+ (_, _, Some(_)) => "schedule",
295
+ (_, Some(_), None) => "stream",
296
+ (true, None, None) => "inflight",
297
+ (false, None, None) => "idle",
185
298
  };
299
+ let plan = args.schedule.as_ref().map(std::fs::read_to_string).transpose()?.map(|text| arrivals_of(&text)).transpose()?;
186
300
  emit("start", json!({ "boot_id": boot_id, "mode": mode }));
187
301
  let deadline = monotonic_ns() + args.max.as_nanos() as u64;
188
302
 
@@ -214,6 +328,9 @@ fn run() -> Result<(), Box<dyn Error>> {
214
328
  // read that waits takes the socket's lock each time it wakes. A keepalive timer that finds the
215
329
  // socket held puts itself off by HZ/20. Thus such a read moves the timer that the client measures
216
330
  stream.set_nonblocking(true)?;
331
+ if let Some(plan) = &plan {
332
+ return open_loop(&mut stream, plan, &args, deadline);
333
+ }
217
334
  let tick = args.sample.as_millis().clamp(1, i32::MAX as u128) as i32;
218
335
  let mut waiting = args.inflight;
219
336
  let mut seen: Option<[u32; 7]> = None;
@@ -319,4 +436,23 @@ mod tests {
319
436
  assert!(reply.take(b"\n"));
320
437
  assert_eq!(reply.got, 17);
321
438
  }
439
+
440
+ #[test]
441
+ fn a_pipeline_of_replies_splits_at_each_end_in_reads_of_any_size() {
442
+ let bytes = b"+PONG\r\n$3\r\nabc\r\n+PONG\r\n";
443
+ for size in 1..bytes.len() {
444
+ let (mut reply, mut lengths) = (Reply::default(), Vec::new());
445
+ for read in bytes.chunks(size) {
446
+ let mut at = 0;
447
+ while at < read.len() {
448
+ at += reply.consume(&read[at..]);
449
+ if reply.whole() {
450
+ lengths.push(reply.got);
451
+ reply = Reply::default();
452
+ }
453
+ }
454
+ }
455
+ assert_eq!(lengths, [7, 9, 7], "reads of {size}");
456
+ }
457
+ }
322
458
  }