@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.
- package/README.md +1 -0
- package/build/{cell-DZm8zXN4.d.ts → cell-biffSiEJ.d.ts} +178 -4
- package/build/{image-EZ8ioZ3L.js → image-BxVTFZ1o.js} +23 -2
- package/build/image-BxVTFZ1o.js.map +1 -0
- package/build/index.d.ts +293 -5
- package/build/index.js +895 -32
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +5 -2
- package/build/schema.js +7 -3
- package/build/schema.js.map +1 -1
- package/image/Dockerfile +3 -3
- package/image/kafka/AdminKafka.java +44 -9
- package/image/kafka/ConsumerKafka.java +359 -0
- package/image/kafka/Dockerfile +2 -2
- package/image/native/Cargo.toml +1 -1
- package/image/native/src/bin/client.rs +164 -28
- package/image/native/src/bin/ctfull.rs +633 -0
- package/image/native/src/bin/inject.rs +16 -0
- package/image/native/src/bin/load.rs +481 -0
- package/image/native/src/bin/ports.rs +1 -8
- package/image/native/src/bin/queue.rs +1 -12
- package/image/native/src/lib.rs +51 -0
- package/package.json +5 -2
- package/build/image-EZ8ioZ3L.js.map +0 -1
|
@@ -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::
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
160
|
-
return Err(format!("--mode is idle, inflight or
|
|
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(_)) => "
|
|
183
|
-
(
|
|
184
|
-
(
|
|
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
|
}
|