@mark1russell7/harness 0.14.4 → 0.15.0

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.
@@ -0,0 +1,481 @@
1
+ //! load HOST PORT --plan PATH [--go PATH] [--sample-ms N] [--max-s N]
2
+ //!
3
+ //! An open-loop driver of an HTTP/1.1 server. Each event is a JSON line stamped on CLOCK_MONOTONIC.
4
+ //! The file --plan has the format systems-load/1: a list of streams. Each stream has a key, a
5
+ //! schedule of the format rigor-schedule/1 and a list of requests. Arrival i of a stream sends request i modulo
6
+ //! the length of the list. Each request is the whole text of an HTTP/1.1 request.
7
+ //!
8
+ //! Each stream has its own connection, thus a slow reply delays only the arrivals of its own stream.
9
+ //! One shared connection gives each request the queue of all the requests before it. Then a heavy
10
+ //! stream moves each other stream, and the noise of one tenant on the others cannot be measured.
11
+ //!
12
+ //! The driver opens each connection and writes ready. Then it waits for the file --go. Then it
13
+ //! sends each arrival at its time, and a send does not wait for a reply (coordinated omission).
14
+ //! The requests of a stream share its connection, and the replies come in their order. The lines
15
+ //! sent and replied name the stream (k) and the arrival (i), and a replied line names the status.
16
+ //! It ends with a done line when each connection has each reply or has ended. An error, an end or
17
+ //! the deadline leaves the rest without a reply.
18
+
19
+ use harness_native::{arrivals_in, emit, emit_at};
20
+ use prober::{errno_name, monotonic_ns};
21
+ use serde_json::{json, Value};
22
+ use std::error::Error;
23
+ use std::io::{ErrorKind, Read, Write};
24
+ use std::net::TcpStream;
25
+ use std::os::fd::AsRawFd;
26
+ use std::path::Path;
27
+ use std::time::Duration;
28
+
29
+ /// The longest head of a response that the driver reads: more is an error of the server.
30
+ const HEAD_LIMIT: usize = 65_536;
31
+
32
+ /// One stream of a plan: its key, its arrivals in ns from the start, and its requests.
33
+ struct Stream {
34
+ key: String,
35
+ times: Vec<u64>,
36
+ requests: Vec<Vec<u8>>,
37
+ }
38
+
39
+ /// The streams of a file of the format systems-load/1.
40
+ fn plan_of(text: &str) -> Result<Vec<Stream>, Box<dyn Error>> {
41
+ let plan: Value = serde_json::from_str(text)?;
42
+ if plan["format"] != "systems-load/1" {
43
+ return Err(format!("the plan has the format {}, not systems-load/1", plan["format"]).into());
44
+ }
45
+ let streams = plan["streams"].as_array().ok_or("the plan has no list of streams")?;
46
+ if streams.is_empty() {
47
+ return Err("the plan has no stream".into());
48
+ }
49
+ let mut keys = std::collections::HashSet::new();
50
+ streams
51
+ .iter()
52
+ .map(|stream| -> Result<Stream, Box<dyn Error>> {
53
+ let key = stream["key"].as_str().ok_or("a stream has no key")?.to_string();
54
+ if !keys.insert(key.clone()) {
55
+ return Err(format!("two streams have the key {key}").into());
56
+ }
57
+ let times = arrivals_in(&stream["schedule"])?;
58
+ let requests = stream["requests"]
59
+ .as_array()
60
+ .ok_or_else(|| format!("the stream {key} has no list of requests"))?
61
+ .iter()
62
+ .map(|request| request.as_str().map(|text| text.as_bytes().to_vec()).ok_or_else(|| format!("a request of the stream {key} is not text")))
63
+ .collect::<Result<Vec<_>, String>>()?;
64
+ if requests.is_empty() {
65
+ return Err(format!("the stream {key} has no request").into());
66
+ }
67
+ Ok(Stream { key, times, requests })
68
+ })
69
+ .collect()
70
+ }
71
+
72
+ fn find(bytes: &[u8], needle: &[u8]) -> Option<usize> {
73
+ bytes.windows(needle.len()).position(|window| window == needle)
74
+ }
75
+
76
+ /// The end of a chunked body that starts at `at`, or None while the body is not all there.
77
+ fn chunked_end(bytes: &[u8], mut at: usize) -> Result<Option<usize>, String> {
78
+ loop {
79
+ let Some(line) = find(&bytes[at..], b"\r\n") else { return Ok(None) };
80
+ let text = std::str::from_utf8(&bytes[at..at + line]).map_err(|_| "a chunk size that is not UTF-8".to_string())?;
81
+ let digits = text.split(';').next().unwrap_or("").trim();
82
+ let size = usize::from_str_radix(digits, 16).map_err(|_| format!("the chunk size {text:?}"))?;
83
+ at += line + 2;
84
+ if size == 0 {
85
+ // the trailers, if any, end at an empty line
86
+ if bytes.len() < at + 2 {
87
+ return Ok(None);
88
+ }
89
+ if &bytes[at..at + 2] == b"\r\n" {
90
+ return Ok(Some(at + 2));
91
+ }
92
+ return Ok(find(&bytes[at..], b"\r\n\r\n").map(|end| at + end + 4));
93
+ }
94
+ if bytes.len() < at + size + 2 {
95
+ return Ok(None);
96
+ }
97
+ if &bytes[at + size..at + size + 2] != b"\r\n" {
98
+ return Err("a chunk with no CRLF after it".into());
99
+ }
100
+ at += size + 2;
101
+ }
102
+ }
103
+
104
+ /// The length and the status of the first response in `bytes`, or None while it is not all there.
105
+ /// A response has a Content-Length or a chunked body, or it has no body (204 and 304). A response
106
+ /// with no length ends only at the end of its connection, thus the driver refuses it.
107
+ fn response_length(bytes: &[u8]) -> Result<Option<(usize, u16)>, String> {
108
+ let Some(end) = find(bytes, b"\r\n\r\n") else {
109
+ return if bytes.len() > HEAD_LIMIT { Err(format!("a head of more than {HEAD_LIMIT} bytes")) } else { Ok(None) };
110
+ };
111
+ let head = std::str::from_utf8(&bytes[..end]).map_err(|_| "a head that is not UTF-8".to_string())?;
112
+ let mut lines = head.split("\r\n");
113
+ let status_line = lines.next().unwrap_or("");
114
+ let mut parts = status_line.splitn(3, ' ');
115
+ let version = parts.next().unwrap_or("");
116
+ let status = parts.next().and_then(|code| code.parse::<u16>().ok());
117
+ let Some(status) = status.filter(|_| version.starts_with("HTTP/1.")) else {
118
+ return Err(format!("the status line {status_line:?}"));
119
+ };
120
+ if status < 200 {
121
+ return Err(format!("an interim response {status}: the driver sends no Expect and no Upgrade"));
122
+ }
123
+ let (mut length, mut chunked) = (None, false);
124
+ for line in lines {
125
+ let Some((name, value)) = line.split_once(':') else { return Err(format!("the header line {line:?}")) };
126
+ let value = value.trim();
127
+ if name.eq_ignore_ascii_case("content-length") {
128
+ length = Some(value.parse::<usize>().map_err(|_| format!("the length {value:?}"))?);
129
+ } else if name.eq_ignore_ascii_case("transfer-encoding") {
130
+ chunked = value.split(',').any(|coding| coding.trim().eq_ignore_ascii_case("chunked"));
131
+ }
132
+ }
133
+ let body = end + 4;
134
+ if status == 204 || status == 304 {
135
+ return Ok(Some((body, status)));
136
+ }
137
+ if chunked {
138
+ return Ok(chunked_end(bytes, body)?.map(|whole| (whole, status)));
139
+ }
140
+ match length {
141
+ Some(length) => Ok((bytes.len() >= body + length).then_some((body + length, status))),
142
+ None => Err(format!("a response of status {status} with no length")),
143
+ }
144
+ }
145
+
146
+ /// One stream on its connection, and where it is in its schedule.
147
+ struct Connection {
148
+ stream: Stream,
149
+ socket: TcpStream,
150
+ /// the next arrival to send, and the bytes of it already written
151
+ next: usize,
152
+ written: usize,
153
+ answered: usize,
154
+ /// the socket took no more bytes: the next write waits for POLLOUT
155
+ blocked: bool,
156
+ open: bool,
157
+ /// the bytes received and not yet a whole response
158
+ buffer: Vec<u8>,
159
+ }
160
+
161
+ impl Connection {
162
+ fn open(host: &str, port: u16, stream: Stream) -> std::io::Result<Self> {
163
+ let socket = TcpStream::connect((host, port))?;
164
+ socket.set_nodelay(true)?;
165
+ socket.set_nonblocking(true)?;
166
+ Ok(Self { stream, socket, next: 0, written: 0, answered: 0, blocked: false, open: true, buffer: Vec::new() })
167
+ }
168
+
169
+ fn finished(&self) -> bool {
170
+ !self.open || self.answered == self.stream.times.len()
171
+ }
172
+
173
+ fn fail(&mut self, out: &mut dyn FnMut(&str, u64, Value), error: &std::io::Error) {
174
+ out("error", monotonic_ns(), json!({ "k": self.stream.key, "errno": errno_name(error.raw_os_error().unwrap_or(0)) }));
175
+ self.open = false;
176
+ }
177
+
178
+ /// Writes each arrival that is due at `start`, until the socket takes no more.
179
+ fn send_due(&mut self, start: u64, out: &mut dyn FnMut(&str, u64, Value)) {
180
+ while self.open && !self.blocked && self.next < self.stream.times.len() && monotonic_ns() >= start + self.stream.times[self.next] {
181
+ let request = &self.stream.requests[self.next % self.stream.requests.len()];
182
+ let t = monotonic_ns();
183
+ match self.socket.write(&request[self.written..]) {
184
+ Ok(n) => {
185
+ self.written += n;
186
+ if self.written == request.len() {
187
+ out("sent", t, json!({ "k": self.stream.key, "i": self.next }));
188
+ self.next += 1;
189
+ self.written = 0;
190
+ }
191
+ }
192
+ Err(error) if error.kind() == ErrorKind::WouldBlock => self.blocked = true,
193
+ Err(error) if error.kind() == ErrorKind::Interrupted => {}
194
+ Err(error) => {
195
+ out("error", monotonic_ns(), json!({ "k": self.stream.key, "errno": errno_name(error.raw_os_error().unwrap_or(0)) }));
196
+ self.open = false;
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ /// Reads what the socket holds, and gives a replied line for each whole response.
203
+ fn receive(&mut self, buf: &mut [u8], out: &mut dyn FnMut(&str, u64, Value)) {
204
+ while self.open {
205
+ match self.socket.read(buf) {
206
+ Ok(0) => {
207
+ out("eof", monotonic_ns(), json!({ "k": self.stream.key }));
208
+ self.open = false;
209
+ }
210
+ Ok(n) => {
211
+ let t = monotonic_ns();
212
+ self.buffer.extend_from_slice(&buf[..n]);
213
+ self.take_responses(t, out);
214
+ }
215
+ Err(error) if error.kind() == ErrorKind::WouldBlock => return,
216
+ Err(error) if error.kind() == ErrorKind::Interrupted => {}
217
+ Err(error) => self.fail(out, &error),
218
+ }
219
+ }
220
+ }
221
+
222
+ fn take_responses(&mut self, t: u64, out: &mut dyn FnMut(&str, u64, Value)) {
223
+ loop {
224
+ match response_length(&self.buffer) {
225
+ Ok(None) => return,
226
+ Ok(Some((length, status))) => {
227
+ self.buffer.drain(..length);
228
+ out("replied", t, json!({ "k": self.stream.key, "i": self.answered, "status": status }));
229
+ self.answered += 1;
230
+ }
231
+ Err(why) => {
232
+ out("error", t, json!({ "k": self.stream.key, "why": why }));
233
+ self.open = false;
234
+ return;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ /// Waits up to `ns` for the open connections. A wait in ms is too coarse for a schedule, thus this
242
+ /// one is ppoll(). It gives the revents of each connection, 0 for one that is not open.
243
+ fn wait_for(connections: &[Connection], ns: u64) -> std::io::Result<Vec<i16>> {
244
+ let mut polled: Vec<libc::pollfd> = connections
245
+ .iter()
246
+ .map(|connection| {
247
+ let writing = if connection.blocked { libc::POLLOUT } else { 0 };
248
+ // a negative fd is skipped by ppoll(), and its revents stay 0
249
+ let fd = if connection.open { connection.socket.as_raw_fd() } else { -1 };
250
+ libc::pollfd { fd, events: libc::POLLIN | libc::POLLRDHUP | writing, revents: 0 }
251
+ })
252
+ .collect();
253
+ 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 };
254
+ // SAFETY: the pollfds and the timespec live on this frame, with their exact count, and no signal mask
255
+ match unsafe { libc::ppoll(polled.as_mut_ptr(), polled.len() as libc::nfds_t, &time, std::ptr::null()) } {
256
+ -1 if std::io::Error::last_os_error().kind() == ErrorKind::Interrupted => Ok(vec![0; connections.len()]),
257
+ -1 => Err(std::io::Error::last_os_error()),
258
+ _ => Ok(polled.iter().map(|entry| entry.revents).collect()),
259
+ }
260
+ }
261
+
262
+ /// Runs the schedule of each connection from `start`, until each connection is finished or the
263
+ /// deadline passes. A wait is at most `tick` ns.
264
+ fn drive(connections: &mut [Connection], start: u64, deadline: u64, tick: u64, out: &mut dyn FnMut(&str, u64, Value)) -> Result<(), Box<dyn Error>> {
265
+ let mut buf = vec![0u8; 65_536];
266
+ loop {
267
+ for connection in connections.iter_mut() {
268
+ connection.send_due(start, out);
269
+ }
270
+ if connections.iter().all(Connection::finished) {
271
+ let requests: usize = connections.iter().map(|connection| connection.next).sum();
272
+ out("done", monotonic_ns(), json!({ "requests": requests }));
273
+ return Ok(());
274
+ }
275
+ let now = monotonic_ns();
276
+ if now > deadline {
277
+ out("deadline", now, json!({}));
278
+ return Ok(());
279
+ }
280
+ let due = connections
281
+ .iter()
282
+ .filter(|connection| connection.open && !connection.blocked)
283
+ .filter_map(|connection| connection.stream.times.get(connection.next))
284
+ .map(|at| (start + at).saturating_sub(now))
285
+ .fold(tick, u64::min);
286
+ let revents = wait_for(connections, due)?;
287
+ for (connection, events) in connections.iter_mut().zip(revents) {
288
+ if events & libc::POLLOUT != 0 {
289
+ connection.blocked = false;
290
+ }
291
+ if events & !libc::POLLOUT != 0 {
292
+ connection.receive(&mut buf, out);
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ struct Args {
299
+ host: String,
300
+ port: u16,
301
+ plan: String,
302
+ go: String,
303
+ sample: Duration,
304
+ max: Duration,
305
+ }
306
+
307
+ fn args() -> Result<Args, Box<dyn Error>> {
308
+ let raw: Vec<String> = std::env::args().skip(1).collect();
309
+ let value = |flag: &str| raw.iter().position(|arg| arg == flag).and_then(|at| raw.get(at + 1)).cloned();
310
+ Ok(Args {
311
+ host: raw.first().ok_or("the target's host comes first")?.clone(),
312
+ port: raw.get(1).ok_or("then its port")?.parse()?,
313
+ plan: value("--plan").ok_or("--plan is required")?,
314
+ go: value("--go").unwrap_or_else(|| "/tmp/go".into()),
315
+ sample: Duration::from_micros((value("--sample-ms").map(|v| v.parse::<f64>()).transpose()?.unwrap_or(2.0) * 1000.0) as u64),
316
+ max: Duration::from_secs_f64(value("--max-s").map(|v| v.parse::<f64>()).transpose()?.unwrap_or(120.0)),
317
+ })
318
+ }
319
+
320
+ fn run() -> Result<(), Box<dyn Error>> {
321
+ let args = args()?;
322
+ let boot_id = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")?.trim().to_string();
323
+ let streams = plan_of(&std::fs::read_to_string(&args.plan)?)?;
324
+ emit("start", json!({ "boot_id": boot_id, "mode": "load", "streams": streams.len() }));
325
+ let deadline = monotonic_ns() + args.max.as_nanos() as u64;
326
+ let mut connections = Vec::with_capacity(streams.len());
327
+ for stream in streams {
328
+ let key = stream.key.clone();
329
+ let connection = Connection::open(&args.host, args.port, stream)?;
330
+ // the harness finds the socket of a stream in the records of the kernel by this address
331
+ emit("connected", json!({ "k": key, "local": connection.socket.local_addr()?.to_string() }));
332
+ connections.push(connection);
333
+ }
334
+ emit("ready", json!({ "streams": connections.len() }));
335
+ while !Path::new(&args.go).exists() {
336
+ if monotonic_ns() > deadline {
337
+ emit("deadline", json!({}));
338
+ return Ok(());
339
+ }
340
+ std::thread::sleep(args.sample);
341
+ }
342
+ let start = monotonic_ns();
343
+ let arrivals: usize = connections.iter().map(|connection| connection.stream.times.len()).sum();
344
+ emit_at("scheduled", start, json!({ "streams": connections.len(), "arrivals": arrivals }));
345
+ drive(&mut connections, start, deadline, args.sample.as_nanos() as u64, &mut |event, t, fields| emit_at(event, t, fields))
346
+ }
347
+
348
+ fn main() {
349
+ if let Err(error) = run() {
350
+ emit("fail", json!({ "why": error.to_string() }));
351
+ std::process::exit(1);
352
+ }
353
+ }
354
+
355
+ #[cfg(test)]
356
+ mod tests {
357
+ use super::*;
358
+ use std::net::TcpListener;
359
+
360
+ const OK: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok";
361
+
362
+ #[test]
363
+ fn a_response_is_whole_after_its_length_or_its_last_chunk() {
364
+ assert_eq!(response_length(OK), Ok(Some((40, 200))));
365
+ assert_eq!(response_length(&OK[..39]), Ok(None));
366
+ let empty = b"HTTP/1.1 204 No Content\r\nX: y\r\n\r\n";
367
+ assert_eq!(response_length(empty), Ok(Some((empty.len(), 204))));
368
+ let chunked = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\n\r\n";
369
+ assert_eq!(response_length(chunked), Ok(Some((chunked.len(), 200))));
370
+ for cut in 0..chunked.len() {
371
+ assert_eq!(response_length(&chunked[..cut]), Ok(None), "cut at {cut}");
372
+ }
373
+ let trailers = b"HTTP/1.1 500 Error\r\ntransfer-encoding: chunked\r\n\r\n2;x=y\r\nab\r\n0\r\nT: 1\r\n\r\n";
374
+ assert_eq!(response_length(trailers), Ok(Some((trailers.len(), 500))));
375
+ }
376
+
377
+ #[test]
378
+ fn a_response_that_the_driver_cannot_frame_is_an_error() {
379
+ assert!(response_length(b"HTTP/1.1 200 OK\r\n\r\nbody").is_err());
380
+ assert!(response_length(b"HTTP/1.1 100 Continue\r\n\r\n").is_err());
381
+ assert!(response_length(b"SPDY/3 200 OK\r\nContent-Length: 0\r\n\r\n").is_err());
382
+ assert!(response_length(b"HTTP/1.1 200 OK\r\nContent-Length: x\r\n\r\n").is_err());
383
+ assert!(response_length(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n").is_err());
384
+ assert!(response_length(&vec![b'a'; HEAD_LIMIT + 1]).is_err());
385
+ }
386
+
387
+ #[test]
388
+ fn a_pipeline_of_responses_splits_at_each_end_in_reads_of_any_size() {
389
+ let parts: [&[u8]; 4] = [OK, b"HTTP/1.1 204 No Content\r\n\r\n", b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n1\r\na\r\n0\r\n\r\n", OK];
390
+ let bytes = parts.concat();
391
+ for size in 1..bytes.len() {
392
+ let (mut buffer, mut statuses) = (Vec::new(), Vec::new());
393
+ for read in bytes.chunks(size) {
394
+ buffer.extend_from_slice(read);
395
+ while let Some((length, status)) = response_length(&buffer).unwrap() {
396
+ buffer.drain(..length);
397
+ statuses.push(status);
398
+ }
399
+ }
400
+ assert_eq!(statuses, [200, 204, 200, 200], "reads of {size}");
401
+ assert!(buffer.is_empty());
402
+ }
403
+ }
404
+
405
+ #[test]
406
+ fn a_plan_gives_its_streams_and_refuses_a_bad_one() {
407
+ let schedule = r#"{"format":"rigor-schedule/1","times":[0,1.5]}"#;
408
+ let plan = format!(r#"{{"format":"systems-load/1","streams":[{{"key":"t0","schedule":{schedule},"requests":["A","B"]}}]}}"#);
409
+ let streams = plan_of(&plan).unwrap();
410
+ assert_eq!(streams.len(), 1);
411
+ assert_eq!(streams[0].key, "t0");
412
+ assert_eq!(streams[0].times, [0, 1_500_000]);
413
+ assert_eq!(streams[0].requests, [b"A".to_vec(), b"B".to_vec()]);
414
+ assert!(plan_of(r#"{"format":"other","streams":[]}"#).is_err());
415
+ assert!(plan_of(r#"{"format":"systems-load/1","streams":[]}"#).is_err());
416
+ let twice = format!(r#"{{"format":"systems-load/1","streams":[{{"key":"a","schedule":{schedule},"requests":["A"]}},{{"key":"a","schedule":{schedule},"requests":["A"]}}]}}"#);
417
+ assert!(plan_of(&twice).is_err());
418
+ let none = format!(r#"{{"format":"systems-load/1","streams":[{{"key":"a","schedule":{schedule},"requests":[]}}]}}"#);
419
+ assert!(plan_of(&none).is_err());
420
+ }
421
+
422
+ /// A server that answers each request of each connection with OK, after `delay` for the key `slow`.
423
+ fn serve(connections: usize, slow: Duration) -> u16 {
424
+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
425
+ let port = listener.local_addr().unwrap().port();
426
+ std::thread::spawn(move || {
427
+ for socket in listener.incoming().take(connections) {
428
+ let mut socket = socket.unwrap();
429
+ std::thread::spawn(move || {
430
+ let (mut buffer, mut buf) = (Vec::new(), [0u8; 4096]);
431
+ while let Ok(n) = socket.read(&mut buf) {
432
+ if n == 0 {
433
+ return;
434
+ }
435
+ buffer.extend_from_slice(&buf[..n]);
436
+ while let Some(end) = find(&buffer, b"\r\n\r\n") {
437
+ let request: Vec<u8> = buffer.drain(..end + 4).collect();
438
+ if find(&request, b"slow").is_some() {
439
+ std::thread::sleep(slow);
440
+ }
441
+ if socket.write_all(OK).is_err() {
442
+ return;
443
+ }
444
+ }
445
+ }
446
+ });
447
+ }
448
+ });
449
+ port
450
+ }
451
+
452
+ fn stream(key: &str, times: &[u64]) -> Stream {
453
+ let request = format!("GET /{key} HTTP/1.1\r\nHost: t\r\n\r\n");
454
+ Stream { key: key.into(), times: times.to_vec(), requests: vec![request.into_bytes()] }
455
+ }
456
+
457
+ #[test]
458
+ fn each_stream_gets_each_reply_and_a_slow_stream_delays_only_itself() {
459
+ let port = serve(2, Duration::from_millis(200));
460
+ let ms = 1_000_000;
461
+ let mut connections = vec![
462
+ Connection::open("127.0.0.1", port, stream("slow", &[0, ms, 2 * ms])).unwrap(),
463
+ Connection::open("127.0.0.1", port, stream("fast", &[0, ms, 2 * ms])).unwrap(),
464
+ ];
465
+ let mut events: Vec<(String, u64, Value)> = Vec::new();
466
+ let start = monotonic_ns();
467
+ let deadline = start + 10_000 * ms;
468
+ drive(&mut connections, start, deadline, ms, &mut |event, t, fields| events.push((event.into(), t, fields))).unwrap();
469
+ let replied = |key: &str| -> Vec<u64> {
470
+ events.iter().filter(|(event, _, fields)| event == "replied" && fields["k"] == key).map(|(_, t, _)| t - start).collect()
471
+ };
472
+ assert_eq!(replied("slow").len(), 3);
473
+ assert_eq!(replied("fast").len(), 3);
474
+ assert!(events.iter().filter(|(event, _, _)| event == "replied").all(|(_, _, fields)| fields["status"] == 200));
475
+ // the last reply of the fast stream comes before the first of the slow one
476
+ assert!(replied("fast")[2] < replied("slow")[0], "{events:?}");
477
+ let sent = events.iter().filter(|(event, _, _)| event == "sent").count();
478
+ assert_eq!(sent, 6);
479
+ assert_eq!(events.last().map(|(event, _, fields)| (event.as_str(), fields["requests"].clone())), Some(("done", json!(6))));
480
+ }
481
+ }
@@ -26,6 +26,7 @@
26
26
  //! reset and holds no port. A FIN at the exit of the client holds that port in TIME_WAIT for 60 s. A
27
27
  //! range of 3 has only 2 ports for a search. Thus the third run of an environment had no control port.
28
28
 
29
+ use harness_native::emit;
29
30
  use prober::{errno_name, monotonic_ns};
30
31
  use serde_json::{json, Value};
31
32
  use std::error::Error;
@@ -40,14 +41,6 @@ const LIMIT: usize = 256;
40
41
  /// how long `close COUNT` waits for the accept of the last connection, in milliseconds
41
42
  const ACCEPT_WAIT_MS: u64 = 2_000;
42
43
 
43
- fn emit(event: &str, fields: Value) {
44
- let mut line = json!({ "ev": event, "t": monotonic_ns() });
45
- if let (Some(line), Value::Object(fields)) = (line.as_object_mut(), fields) {
46
- line.extend(fields);
47
- }
48
- println!("{line}");
49
- }
50
-
51
44
  fn locked(held: &Mutex<Vec<TcpStream>>) -> MutexGuard<'_, Vec<TcpStream>> {
52
45
  held.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
53
46
  }
@@ -30,6 +30,7 @@
30
30
  //! 6. It writes the state of each connection, drains the queue with `--drain`, and writes the
31
31
  //! counters and `done`.
32
32
 
33
+ use harness_native::{emit, emit_at};
33
34
  use prober::{errno_name, monotonic_ns};
34
35
  use serde_json::{json, Value};
35
36
  use std::error::Error;
@@ -45,18 +46,6 @@ const HANDSHAKE_NS: u64 = 300_000_000;
45
46
  /// the longest count that a drain asks for
46
47
  const DRAIN: i32 = 1024;
47
48
 
48
- fn emit(event: &str, fields: Value) {
49
- emit_at(event, monotonic_ns(), fields);
50
- }
51
-
52
- fn emit_at(event: &str, t: u64, fields: Value) {
53
- let mut line = json!({ "ev": event, "t": t });
54
- if let (Some(line), Value::Object(fields)) = (line.as_object_mut(), fields) {
55
- line.extend(fields);
56
- }
57
- println!("{line}");
58
- }
59
-
60
49
  fn set_int(fd: i32, level: i32, name: i32, value: i32) -> std::io::Result<()> {
61
50
  // SAFETY: the option value is an int on this stack, passed with its exact size
62
51
  let status = unsafe {
@@ -0,0 +1,51 @@
1
+ //! What the programs of the harness's native side share: their event lines and the arrivals of a
2
+ //! schedule.
3
+
4
+ use prober::monotonic_ns;
5
+ use serde_json::{json, Value};
6
+ use std::error::Error;
7
+
8
+ /// Prints one event line, with the time of the call on CLOCK_MONOTONIC.
9
+ pub fn emit(event: &str, fields: Value) {
10
+ emit_at(event, monotonic_ns(), fields);
11
+ }
12
+
13
+ /// Prints one event line with the stamp `t`: a JSON object with `ev`, `t` and the fields.
14
+ pub fn emit_at(event: &str, t: u64, fields: Value) {
15
+ let mut line = json!({ "ev": event, "t": t });
16
+ if let (Some(line), Value::Object(fields)) = (line.as_object_mut(), fields) {
17
+ line.extend(fields);
18
+ }
19
+ println!("{line}");
20
+ }
21
+
22
+ /// The arrivals of a schedule of the format rigor-schedule/1, each in ns from the start of the schedule.
23
+ pub fn arrivals_in(plan: &Value) -> Result<Vec<u64>, Box<dyn Error>> {
24
+ if plan["format"] != "rigor-schedule/1" {
25
+ return Err(format!("the schedule has the format {}, not rigor-schedule/1", plan["format"]).into());
26
+ }
27
+ let times = plan["times"].as_array().ok_or("the schedule has no list of times")?;
28
+ times
29
+ .iter()
30
+ .map(|time| time.as_f64().map(|ms| (ms * 1e6) as u64).ok_or_else(|| Box::<dyn Error>::from(format!("{time} is not a time"))))
31
+ .collect()
32
+ }
33
+
34
+ /// The arrivals of a file of the format rigor-schedule/1.
35
+ pub fn arrivals_of(text: &str) -> Result<Vec<u64>, Box<dyn Error>> {
36
+ arrivals_in(&serde_json::from_str(text)?)
37
+ }
38
+
39
+ #[cfg(test)]
40
+ mod tests {
41
+ use super::*;
42
+
43
+ #[test]
44
+ fn a_schedule_gives_its_times_in_ns_and_refuses_another_format() {
45
+ let plan = r#"{"format":"rigor-schedule/1","times":[0.5,10,20.25]}"#;
46
+ assert_eq!(arrivals_of(plan).unwrap(), [500_000, 10_000_000, 20_250_000]);
47
+ assert!(arrivals_of(r#"{"format":"other","times":[]}"#).is_err());
48
+ assert!(arrivals_of(r#"{"format":"rigor-schedule/1"}"#).is_err());
49
+ assert!(arrivals_of(r#"{"format":"rigor-schedule/1","times":["a"]}"#).is_err());
50
+ }
51
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/harness",
4
- "version": "0.14.4",
4
+ "version": "0.15.0",
5
5
  "description": "A fault-injection harness for connection timers: a cell runs a client against a stock target in Docker, through a topology of routers and proxies, a replicated group under Sentinels or a replica set, injects a fault, measures the kernel's own records and judges a prediction. HTTP clients of Go, Node and undici race the keep-alive of a Node server, and the MongoDB driver meets the faults of a replica set. The WebSocket client of @procedures/client drops a silent connection on its own heartbeat, and the harness times that detection and the reconnect from the program's lines and the kernel's stamps.",
6
6
  "keywords": [
7
7
  "fault-injection",
@@ -75,6 +75,7 @@
75
75
  "image/kafka/Dockerfile",
76
76
  "image/kafka/ClientKafka.java",
77
77
  "image/kafka/AdminKafka.java",
78
+ "image/kafka/ConsumerKafka.java",
78
79
  "image/kafka/jars.sha256",
79
80
  "image/amqp/Dockerfile",
80
81
  "image/amqp/client-amqp.mjs",
@@ -106,7 +107,9 @@
106
107
  "dependencies": {
107
108
  "@mark1russell7/prober": "^0.3.2",
108
109
  "@mark1russell7/quantity": "^0.3.0",
109
- "@mark1russell7/rigor-statistics": "^0.1.3",
110
+ "@mark1russell7/rigor-bench": "^0.2.10",
111
+ "@mark1russell7/rigor-chaos": "^0.1.0",
112
+ "@mark1russell7/rigor-statistics": "^0.1.11",
110
113
  "testcontainers": "12.2.0"
111
114
  },
112
115
  "peerDependencies": {