@mark1russell7/harness 0.11.13 → 0.11.15

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.
package/build/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { J as Subject, _ as ImageSource, i as ClientImage, r as Client } from "./cell-COlAmUZ-.js";
1
+ import { Y as Subject, i as ClientImage, r as Client, v as ImageSource } from "./cell-DOoU1LRf.js";
2
2
  import { z } from "zod";
3
3
  //#region src/schema.d.ts
4
4
  /**
@@ -27,13 +27,17 @@
27
27
  //! toxic of Toxiproxy. The stamps bracket the request and its answer, after the connect, so the
28
28
  //! commit of the toxic is known on the same clock as the kernel's records.
29
29
  //!
30
- //! - `signal NAME SIGNAL`: SIGNAL (KILL, STOP, CONT or TERM) to each process whose name is NAME,
31
- //! in the process namespace this sidecar shares with a member of a group. STOP and CONT freeze
32
- //! a server and thaw it while its kernel stays up; KILL stops it, and its kernel closes its
33
- //! sockets. The stamps bracket the kill(2) calls.
30
+ //! - `signal NAME SIGNAL`: SIGNAL (KILL, STOP, CONT, TERM or HUP) to each process whose name is
31
+ //! NAME, in the process namespace this sidecar shares with a member of a group or a hop. STOP and
32
+ //! CONT freeze a server and thaw it while its kernel stays up; KILL stops it, and its kernel
33
+ //! closes its sockets; HUP asks a pooler to read its configuration again. The stamps bracket the
34
+ //! kill(2) calls.
34
35
  //! - `resp ADDR:PORT ARG...`: one command to a Redis-protocol server, for example
35
36
  //! `SENTINEL FAILOVER mymaster` to a Sentinel. The stamps bracket the write of the command and
36
37
  //! the first line of its reply, so a planned failover starts at a known instant.
38
+ //! - `query ADDR:PORT USER DATABASE SQL`: one simple query to a server of the PostgreSQL protocol
39
+ //! that trusts the user, for example `PAUSE` on the console of PgBouncer. The stamps bracket the
40
+ //! write of the query and its ReadyForQuery, after the login, so the end of a PAUSE is known.
37
41
  //!
38
42
  //! A filter rule's PEER can be a list, `A,B,C`: the rules for each peer go in the same commit, so
39
43
  //! a part of a group loses all of its peers at one instant.
@@ -252,6 +256,7 @@ fn signal_number(name: &str) -> Result<libc::c_int, Box<dyn Error>> {
252
256
  "STOP" => libc::SIGSTOP,
253
257
  "CONT" => libc::SIGCONT,
254
258
  "TERM" => libc::SIGTERM,
259
+ "HUP" => libc::SIGHUP,
255
260
  other => return Err(format!("not a signal this injector sends: {other}").into()),
256
261
  })
257
262
  }
@@ -296,7 +301,7 @@ fn processes(name: &str) -> Result<Vec<libc::pid_t>, Box<dyn Error>> {
296
301
  /// `signal NAME SIGNAL [--at NS]`: the signal to each process of that name, bracketed by stamps
297
302
  fn signal(args: &[String]) -> Result<(), Box<dyn Error>> {
298
303
  let name = process_name(args.get(1).ok_or("signal NAME SIGNAL [--at NS]")?)?;
299
- let sig = signal_number(args.get(2).ok_or("and the signal: KILL, STOP, CONT or TERM")?)?;
304
+ let sig = signal_number(args.get(2).ok_or("and the signal: KILL, STOP, CONT, TERM or HUP")?)?;
300
305
  let opts = options(&args[3..])?;
301
306
  let pids = processes(name)?;
302
307
  if pids.is_empty() {
@@ -357,6 +362,98 @@ fn resp(args: &[String]) -> Result<(), Box<dyn Error>> {
357
362
  Ok(())
358
363
  }
359
364
 
365
+ /// a message of the PostgreSQL protocol from the client: its type, its length and its body
366
+ fn pg_message(kind: u8, body: &[u8]) -> Vec<u8> {
367
+ let mut out = vec![kind];
368
+ out.extend_from_slice(&((body.len() + 4) as u32).to_be_bytes());
369
+ out.extend_from_slice(body);
370
+ out
371
+ }
372
+
373
+ /// the startup message of the protocol 3.0 for a user and a database: it has no type byte
374
+ fn pg_startup(user: &str, database: &str) -> Vec<u8> {
375
+ let mut body = 196_608u32.to_be_bytes().to_vec();
376
+ for (key, value) in [("user", user), ("database", database)] {
377
+ body.extend_from_slice(key.as_bytes());
378
+ body.push(0);
379
+ body.extend_from_slice(value.as_bytes());
380
+ body.push(0);
381
+ }
382
+ body.push(0);
383
+ let mut out = ((body.len() + 4) as u32).to_be_bytes().to_vec();
384
+ out.extend_from_slice(&body);
385
+ out
386
+ }
387
+
388
+ /// one message of the server: its type and its body
389
+ fn pg_read(stream: &mut impl std::io::Read) -> Result<(u8, Vec<u8>), Box<dyn Error>> {
390
+ let mut head = [0u8; 5];
391
+ stream.read_exact(&mut head)?;
392
+ let length = u32::from_be_bytes([head[1], head[2], head[3], head[4]]) as usize;
393
+ if length < 4 {
394
+ return Err("a message of the server is shorter than its length field".into());
395
+ }
396
+ let mut body = vec![0u8; length - 4];
397
+ stream.read_exact(&mut body)?;
398
+ Ok((head[0], body))
399
+ }
400
+
401
+ /// the text of an ErrorResponse: its field M, or the whole body when it has none
402
+ fn pg_error(body: &[u8]) -> String {
403
+ body.split(|byte| *byte == 0)
404
+ .filter_map(|field| field.split_first())
405
+ .find(|(kind, _)| **kind == b'M')
406
+ .map(|(_, text)| String::from_utf8_lossy(text).to_string())
407
+ .unwrap_or_else(|| String::from_utf8_lossy(body).to_string())
408
+ }
409
+
410
+ /// `query ADDR:PORT USER DATABASE SQL [--at NS]`: one simple query, and its ReadyForQuery, bracketed
411
+ /// by stamps. The login must need no password: the console of PgBouncer with auth_type trust is one
412
+ fn query(args: &[String]) -> Result<(), Box<dyn Error>> {
413
+ use std::net::{SocketAddrV4, TcpStream};
414
+ let usage = "query ADDR:PORT USER DATABASE SQL [--at NS]";
415
+ let addr: SocketAddrV4 = args.get(1).ok_or(usage)?.parse()?;
416
+ let user = args.get(2).ok_or(usage)?;
417
+ let database = args.get(3).ok_or(usage)?;
418
+ let sql = args.get(4).ok_or(usage)?;
419
+ let opts = options(&args[5..])?;
420
+ let mut stream = TcpStream::connect(addr)?;
421
+ stream.write_all(&pg_startup(user, database))?;
422
+ // the login: AuthenticationOk, the parameters of the server, then ReadyForQuery
423
+ loop {
424
+ let (kind, body) = pg_read(&mut stream)?;
425
+ match kind {
426
+ b'R' if body.get(..4) == Some(&[0u8, 0, 0, 0][..]) => {}
427
+ b'R' => return Err("the server asks for a password: the query needs a user that it trusts".into()),
428
+ b'E' => return Err(format!("the login failed: {}", pg_error(&body)).into()),
429
+ b'Z' => break,
430
+ _ => {}
431
+ }
432
+ }
433
+ if let Some(at) = opts.at {
434
+ until(at)?;
435
+ }
436
+ let t0 = monotonic_ns();
437
+ let mut text = sql.as_bytes().to_vec();
438
+ text.push(0);
439
+ stream.write_all(&pg_message(b'Q', &text))?;
440
+ let mut tag = String::new();
441
+ loop {
442
+ let (kind, body) = pg_read(&mut stream)?;
443
+ match kind {
444
+ b'C' => tag = String::from_utf8_lossy(body.split(|byte| *byte == 0).next().unwrap_or(&[])).to_string(),
445
+ b'E' => return Err(format!("{sql} failed: {}", pg_error(&body)).into()),
446
+ b'Z' => break,
447
+ _ => {}
448
+ }
449
+ }
450
+ let t1 = monotonic_ns();
451
+ // Terminate: the server closes the session, and the sidecar does not wait for it
452
+ stream.write_all(&pg_message(b'X', &[]))?;
453
+ println!("{}", json!({ "action": "query", "query": sql, "tag": tag, "t0": t0, "t1": t1 }));
454
+ Ok(())
455
+ }
456
+
360
457
  /// the peers of a filter rule: one address, or a list `A,B,C`
361
458
  fn peers(text: &str) -> Result<Vec<Ipv4Addr>, Box<dyn Error>> {
362
459
  let list: Vec<Ipv4Addr> = text.split(',').map(str::parse).collect::<Result<_, _>>()?;
@@ -378,6 +475,9 @@ fn run() -> Result<(), Box<dyn Error>> {
378
475
  if action == "resp" {
379
476
  return resp(&args);
380
477
  }
478
+ if action == "query" {
479
+ return query(&args);
480
+ }
381
481
  match action {
382
482
  "netem" | "clear-netem" | "offload" => {
383
483
  let dev = device(args.get(1).ok_or("and a device")?)?;
@@ -522,7 +622,8 @@ mod tests {
522
622
  assert_eq!(signal_number("STOP").unwrap(), libc::SIGSTOP);
523
623
  assert_eq!(signal_number("CONT").unwrap(), libc::SIGCONT);
524
624
  assert_eq!(signal_number("KILL").unwrap(), libc::SIGKILL);
525
- assert!(signal_number("HUP").is_err());
625
+ assert_eq!(signal_number("HUP").unwrap(), libc::SIGHUP);
626
+ assert!(signal_number("USR1").is_err());
526
627
  assert!(process_name("valkey-server").is_ok());
527
628
  assert!(process_name("valkey-sentinel").is_ok());
528
629
  for name in ["", "a b", "x;y", "much-too-long-a-name"] {
@@ -543,6 +644,22 @@ mod tests {
543
644
  );
544
645
  }
545
646
 
647
+ #[test]
648
+ fn a_query_is_a_startup_message_and_a_simple_query() {
649
+ // the length counts itself, then the protocol 3.0, then each pair, then a final zero
650
+ let startup = pg_startup("postgres", "pgbouncer");
651
+ assert_eq!(&startup[..4], &(startup.len() as u32).to_be_bytes());
652
+ assert_eq!(&startup[4..8], &[0, 3, 0, 0]);
653
+ assert_eq!(&startup[8..], b"user\0postgres\0database\0pgbouncer\0\0");
654
+ assert_eq!(pg_message(b'Q', b"PAUSE\0"), b"Q\0\0\0\x0aPAUSE\0".to_vec());
655
+ assert_eq!(pg_message(b'X', &[]), b"X\0\0\0\x04".to_vec());
656
+ let mut reply: &[u8] = b"Z\0\0\0\x05I";
657
+ assert_eq!(pg_read(&mut reply).unwrap(), (b'Z', b"I".to_vec()));
658
+ let mut short: &[u8] = b"Z\0\0\0\x03";
659
+ assert!(pg_read(&mut short).is_err());
660
+ assert_eq!(pg_error(b"SERROR\0C08P01\0Mno such command\0\0"), "no such command");
661
+ }
662
+
546
663
  #[test]
547
664
  fn http_requests_take_only_a_method_a_path_and_a_length() {
548
665
  assert!(http_path("/proxies/redis/toxics").is_ok());
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.11.13",
4
+ "version": "0.11.15",
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",