@mark1russell7/harness 0.14.3 → 0.14.4

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,553 @@
1
+ //! queue target PORT
2
+ //! queue client [--backlog N] [--fill N] [--settle-ms N] [--burst N] [--blocked] [--late-ms N]
3
+ //! [--accept N] [--accept-at-ms N] [--hold-ms N] [--drain] HOST PORT [--mode M] [--max-s N]
4
+ //!
5
+ //! This program is the two ends of the accept queue cells: a listener that does not accept, and a
6
+ //! client of many connections. Each line is JSON with an `ev` and a `t` on CLOCK_MONOTONIC.
7
+ //!
8
+ //! The target serves commands on PORT + 1, its control port, one line for each command:
9
+ //!
10
+ //! - `open BACKLOG`: closes the listener of the run before, and calls listen(2) on PORT with
11
+ //! BACKLOG. Thus each run starts with an empty queue. The harness sets the sysctls of the
12
+ //! namespace after the target starts, and listen(2) reads somaxconn in this command, after them.
13
+ //! - `accept COUNT`: takes up to COUNT connections from the queue.
14
+ //! - `stats`: gives the count and the counters at this time.
15
+ //!
16
+ //! A second thread reads the queue each 5 ms, and it writes a `queue` line for each change. TCP_INFO
17
+ //! of a listener gives the count (`tcpi_unacked`) and the limit (`tcpi_sacked`). /proc/net/netstat
18
+ //! gives the counters of the namespace.
19
+ //!
20
+ //! The client does these steps in sequence:
21
+ //!
22
+ //! 1. It opens the listener and writes `ready`.
23
+ //! 2. `--fill` connections, each one after the one before ends or `--settle-ms` passes.
24
+ //! 3. `--burst` connections at one time: `in`, `busy` and `quiet`. All of their SYNs arrive inside
25
+ //! one round trip. `in` and `busy` send one byte when they connect.
26
+ //! 4. `--blocked` adds one connection after the burst.
27
+ //! 5. It waits until each connection after the fill ends, or `--hold-ms` passes after the burst. In
28
+ //! this wait, `quiet` sends one byte at `--late-ms`, and the target accepts `--accept`
29
+ //! connections at `--accept-at-ms`.
30
+ //! 6. It writes the state of each connection, drains the queue with `--drain`, and writes the
31
+ //! counters and `done`.
32
+
33
+ use prober::{errno_name, monotonic_ns};
34
+ use serde_json::{json, Value};
35
+ use std::error::Error;
36
+ use std::io::{BufRead, BufReader, ErrorKind, Write};
37
+ use std::net::{Ipv4Addr, TcpListener, TcpStream};
38
+ use std::sync::{Arc, Mutex, MutexGuard};
39
+ use std::time::Duration;
40
+
41
+ /// the names of the connections of a burst, in the sequence of their SYNs
42
+ const BURST: [&str; 3] = ["in", "busy", "quiet"];
43
+ /// how long a burst waits for its handshakes, in nanoseconds
44
+ const HANDSHAKE_NS: u64 = 300_000_000;
45
+ /// the longest count that a drain asks for
46
+ const DRAIN: i32 = 1024;
47
+
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
+ fn set_int(fd: i32, level: i32, name: i32, value: i32) -> std::io::Result<()> {
61
+ // SAFETY: the option value is an int on this stack, passed with its exact size
62
+ let status = unsafe {
63
+ libc::setsockopt(
64
+ fd,
65
+ level,
66
+ name,
67
+ (&value as *const i32).cast(),
68
+ std::mem::size_of::<i32>() as libc::socklen_t,
69
+ )
70
+ };
71
+ if status == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) }
72
+ }
73
+
74
+ fn tcp_info(fd: i32) -> std::io::Result<libc::tcp_info> {
75
+ // SAFETY: tcp_info is plain old data, and the kernel writes at most the length it is given
76
+ let mut info: libc::tcp_info = unsafe { std::mem::zeroed() };
77
+ let mut len = std::mem::size_of::<libc::tcp_info>() as libc::socklen_t;
78
+ let status = unsafe {
79
+ libc::getsockopt(fd, libc::IPPROTO_TCP, libc::TCP_INFO, (&mut info as *mut libc::tcp_info).cast(), &mut len)
80
+ };
81
+ if status == 0 { Ok(info) } else { Err(std::io::Error::last_os_error()) }
82
+ }
83
+
84
+ fn socket_error(fd: i32) -> std::io::Result<i32> {
85
+ let mut code: i32 = 0;
86
+ let mut len = std::mem::size_of::<i32>() as libc::socklen_t;
87
+ // SAFETY: the option value is an int on this stack, passed with its exact size
88
+ let status = unsafe {
89
+ libc::getsockopt(fd, libc::SOL_SOCKET, libc::SO_ERROR, (&mut code as *mut i32).cast(), &mut len)
90
+ };
91
+ if status == 0 { Ok(code) } else { Err(std::io::Error::last_os_error()) }
92
+ }
93
+
94
+ /// the counters of /proc/net/netstat that the law of the queue reads
95
+ #[derive(Clone, Copy, Default, PartialEq)]
96
+ struct Counters {
97
+ overflows: u64,
98
+ drops: u64,
99
+ cookies: u64,
100
+ timeouts: u64,
101
+ resets: u64,
102
+ }
103
+
104
+ /// Reads the counters from the text of /proc/net/netstat: a line of names, then a line of values.
105
+ fn counters_of(text: &str) -> Counters {
106
+ let mut found = Counters::default();
107
+ let mut lines = text.lines();
108
+ while let (Some(names), Some(values)) = (lines.next(), lines.next()) {
109
+ if !names.starts_with("TcpExt:") {
110
+ continue;
111
+ }
112
+ for (name, value) in names.split_whitespace().zip(values.split_whitespace()) {
113
+ let value = value.parse::<u64>().unwrap_or(0);
114
+ match name {
115
+ "ListenOverflows" => found.overflows = value,
116
+ "ListenDrops" => found.drops = value,
117
+ "TCPReqQFullDoCookies" => found.cookies = value,
118
+ "TCPTimeouts" => found.timeouts = value,
119
+ "EmbryonicRsts" => found.resets = value,
120
+ _ => {}
121
+ }
122
+ }
123
+ }
124
+ found
125
+ }
126
+
127
+ fn counters() -> Counters {
128
+ std::fs::read_to_string("/proc/net/netstat").map(|text| counters_of(&text)).unwrap_or_default()
129
+ }
130
+
131
+ fn fields(count: u32, max: u32, seen: Counters) -> Value {
132
+ json!({
133
+ "count": count,
134
+ "max": max,
135
+ "overflows": seen.overflows,
136
+ "drops": seen.drops,
137
+ "cookies": seen.cookies,
138
+ "timeouts": seen.timeouts,
139
+ "resets": seen.resets,
140
+ })
141
+ }
142
+
143
+ /// the listener of the run, and the connections that it accepted
144
+ #[derive(Default)]
145
+ struct Queue {
146
+ listener: Option<i32>,
147
+ accepted: Vec<i32>,
148
+ }
149
+
150
+ fn locked(queue: &Mutex<Queue>) -> MutexGuard<'_, Queue> {
151
+ queue.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
152
+ }
153
+
154
+ /// Reads the count and the limit of the queue, with the lock held: `open` closes the listener under it.
155
+ fn reading(queue: &Mutex<Queue>) -> Option<(u32, u32)> {
156
+ let held = locked(queue);
157
+ let info = tcp_info(held.listener?).ok()?;
158
+ Some((info.tcpi_unacked, info.tcpi_sacked))
159
+ }
160
+
161
+ fn stats(queue: &Mutex<Queue>) -> Value {
162
+ let (count, max) = reading(queue).unwrap_or((0, 0));
163
+ fields(count, max, counters())
164
+ }
165
+
166
+ fn open(queue: &Mutex<Queue>, port: u16, backlog: i32) -> Result<Value, Box<dyn Error>> {
167
+ let mut held = locked(queue);
168
+ for fd in held.accepted.drain(..) {
169
+ // SAFETY: this program opened each descriptor, and it closes each one time
170
+ unsafe { libc::close(fd) };
171
+ }
172
+ if let Some(fd) = held.listener.take() {
173
+ // SAFETY: as above. The close resets each connection that the queue still holds
174
+ unsafe { libc::close(fd) };
175
+ }
176
+ // SAFETY: socket(2) takes no pointer
177
+ let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_NONBLOCK, 0) };
178
+ if fd < 0 {
179
+ return Err(std::io::Error::last_os_error().into());
180
+ }
181
+ set_int(fd, libc::SOL_SOCKET, libc::SO_REUSEADDR, 1)?;
182
+ // SAFETY: sockaddr_in is plain old data. Zero is INADDR_ANY
183
+ let mut address: libc::sockaddr_in = unsafe { std::mem::zeroed() };
184
+ address.sin_family = libc::AF_INET as libc::sa_family_t;
185
+ address.sin_port = port.to_be();
186
+ // SAFETY: the address is on this stack, passed with its exact size
187
+ let bound = unsafe {
188
+ libc::bind(
189
+ fd,
190
+ (&address as *const libc::sockaddr_in).cast(),
191
+ std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
192
+ )
193
+ };
194
+ // SAFETY: listen(2) takes no pointer
195
+ if bound != 0 || unsafe { libc::listen(fd, backlog) } != 0 {
196
+ return Err(std::io::Error::last_os_error().into());
197
+ }
198
+ held.listener = Some(fd);
199
+ drop(held);
200
+ Ok(stats(queue))
201
+ }
202
+
203
+ fn accept(queue: &Mutex<Queue>, count: i32) -> Result<Value, Box<dyn Error>> {
204
+ let mut held = locked(queue);
205
+ let fd = held.listener.ok_or("no listener")?;
206
+ let mut peers = Vec::new();
207
+ for _ in 0..count {
208
+ // SAFETY: sockaddr_in is plain old data
209
+ let mut peer: libc::sockaddr_in = unsafe { std::mem::zeroed() };
210
+ let mut len = std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t;
211
+ // SAFETY: the kernel writes at most `len` bytes to the address on this stack
212
+ let child = unsafe { libc::accept(fd, (&mut peer as *mut libc::sockaddr_in).cast(), &mut len) };
213
+ // the listener does not block: an empty queue gives EAGAIN
214
+ if child < 0 {
215
+ break;
216
+ }
217
+ held.accepted.push(child);
218
+ peers.push(u16::from_be(peer.sin_port));
219
+ }
220
+ Ok(json!({ "peers": peers }))
221
+ }
222
+
223
+ fn serve(stream: TcpStream, queue: &Mutex<Queue>, port: u16) -> Result<(), Box<dyn Error>> {
224
+ stream.set_nodelay(true)?;
225
+ let mut writer = stream.try_clone()?;
226
+ for line in BufReader::new(stream).lines() {
227
+ let line = line?;
228
+ let mut words = line.split_whitespace();
229
+ let command = words.next();
230
+ let number = words.next().and_then(|word| word.parse::<i32>().ok());
231
+ let reply = match (command, number) {
232
+ (Some("open"), Some(backlog)) => open(queue, port, backlog)?,
233
+ (Some("accept"), Some(count)) => accept(queue, count)?,
234
+ (Some("stats"), _) => stats(queue),
235
+ _ => json!({ "refused": line }),
236
+ };
237
+ writeln!(writer, "{reply}")?;
238
+ }
239
+ Ok(())
240
+ }
241
+
242
+ /// Writes a `queue` line for each change of the queue or of its counters, until the process ends.
243
+ fn sample(queue: &Mutex<Queue>) {
244
+ let mut last = None;
245
+ loop {
246
+ let now = reading(queue).map(|(count, max)| (count, max, counters()));
247
+ if now != last {
248
+ if let Some((count, max, seen)) = now {
249
+ emit("queue", fields(count, max, seen));
250
+ }
251
+ last = now;
252
+ }
253
+ std::thread::sleep(Duration::from_millis(5));
254
+ }
255
+ }
256
+
257
+ fn target(args: &[String]) -> Result<(), Box<dyn Error>> {
258
+ let port: u16 = args.first().ok_or("queue target PORT")?.parse()?;
259
+ let queue = Arc::new(Mutex::new(Queue::default()));
260
+ let sampled = Arc::clone(&queue);
261
+ std::thread::spawn(move || sample(&sampled));
262
+ let control = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port + 1))?;
263
+ emit("listening", json!({ "control": port + 1 }));
264
+ for stream in control.incoming() {
265
+ if let Err(error) = serve(stream?, &queue, port) {
266
+ emit("control-error", json!({ "error": error.to_string() }));
267
+ }
268
+ }
269
+ Ok(())
270
+ }
271
+
272
+ #[derive(Clone, Copy, PartialEq)]
273
+ enum Phase {
274
+ Dialing,
275
+ Open,
276
+ Ended,
277
+ }
278
+
279
+ struct Connection {
280
+ name: String,
281
+ fd: i32,
282
+ local: String,
283
+ phase: Phase,
284
+ }
285
+
286
+ fn local_of(fd: i32) -> std::io::Result<String> {
287
+ // SAFETY: sockaddr_in is plain old data
288
+ let mut address: libc::sockaddr_in = unsafe { std::mem::zeroed() };
289
+ let mut len = std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t;
290
+ // SAFETY: the kernel writes at most `len` bytes to the address on this stack
291
+ let status = unsafe { libc::getsockname(fd, (&mut address as *mut libc::sockaddr_in).cast(), &mut len) };
292
+ if status != 0 {
293
+ return Err(std::io::Error::last_os_error());
294
+ }
295
+ let ip = Ipv4Addr::from(u32::from_be(address.sin_addr.s_addr));
296
+ Ok(format!("{ip}:{}", u16::from_be(address.sin_port)))
297
+ }
298
+
299
+ /// Starts one connection and does not wait. The stamp comes before connect(2), which sends the SYN.
300
+ fn dial(name: &str, ip: Ipv4Addr, port: u16) -> Result<Connection, Box<dyn Error>> {
301
+ // SAFETY: socket(2) takes no pointer
302
+ let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_NONBLOCK, 0) };
303
+ if fd < 0 {
304
+ return Err(std::io::Error::last_os_error().into());
305
+ }
306
+ // SAFETY: sockaddr_in is plain old data
307
+ let mut address: libc::sockaddr_in = unsafe { std::mem::zeroed() };
308
+ address.sin_family = libc::AF_INET as libc::sa_family_t;
309
+ address.sin_port = port.to_be();
310
+ address.sin_addr.s_addr = u32::from_ne_bytes(ip.octets());
311
+ let t = monotonic_ns();
312
+ // SAFETY: the address is on this stack, passed with its exact size
313
+ let status = unsafe {
314
+ libc::connect(
315
+ fd,
316
+ (&address as *const libc::sockaddr_in).cast(),
317
+ std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
318
+ )
319
+ };
320
+ if status != 0 {
321
+ let error = std::io::Error::last_os_error();
322
+ if error.raw_os_error() != Some(libc::EINPROGRESS) {
323
+ return Err(error.into());
324
+ }
325
+ }
326
+ let local = local_of(fd)?;
327
+ emit_at("dial", t, json!({ "name": name, "local": local }));
328
+ Ok(Connection { name: name.to_string(), fd, local, phase: Phase::Dialing })
329
+ }
330
+
331
+ /// Waits up to `ms` for a change of the given connections, and writes one line for each change.
332
+ fn watch(connections: &mut [Connection], ms: i32) -> std::io::Result<()> {
333
+ let mut wanted: Vec<libc::pollfd> = connections
334
+ .iter()
335
+ .map(|each| libc::pollfd {
336
+ // poll(2) skips a negative descriptor
337
+ fd: if each.phase == Phase::Ended { -1 } else { each.fd },
338
+ events: if each.phase == Phase::Dialing { libc::POLLOUT } else { libc::POLLIN },
339
+ revents: 0,
340
+ })
341
+ .collect();
342
+ // SAFETY: the vector holds its length of pollfds
343
+ let ready = unsafe { libc::poll(wanted.as_mut_ptr(), wanted.len() as libc::nfds_t, ms) };
344
+ if ready < 0 {
345
+ let error = std::io::Error::last_os_error();
346
+ return if error.kind() == ErrorKind::Interrupted { Ok(()) } else { Err(error) };
347
+ }
348
+ for (each, seen) in connections.iter_mut().zip(&wanted) {
349
+ if seen.revents == 0 {
350
+ continue;
351
+ }
352
+ let code = socket_error(each.fd)?;
353
+ if code != 0 {
354
+ emit("failed", json!({ "name": each.name, "errno": errno_name(code) }));
355
+ each.phase = Phase::Ended;
356
+ } else if each.phase == Phase::Dialing {
357
+ emit("connected", json!({ "name": each.name, "local": each.local }));
358
+ each.phase = Phase::Open;
359
+ } else {
360
+ emit("closed", json!({ "name": each.name }));
361
+ each.phase = Phase::Ended;
362
+ }
363
+ }
364
+ Ok(())
365
+ }
366
+
367
+ /// Sends one byte, stamped before the write.
368
+ fn send(connection: &Connection) {
369
+ let t = monotonic_ns();
370
+ // SAFETY: the buffer holds the one byte that the call reads
371
+ let written = unsafe { libc::write(connection.fd, b"x".as_ptr().cast(), 1) };
372
+ emit_at("sent", t, json!({ "name": connection.name, "written": written }));
373
+ }
374
+
375
+ /// the control connection of the client: one line for each command, and one line for each reply
376
+ struct Control {
377
+ reader: BufReader<TcpStream>,
378
+ writer: TcpStream,
379
+ }
380
+
381
+ impl Control {
382
+ fn ask(&mut self, command: &str) -> Result<Value, Box<dyn Error>> {
383
+ writeln!(self.writer, "{command}")?;
384
+ let mut reply = String::new();
385
+ self.reader.read_line(&mut reply)?;
386
+ Ok(serde_json::from_str(&reply)?)
387
+ }
388
+ }
389
+
390
+ #[derive(Default)]
391
+ struct Plan {
392
+ host: String,
393
+ port: u16,
394
+ backlog: i32,
395
+ fill: usize,
396
+ settle_ms: i32,
397
+ burst: usize,
398
+ blocked: bool,
399
+ late_ms: Option<u64>,
400
+ accept: i32,
401
+ accept_at_ms: Option<u64>,
402
+ hold_ms: u64,
403
+ drain: bool,
404
+ }
405
+
406
+ fn plan_of(args: &[String]) -> Result<Plan, Box<dyn Error>> {
407
+ let mut plan = Plan { settle_ms: 300, ..Plan::default() };
408
+ let mut places = Vec::new();
409
+ let mut rest = args.iter();
410
+ while let Some(arg) = rest.next() {
411
+ let mut value = || rest.next().ok_or_else(|| format!("{arg} needs a value"));
412
+ match arg.as_str() {
413
+ "--backlog" => plan.backlog = value()?.parse()?,
414
+ "--fill" => plan.fill = value()?.parse()?,
415
+ "--settle-ms" => plan.settle_ms = value()?.parse()?,
416
+ "--burst" => plan.burst = value()?.parse()?,
417
+ "--blocked" => plan.blocked = true,
418
+ "--late-ms" => plan.late_ms = Some(value()?.parse()?),
419
+ "--accept" => plan.accept = value()?.parse()?,
420
+ "--accept-at-ms" => plan.accept_at_ms = Some(value()?.parse()?),
421
+ "--hold-ms" => plan.hold_ms = value()?.parse()?,
422
+ "--drain" => plan.drain = true,
423
+ // the harness gives these flags to each client, and the plan does not read them
424
+ "--mode" | "--max-s" => {
425
+ value()?;
426
+ }
427
+ other if other.starts_with("--") => return Err(format!("no flag {other}").into()),
428
+ other => places.push(other.to_string()),
429
+ }
430
+ }
431
+ if let [host, port] = places.as_slice() {
432
+ plan.host = host.clone();
433
+ plan.port = port.parse()?;
434
+ return Ok(plan);
435
+ }
436
+ Err("queue client [flags] HOST PORT".into())
437
+ }
438
+
439
+ fn client(args: &[String]) -> Result<(), Box<dyn Error>> {
440
+ let plan = plan_of(args)?;
441
+ let ip: Ipv4Addr = plan.host.parse()?;
442
+ let stream = TcpStream::connect((ip, plan.port + 1))?;
443
+ stream.set_nodelay(true)?;
444
+ let mut control = Control { reader: BufReader::new(stream.try_clone()?), writer: stream };
445
+ emit("opened", control.ask(&format!("open {}", plan.backlog))?);
446
+ emit("ready", json!({}));
447
+
448
+ let mut connections = Vec::new();
449
+ for at in 0..plan.fill {
450
+ connections.push(dial(&format!("fill-{at}"), ip, plan.port)?);
451
+ let last = connections.len() - 1;
452
+ watch(&mut connections[last..], plan.settle_ms)?;
453
+ }
454
+
455
+ let first = connections.len();
456
+ let burst_at = monotonic_ns();
457
+ for name in BURST.iter().take(plan.burst) {
458
+ connections.push(dial(name, ip, plan.port)?);
459
+ }
460
+ while connections[first..].iter().any(|each| each.phase == Phase::Dialing)
461
+ && monotonic_ns() < burst_at + HANDSHAKE_NS
462
+ {
463
+ watch(&mut connections[first..], 1)?;
464
+ }
465
+ for each in connections[first..].iter().filter(|each| each.name != "quiet" && each.phase == Phase::Open) {
466
+ send(each);
467
+ }
468
+ if plan.blocked {
469
+ connections.push(dial("blocked", ip, plan.port)?);
470
+ }
471
+
472
+ let at_ms = |ms: u64| burst_at + ms * 1_000_000;
473
+ let end = at_ms(plan.hold_ms);
474
+ let mut accept_due = plan.accept_at_ms.map(at_ms);
475
+ let mut late_due = plan.late_ms.map(at_ms);
476
+ loop {
477
+ let now = monotonic_ns();
478
+ if accept_due.is_some_and(|due| now >= due) {
479
+ accept_due = None;
480
+ emit("accepted", control.ask(&format!("accept {}", plan.accept))?);
481
+ }
482
+ if late_due.is_some_and(|due| now >= due) {
483
+ late_due = None;
484
+ for each in connections.iter().filter(|each| each.name == "quiet" && each.phase == Phase::Open) {
485
+ send(each);
486
+ }
487
+ }
488
+ let ended = connections[first..]
489
+ .iter()
490
+ .filter(|each| each.name != "in")
491
+ .all(|each| each.phase == Phase::Ended);
492
+ if now >= end || ended {
493
+ break;
494
+ }
495
+ watch(&mut connections, 5)?;
496
+ }
497
+
498
+ for each in &connections {
499
+ if let Ok(info) = tcp_info(each.fd) {
500
+ emit("state", json!({ "name": each.name, "state": info.tcpi_state, "unacked": info.tcpi_unacked }));
501
+ }
502
+ }
503
+ if plan.drain {
504
+ emit("accepted", control.ask(&format!("accept {DRAIN}"))?);
505
+ }
506
+ emit("stats", control.ask("stats")?);
507
+ emit("done", json!({}));
508
+ Ok(())
509
+ }
510
+
511
+ fn main() -> Result<(), Box<dyn Error>> {
512
+ let args: Vec<String> = std::env::args().skip(1).collect();
513
+ match args.first().map(String::as_str) {
514
+ Some("target") => target(&args[1..]),
515
+ Some("client") => client(&args[1..]),
516
+ _ => Err("queue target PORT | queue client [flags] HOST PORT".into()),
517
+ }
518
+ }
519
+
520
+ #[cfg(test)]
521
+ mod tests {
522
+ use super::*;
523
+
524
+ #[test]
525
+ fn the_counters_come_from_the_tcpext_lines_of_netstat() {
526
+ let text = "TcpExt: SyncookiesSent ListenOverflows ListenDrops TCPTimeouts EmbryonicRsts TCPReqQFullDoCookies\n\
527
+ TcpExt: 1 2 3 4 5 6\n\
528
+ IpExt: InNoRoutes\n\
529
+ IpExt: 7\n";
530
+ let found = counters_of(text);
531
+ assert_eq!(found.overflows, 2);
532
+ assert_eq!(found.drops, 3);
533
+ assert_eq!(found.timeouts, 4);
534
+ assert_eq!(found.resets, 5);
535
+ assert_eq!(found.cookies, 6);
536
+ }
537
+
538
+ #[test]
539
+ fn a_plan_reads_its_flags_and_skips_the_flags_of_the_harness() {
540
+ let args: Vec<String> = ["--backlog", "4", "--burst", "3", "--blocked", "10.0.0.2", "6390", "--mode", "idle", "--max-s", "90"]
541
+ .iter()
542
+ .map(|each| each.to_string())
543
+ .collect();
544
+ let plan = plan_of(&args).expect("a plan");
545
+ assert_eq!(plan.host, "10.0.0.2");
546
+ assert_eq!(plan.port, 6390);
547
+ assert_eq!(plan.backlog, 4);
548
+ assert_eq!(plan.burst, 3);
549
+ assert!(plan.blocked);
550
+ assert_eq!(plan.settle_ms, 300);
551
+ assert!(plan_of(&["--no-such".to_string()]).is_err());
552
+ }
553
+ }
@@ -1,4 +1,5 @@
1
1
  // client-sentinel --sentinels H:P,H:P,... --name NAME [--interval-ms N] [--max-s N] [--options JSON]
2
+ // [--op set|rpush] [--window N]
2
3
  //
3
4
  // A group cell's client: ioredis 6.0.0 in Sentinel mode, which asks the Sentinels for the primary
4
5
  // of NAME and connects to it. Every event is a JSON line stamped on CLOCK_MONOTONIC
@@ -12,6 +13,10 @@
12
13
  // run tell which writes it kept. --options are ioredis options over its defaults: none at the
13
14
  // defaults of a cell. The client never stops on an error: ioredis reconnects, and the stream
14
15
  // goes on until --max-s or SIGTERM.
16
+ //
17
+ // With --op rpush, each write is RPUSH w <n>: a key hides a second copy of a write, and a list
18
+ // keeps it. --window N runs N writers with one counter, thus N writes can be in flight. The
19
+ // defaults are SET and one writer.
15
20
 
16
21
  import { readFileSync } from "node:fs";
17
22
  import Redis from "ioredis";
@@ -40,6 +45,8 @@ if (sentinels.length === 0 || name === undefined) {
40
45
  const interval = Number(flag("--interval-ms") ?? 50);
41
46
  const max = Number(flag("--max-s") ?? 120) * 1000;
42
47
  const options = JSON.parse(flag("--options") ?? "{}");
48
+ const op = flag("--op") ?? "set";
49
+ const writers = Number(flag("--window") ?? 1);
43
50
 
44
51
  emit("start", {
45
52
  boot_id: readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim(),
@@ -91,16 +98,25 @@ try {
91
98
  process.exit(1);
92
99
  }
93
100
 
94
- for (let n = 1; !stopped; n += 1) {
95
- const at = now();
96
- // stamped before the write: the kernel can answer before the call returns
97
- const reply = redis.set(`w:${n}`, String(n));
98
- emit("sent", { n }, at);
99
- try {
100
- const answer = await reply;
101
- emit("ack", { n, at: remote(), reply: answer });
102
- } catch (error) {
103
- emit("fail", { n, name: error.name, message: error.message });
101
+ const write = (n) => (op === "rpush" ? redis.rpush("w", String(n)) : redis.set(`w:${n}`, String(n)));
102
+
103
+ let next = 1;
104
+ const writer = async () => {
105
+ while (!stopped) {
106
+ const n = next;
107
+ next += 1;
108
+ const at = now();
109
+ // stamped before the write: the kernel can answer before the call returns
110
+ const reply = write(n);
111
+ emit("sent", { n }, at);
112
+ try {
113
+ const answer = await reply;
114
+ emit("ack", { n, at: remote(), reply: answer });
115
+ } catch (error) {
116
+ emit("fail", { n, name: error.name, message: error.message });
117
+ }
118
+ await sleep(interval);
104
119
  }
105
- await sleep(interval);
106
- }
120
+ };
121
+
122
+ await Promise.all(Array.from({ length: writers }, writer));