Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4563816873 | ||
|
|
70a2998f8c | ||
|
|
4ddefb2101 | ||
|
|
04291a0b7b | ||
|
|
c426884fd1 |
No files matched your search
@@ -106,10 +106,22 @@ pub struct ServerArgs {
|
|||||||
pub timestamp_allowance: Option<TimestampAllowanceArg>,
|
pub timestamp_allowance: Option<TimestampAllowanceArg>,
|
||||||
|
|
||||||
/// Refuse to provide requested DSCP marking.
|
/// Refuse to provide requested DSCP marking.
|
||||||
|
//
|
||||||
|
// `FalseyValueParser` rather than clap's default `bool` parser for a flag,
|
||||||
|
// which is what makes IRTT_SERVER_NO_DSCP usable from the environment at
|
||||||
|
// all. A flag's default parser accepts only the literals `true` and
|
||||||
|
// `false`, so the ordinary container/orchestrator spelling
|
||||||
|
// `IRTT_SERVER_NO_DSCP=1` did not enable the flag — it failed argument
|
||||||
|
// parsing and the server refused to start. Falsey semantics are clap's own
|
||||||
|
// documented recipe for a boolean environment flag: `0`, `false`, `no`,
|
||||||
|
// `n`, `off` and empty are off, anything else is on, and an absent
|
||||||
|
// variable is off as before. The command-line flag is unaffected: it still
|
||||||
|
// takes no value and still means "on".
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
env = "IRTT_SERVER_NO_DSCP",
|
env = "IRTT_SERVER_NO_DSCP",
|
||||||
long_help = "Refuse to provide requested traffic-class marking. Any requested DSCP is negotiated to zero, so the client is told its echo replies will be unmarked, and they are sent unmarked. The session is not refused."
|
value_parser = clap::builder::FalseyValueParser::new(),
|
||||||
|
long_help = "Refuse to provide requested traffic-class marking. Any requested DSCP is negotiated to zero, so the client is told its echo replies will be unmarked, and they are sent unmarked. The session is not refused.\n\nIRTT_SERVER_NO_DSCP sets the same thing from the environment: 0, false, no, n, off and an empty value leave marking enabled, and any other value disables it."
|
||||||
)]
|
)]
|
||||||
pub no_dscp: bool,
|
pub no_dscp: bool,
|
||||||
}
|
}
|
||||||
@@ -548,6 +560,43 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `IRTT_SERVER_NO_DSCP` is the one policy variable whose value is a
|
||||||
|
/// boolean rather than a number, a duration or a string, and the
|
||||||
|
/// container/orchestrator deployments this whole environment surface
|
||||||
|
/// exists for spell booleans `1`/`0`. clap's default parser for a flag
|
||||||
|
/// accepts only the literals `true`/`false` and turns everything else
|
||||||
|
/// into a *parse failure*, so `IRTT_SERVER_NO_DSCP=1` used to stop the
|
||||||
|
/// server from starting at all rather than enabling the flag. Falsey
|
||||||
|
/// semantics are what keeps that from being a startup failure.
|
||||||
|
#[test]
|
||||||
|
fn the_no_dscp_environment_variable_accepts_ordinary_boolean_spellings() {
|
||||||
|
for value in ["1", "true", "TRUE", "yes", "on", "enabled"] {
|
||||||
|
with_env(&[("IRTT_SERVER_NO_DSCP", value)], || {
|
||||||
|
let config = parse(&["--bind", "127.0.0.1:2112"])
|
||||||
|
.expect("a truthy value must not fail argument parsing")
|
||||||
|
.server_config();
|
||||||
|
assert!(!config.dscp_allowed(), "IRTT_SERVER_NO_DSCP={value}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for value in ["0", "false", "no", "off", ""] {
|
||||||
|
with_env(&[("IRTT_SERVER_NO_DSCP", value)], || {
|
||||||
|
let config = parse(&["--bind", "127.0.0.1:2112"])
|
||||||
|
.expect("a falsey value must not fail argument parsing")
|
||||||
|
.server_config();
|
||||||
|
assert!(config.dscp_allowed(), "IRTT_SERVER_NO_DSCP={value}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The command-line flag stays a flag: it takes no value, whatever the
|
||||||
|
/// environment parser accepts.
|
||||||
|
#[test]
|
||||||
|
fn the_no_dscp_flag_still_takes_no_command_line_value() {
|
||||||
|
assert!(parse(&["--bind", "127.0.0.1:2112", "--no-dscp=false"]).is_err());
|
||||||
|
assert!(!bound(&["--no-dscp"]).server_config().dscp_allowed());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_explicit_flag_overrides_the_environment() {
|
fn an_explicit_flag_overrides_the_environment() {
|
||||||
with_env(&[("IRTT_SERVER_MAX_SESSIONS", "512")], || {
|
with_env(&[("IRTT_SERVER_MAX_SESSIONS", "512")], || {
|
||||||
|
|||||||
@@ -1613,6 +1613,30 @@ impl ManagedClientTask {
|
|||||||
self.send_gate = self.send_gate.filter(|gate| *gate > now);
|
self.send_gate = self.send_gate.filter(|gate| *gate > now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The next stagger gate after a send was accepted at `accepted_at`.
|
||||||
|
///
|
||||||
|
/// The gate advances on its **own** cadence rather than being re-anchored
|
||||||
|
/// on every observed accept time. Anchoring on `accepted_at` folds each
|
||||||
|
/// send's wakeup latency into the next slot and then into every slot after
|
||||||
|
/// it, so the gate — and with it the whole group's send cadence — drifts
|
||||||
|
/// later without bound while the per-target schedules stay on their
|
||||||
|
/// absolute grid. The drift is not hypothetical: it shows up as a
|
||||||
|
/// monotonically growing `EchoSent::timer_error` and, once it exceeds one
|
||||||
|
/// interval, as probe slots the schedule then skips.
|
||||||
|
///
|
||||||
|
/// `accepted_at` is still the anchor in the two cases where continuing the
|
||||||
|
/// old cadence would be wrong: there is no previous gate, or the pacer has
|
||||||
|
/// fallen a full slot behind it. Re-anchoring there is what keeps a gate
|
||||||
|
/// stranded in the past from releasing a burst of catch-up sends once the
|
||||||
|
/// group starts moving again.
|
||||||
|
fn next_stagger_gate(&self, spacing: Duration, accepted_at: Instant) -> Option<Instant> {
|
||||||
|
let base = match self.send_gate {
|
||||||
|
Some(gate) if gate <= accepted_at && accepted_at.duration_since(gate) < spacing => gate,
|
||||||
|
_ => accepted_at,
|
||||||
|
};
|
||||||
|
base.checked_add(spacing)
|
||||||
|
}
|
||||||
|
|
||||||
fn record_stagger_acceptance(
|
fn record_stagger_acceptance(
|
||||||
&mut self,
|
&mut self,
|
||||||
result: SendResult,
|
result: SendResult,
|
||||||
@@ -1626,7 +1650,7 @@ impl ManagedClientTask {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.last_stagger_send = Some(accepted_at);
|
self.last_stagger_send = Some(accepted_at);
|
||||||
self.send_gate = accepted_at.checked_add(spacing);
|
self.send_gate = self.next_stagger_gate(spacing, accepted_at);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if let Some(observations) = &self.stagger_observations {
|
if let Some(observations) = &self.stagger_observations {
|
||||||
observations.lock().unwrap().push((_active, spacing));
|
observations.lock().unwrap().push((_active, spacing));
|
||||||
|
|||||||
@@ -1895,6 +1895,78 @@ fn stagger_gate_tracks_active_membership() {
|
|||||||
second.finish();
|
second.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The stagger gate must not fold each send's observed wakeup latency into
|
||||||
|
/// every later slot.
|
||||||
|
///
|
||||||
|
/// The gate is the binding constraint on when the *group* may send next (see
|
||||||
|
/// `next_deadline`), while each target's own schedule stays on an absolute
|
||||||
|
/// grid anchored at its open. Re-anchoring the gate on every observed accept
|
||||||
|
/// time therefore made the whole group's cadence drift later without bound:
|
||||||
|
/// `EchoSent::timer_error` grew monotonically, and once the drift exceeded one
|
||||||
|
/// interval `skip_missed_probe_slots_at` started dropping probe slots
|
||||||
|
/// outright. Measured before this was fixed, a single-target 20 s run at a
|
||||||
|
/// 50 ms interval sent 381 probes instead of 400.
|
||||||
|
///
|
||||||
|
/// This asserts the invariant directly rather than through a real-time run,
|
||||||
|
/// because reproducing accumulation behaviorally needs seconds of wall clock
|
||||||
|
/// and a jitter assumption. Two properties, both durable:
|
||||||
|
///
|
||||||
|
/// - while the pacer keeps up, N accepted sends advance the gate by exactly
|
||||||
|
/// N spacings from the first anchor, whatever lateness each send is
|
||||||
|
/// observed with;
|
||||||
|
/// - a gate the pacer has fallen a full spacing behind re-anchors on the
|
||||||
|
/// accept time instead, so a stranded gate cannot release a burst of
|
||||||
|
/// catch-up sends.
|
||||||
|
#[test]
|
||||||
|
fn the_stagger_gate_advances_by_one_spacing_per_send_without_accumulating_lateness() {
|
||||||
|
// No socket is involved: the gate is driven directly and the task is never
|
||||||
|
// polled, so an unreachable address is all a valid target set needs here.
|
||||||
|
let (mut task, _) = ManagedClient::task(
|
||||||
|
config(ManagedPacing::Staggered),
|
||||||
|
vec![target("only", "127.0.0.1:9".parse().unwrap())],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spacing = Duration::from_millis(50);
|
||||||
|
let start = Instant::now();
|
||||||
|
let lateness = Duration::from_millis(3);
|
||||||
|
|
||||||
|
task.record_stagger_acceptance(
|
||||||
|
SendResult::Ready { accepted: true },
|
||||||
|
Some((1, spacing)),
|
||||||
|
start,
|
||||||
|
);
|
||||||
|
assert_eq!(task.send_gate, Some(start + spacing), "first send anchors");
|
||||||
|
|
||||||
|
// Every later send is observed `lateness` after the gate that released it.
|
||||||
|
// The gate must still advance one spacing per send from `start`, not
|
||||||
|
// `spacing + lateness`.
|
||||||
|
for slot in 1..=8_u32 {
|
||||||
|
let released_at = start + spacing * slot;
|
||||||
|
task.record_stagger_acceptance(
|
||||||
|
SendResult::Ready { accepted: true },
|
||||||
|
Some((1, spacing)),
|
||||||
|
released_at + lateness,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
task.send_gate,
|
||||||
|
Some(start + spacing * (slot + 1)),
|
||||||
|
"slot {slot} accumulated observed lateness into the gate"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A full spacing behind is no longer the same cadence, so the gate
|
||||||
|
// re-anchors rather than staying stranded in the past.
|
||||||
|
let stalled_gate = task.send_gate.expect("the gate is set");
|
||||||
|
let resumed_at = stalled_gate + spacing;
|
||||||
|
task.record_stagger_acceptance(
|
||||||
|
SendResult::Ready { accepted: true },
|
||||||
|
Some((1, spacing)),
|
||||||
|
resumed_at,
|
||||||
|
);
|
||||||
|
assert_eq!(task.send_gate, Some(resumed_at + spacing));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn timeout_discovery_inspects_at_most_one_budget_of_targets() {
|
fn timeout_discovery_inspects_at_most_one_budget_of_targets() {
|
||||||
let target_count = TIMEOUT_WORK_BUDGET + 1;
|
let target_count = TIMEOUT_WORK_BUDGET + 1;
|
||||||
|
|||||||
+31
-1
@@ -60,7 +60,8 @@ served.
|
|||||||
A port of `0` selects an unused port per listener, so two such binds get two
|
A port of `0` selects an unused port per listener, so two such binds get two
|
||||||
different ports.
|
different ports.
|
||||||
|
|
||||||
With no `--bind` at all, see [Default bind](#default-bind) above.
|
With no `--bind` at all, see [Description](#description) above for the
|
||||||
|
default wildcard pair and the address-family fallback it allows.
|
||||||
|
|
||||||
### `--hmac KEY`
|
### `--hmac KEY`
|
||||||
|
|
||||||
@@ -136,6 +137,35 @@ and they are sent unmarked. The session is not refused.
|
|||||||
Durations everywhere in this command accept an integer followed by a unit:
|
Durations everywhere in this command accept an integer followed by a unit:
|
||||||
`ms`, `s`, or `m`.
|
`ms`, `s`, or `m`.
|
||||||
|
|
||||||
|
## ENVIRONMENT
|
||||||
|
|
||||||
|
Every option above can also be supplied through an environment variable, for
|
||||||
|
deployments that configure by environment rather than by command line
|
||||||
|
(containers, orchestrators, unit files). A flag given on the command line
|
||||||
|
overrides the corresponding variable entirely; an unset variable leaves the
|
||||||
|
built-in default in place.
|
||||||
|
|
||||||
|
| Variable | Option |
|
||||||
|
| --- | --- |
|
||||||
|
| `IRTT_SERVER_BIND` | `--bind`, one address only |
|
||||||
|
| `IRTT_SERVER_HMAC` | `--hmac` |
|
||||||
|
| `IRTT_SERVER_MAX_SESSIONS` | `--max-sessions` |
|
||||||
|
| `IRTT_SERVER_MAX_PACKET_LENGTH` | `--max-packet-length` |
|
||||||
|
| `IRTT_SERVER_MIN_INTERVAL` | `--min-interval` |
|
||||||
|
| `IRTT_SERVER_BURST` | `--burst` |
|
||||||
|
| `IRTT_SERVER_IDLE_TIMEOUT` | `--idle-timeout` |
|
||||||
|
| `IRTT_SERVER_MAX_DURATION` | `--max-duration` |
|
||||||
|
| `IRTT_SERVER_TIMESTAMP_ALLOWANCE` | `--timestamp-allowance` |
|
||||||
|
| `IRTT_SERVER_NO_DSCP` | `--no-dscp` |
|
||||||
|
|
||||||
|
`IRTT_SERVER_BIND` takes exactly one address, the same as a single `--bind`;
|
||||||
|
there is no way to request several listeners from the environment. Serving
|
||||||
|
more than one address needs repeated `--bind` options.
|
||||||
|
|
||||||
|
`IRTT_SERVER_NO_DSCP` is the only boolean. `0`, `false`, `no`, `n`, `off` and
|
||||||
|
an empty value leave traffic-class marking enabled; any other value disables
|
||||||
|
it, and an unset variable leaves it enabled.
|
||||||
|
|
||||||
## OUTPUT
|
## OUTPUT
|
||||||
|
|
||||||
Each bound endpoint is printed on startup, once every listener is up, which
|
Each bound endpoint is printed on startup, once every listener is up, which
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ presentation bounds on top of the client's own statistics retention (see
|
|||||||
total memory is dominated by whichever of the two is larger for your target
|
total memory is dominated by whichever of the two is larger for your target
|
||||||
count.
|
count.
|
||||||
|
|
||||||
|
A **finite** `--duration` TUI run retains exact statistics samples exactly as
|
||||||
|
`irtt-client(1)` does in finite mode, so its retained state grows with the
|
||||||
|
probe count rather than approaching a bound. Unlike `irtt-client`, the TUI
|
||||||
|
prints **no** memory warning when that estimate gets large: it owns the
|
||||||
|
alternate screen, and a stderr warning there would be invisible or would
|
||||||
|
corrupt the display. Size a long finite TUI run from the figures in
|
||||||
|
`irtt-client(1)` MEASUREMENTS AND MEMORY, or use the continuous default.
|
||||||
|
|
||||||
## EXIT BEHAVIOR
|
## EXIT BEHAVIOR
|
||||||
|
|
||||||
Quitting with `q` or `Ctrl-C` is an interrupted, successful exit. Otherwise,
|
Quitting with `q` or `Ctrl-C` is an interrupted, successful exit. Otherwise,
|
||||||
|
|||||||
Reference in new issue
Block a user