@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/README.md +5 -0
- package/build/{cell-COlAmUZ-.d.ts → cell-DOoU1LRf.d.ts} +32 -3
- package/build/index.d.ts +14 -3
- package/build/index.js +67 -5
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/image/native/src/bin/inject.rs +123 -6
- package/package.json +1 -1
package/build/schema.d.ts
CHANGED
|
@@ -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
|
|
31
|
-
//! in the process namespace this sidecar shares with a member of a group. STOP and
|
|
32
|
-
//! a server and thaw it while its kernel stays up; KILL stops it, and its kernel
|
|
33
|
-
//! sockets. The stamps bracket the
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|