#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum TlsFlushPhase {
BeforeFlush,
AfterFlush,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CloseAction {
CloseSession,
Flush,
ReArmAndContinue,
Disconnect,
}
pub(super) fn goaway_close_action(
phase: TlsFlushPhase,
peer_gone: bool,
tls_wants_write: bool,
) -> CloseAction {
match phase {
TlsFlushPhase::BeforeFlush => {
if peer_gone {
CloseAction::CloseSession
} else if tls_wants_write {
CloseAction::Flush
} else {
CloseAction::Disconnect
}
}
TlsFlushPhase::AfterFlush => {
if tls_wants_write {
CloseAction::ReArmAndContinue
} else {
CloseAction::Disconnect
}
}
}
}
pub(super) fn error_close_action(tls_wants_write: bool) -> CloseAction {
if tls_wants_write {
CloseAction::ReArmAndContinue
} else {
CloseAction::CloseSession
}
}
pub(super) fn force_disconnect_action(peer_gone: bool, tls_wants_write: bool) -> CloseAction {
if peer_gone {
CloseAction::CloseSession
} else if tls_wants_write {
CloseAction::ReArmAndContinue
} else {
CloseAction::CloseSession
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum FinalizeAction {
Flush,
SkipFlush,
Parked,
RetainPendingBack,
ArmControlQueue,
Quiesce,
ReArm,
Settled,
}
pub(super) fn finalize_action<P>(
phase: TlsFlushPhase,
tls_wants_write: bool,
socket_write: bool,
expect_write_parked: bool,
made_progress: bool,
any_pending_back: P,
control_pending: bool,
) -> FinalizeAction
where
P: FnOnce() -> bool,
{
match phase {
TlsFlushPhase::BeforeFlush => {
if tls_wants_write {
if socket_write {
FinalizeAction::SkipFlush
} else {
FinalizeAction::Flush
}
} else if expect_write_parked {
FinalizeAction::Parked
} else if made_progress && any_pending_back() {
FinalizeAction::RetainPendingBack
} else if control_pending {
FinalizeAction::ArmControlQueue
} else {
FinalizeAction::Quiesce
}
}
TlsFlushPhase::AfterFlush => {
if tls_wants_write {
FinalizeAction::ReArm
} else {
FinalizeAction::Settled
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn goaway_before_flush_is_exhaustive() {
use TlsFlushPhase::BeforeFlush;
assert_eq!(
goaway_close_action(BeforeFlush, true, true),
CloseAction::CloseSession
);
assert_eq!(
goaway_close_action(BeforeFlush, true, false),
CloseAction::CloseSession
);
assert_eq!(
goaway_close_action(BeforeFlush, false, true),
CloseAction::Flush
);
assert_eq!(
goaway_close_action(BeforeFlush, false, false),
CloseAction::Disconnect
);
}
#[test]
fn goaway_after_flush_is_exhaustive_and_ignores_peer_gone() {
use TlsFlushPhase::AfterFlush;
for peer_gone in [true, false] {
assert_eq!(
goaway_close_action(AfterFlush, peer_gone, true),
CloseAction::ReArmAndContinue,
"records survived the flush: keep the session whatever the \
pre-flush peer answer was"
);
assert_eq!(
goaway_close_action(AfterFlush, peer_gone, false),
CloseAction::Disconnect,
"the kernel took everything: it is safe to disconnect"
);
}
}
#[test]
fn the_two_phases_are_not_interchangeable() {
assert_ne!(
goaway_close_action(TlsFlushPhase::BeforeFlush, false, true),
goaway_close_action(TlsFlushPhase::AfterFlush, false, true),
"a live peer with records pending must FLUSH before the flush and \
RE-ARM after it"
);
assert_eq!(
goaway_close_action(TlsFlushPhase::BeforeFlush, false, true),
CloseAction::Flush
);
assert_eq!(
goaway_close_action(TlsFlushPhase::AfterFlush, false, true),
CloseAction::ReArmAndContinue
);
}
#[test]
fn flush_is_never_returned_after_a_flush() {
for peer_gone in [true, false] {
for tls_wants_write in [true, false] {
assert_ne!(
goaway_close_action(TlsFlushPhase::AfterFlush, peer_gone, tls_wants_write),
CloseAction::Flush,
"AfterFlush must never ask for another flush"
);
}
}
}
#[test]
fn error_arm_is_exhaustive() {
assert_eq!(error_close_action(true), CloseAction::ReArmAndContinue);
assert_eq!(error_close_action(false), CloseAction::CloseSession);
}
#[test]
fn the_error_arm_closes_where_goaway_disconnects() {
assert_eq!(error_close_action(false), CloseAction::CloseSession);
assert_eq!(
goaway_close_action(TlsFlushPhase::AfterFlush, false, false),
CloseAction::Disconnect
);
}
#[test]
fn force_disconnect_arm_is_exhaustive() {
assert_eq!(
force_disconnect_action(true, true),
CloseAction::CloseSession,
"a departed peer closes even with records pending"
);
assert_eq!(
force_disconnect_action(true, false),
CloseAction::CloseSession
);
assert_eq!(
force_disconnect_action(false, true),
CloseAction::ReArmAndContinue,
"records pending: keep WRITABLE rather than sending FIN and \
truncating them"
);
assert_eq!(
force_disconnect_action(false, false),
CloseAction::CloseSession
);
}
#[test]
fn no_site_closes_on_a_live_peer_with_records_pending() {
let live_peer_with_records = [
goaway_close_action(TlsFlushPhase::AfterFlush, false, true),
error_close_action(true),
force_disconnect_action(false, true),
];
for action in live_peer_with_records {
assert!(
matches!(action, CloseAction::ReArmAndContinue),
"closing on a live peer with records pending is the truncation \
bug: got {action:?}"
);
}
assert_eq!(
goaway_close_action(TlsFlushPhase::BeforeFlush, false, true),
CloseAction::Flush
);
}
fn act(phase: TlsFlushPhase, inputs: [bool; 6]) -> FinalizeAction {
let [
tls_wants_write,
socket_write,
expect_write_parked,
made_progress,
any_pending_back,
control_pending,
] = inputs;
finalize_action(
phase,
tls_wants_write,
socket_write,
expect_write_parked,
made_progress,
|| any_pending_back,
control_pending,
)
}
#[test]
fn the_pre_flush_answer_depends_on_whether_the_pass_already_wrote() {
let tls_wants_write = true;
let expect_write_parked = false;
let made_progress = false;
let control_pending = false;
assert_eq!(
finalize_action(
TlsFlushPhase::BeforeFlush,
tls_wants_write,
false, expect_write_parked,
made_progress,
|| false,
control_pending,
),
FinalizeAction::Flush,
"records pending and no write this pass: the empty-buffer flush is \
this site's only attempt"
);
assert_eq!(
finalize_action(
TlsFlushPhase::BeforeFlush,
tls_wants_write,
true, expect_write_parked,
made_progress,
|| false,
control_pending,
),
FinalizeAction::SkipFlush,
"a pass that already wrote must not spend a second empty-buffer \
flush"
);
}
#[test]
fn tls_backpressure_dominates_every_readiness_input() {
for socket_write in [false, true] {
let expected = if socket_write {
FinalizeAction::SkipFlush
} else {
FinalizeAction::Flush
};
for expect_write_parked in [false, true] {
for made_progress in [false, true] {
for any_pending_back in [false, true] {
for control_pending in [false, true] {
let inputs = [
true,
socket_write,
expect_write_parked,
made_progress,
any_pending_back,
control_pending,
];
assert_eq!(
act(TlsFlushPhase::BeforeFlush, inputs),
expected,
"pending TLS records must answer from the flush \
alone, not from the readiness policy: {inputs:?}"
);
}
}
}
}
}
}
#[test]
fn a_parked_expect_write_suppresses_the_readiness_policy() {
for made_progress in [false, true] {
for any_pending_back in [false, true] {
for control_pending in [false, true] {
let inputs = [
false,
false,
true,
made_progress,
any_pending_back,
control_pending,
];
assert_eq!(
act(TlsFlushPhase::BeforeFlush, inputs),
FinalizeAction::Parked,
"a parked expect_write owns the next tick: {inputs:?}"
);
}
}
}
}
#[test]
fn invariant_16_retains_writable_only_with_progress_and_pending_back() {
let rows: [([bool; 3], FinalizeAction, &str); 8] = [
(
[true, true, false],
FinalizeAction::RetainPendingBack,
"progress and queued bytes: WRITABLE must survive",
),
(
[true, true, true],
FinalizeAction::RetainPendingBack,
"invariant 16 outranks the control queue: both want WRITABLE \
kept and only one of them needs to re-arm the event",
),
(
[true, false, true],
FinalizeAction::ArmControlQueue,
"progress but nothing queued on a stream: the deferred control \
frame is what still needs the next tick",
),
(
[true, false, false],
FinalizeAction::Quiesce,
"progress, nothing left anywhere: withdraw WRITABLE",
),
(
[false, true, false],
FinalizeAction::Quiesce,
"no progress: a starved pass must relinquish WRITABLE rather \
than busy-spin",
),
(
[false, true, true],
FinalizeAction::ArmControlQueue,
"no progress, but a queued control frame still needs a tick",
),
(
[false, false, true],
FinalizeAction::ArmControlQueue,
"nothing on the streams, a frame in the control queue",
),
(
[false, false, false],
FinalizeAction::Quiesce,
"nothing owed anywhere: withdraw WRITABLE",
),
];
for ([made_progress, any_pending_back, control_pending], expected, why) in rows {
let inputs = [
false,
false,
false,
made_progress,
any_pending_back,
control_pending,
];
assert_eq!(
act(TlsFlushPhase::BeforeFlush, inputs),
expected,
"{why}, got {:?}",
act(TlsFlushPhase::BeforeFlush, inputs)
);
}
}
#[test]
fn the_invariant_16_probe_is_not_walked_while_rustls_holds_records() {
let walked = std::cell::Cell::new(false);
let action = finalize_action(
TlsFlushPhase::BeforeFlush,
true, false, false, true, || {
walked.set(true);
true
},
false,
);
assert_eq!(
action,
FinalizeAction::Flush,
"premise: this is the TLS arm"
);
assert!(
!walked.get(),
"the invariant 16 probe must not be walked while rustls still \
holds records"
);
}
#[test]
fn the_invariant_16_probe_is_not_walked_without_progress() {
let walked = std::cell::Cell::new(false);
let action = finalize_action(
TlsFlushPhase::BeforeFlush,
false, false, false, false, || {
walked.set(true);
true
},
false,
);
assert_eq!(
action,
FinalizeAction::Quiesce,
"premise: a zero-progress pass with nothing queued quiesces"
);
assert!(
!walked.get(),
"a zero-progress pass cannot retain WRITABLE, so the probe must \
not be walked"
);
}
#[test]
fn after_flush_answers_from_the_records_and_nothing_else() {
for tls_wants_write in [false, true] {
let expected = if tls_wants_write {
FinalizeAction::ReArm
} else {
FinalizeAction::Settled
};
for socket_write in [false, true] {
for expect_write_parked in [false, true] {
for made_progress in [false, true] {
for any_pending_back in [false, true] {
for control_pending in [false, true] {
let inputs = [
tls_wants_write,
socket_write,
expect_write_parked,
made_progress,
any_pending_back,
control_pending,
];
assert_eq!(
act(TlsFlushPhase::AfterFlush, inputs),
expected,
"the post-flush query reads the records \
only: {inputs:?}"
);
}
}
}
}
}
}
}
#[test]
fn the_flush_answers_are_never_returned_after_a_flush() {
for tls_wants_write in [false, true] {
for socket_write in [false, true] {
for expect_write_parked in [false, true] {
let inputs = [
tls_wants_write,
socket_write,
expect_write_parked,
true,
true,
true,
];
let action = act(TlsFlushPhase::AfterFlush, inputs);
assert!(
!matches!(action, FinalizeAction::Flush | FinalizeAction::SkipFlush),
"AfterFlush must never ask for another flush: \
{inputs:?} gave {action:?}"
);
}
}
}
}
#[test]
fn the_post_flush_answers_are_never_returned_before_a_flush() {
for tls_wants_write in [false, true] {
for socket_write in [false, true] {
for expect_write_parked in [false, true] {
for made_progress in [false, true] {
for any_pending_back in [false, true] {
for control_pending in [false, true] {
let inputs = [
tls_wants_write,
socket_write,
expect_write_parked,
made_progress,
any_pending_back,
control_pending,
];
let action = act(TlsFlushPhase::BeforeFlush, inputs);
assert!(
!matches!(
action,
FinalizeAction::ReArm | FinalizeAction::Settled
),
"BeforeFlush must never return a post-flush \
answer: {inputs:?} gave {action:?}"
);
}
}
}
}
}
}
}
#[test]
fn the_two_finalize_phases_are_not_interchangeable() {
let inputs = [true, false, false, false, false, false];
assert_ne!(
act(TlsFlushPhase::BeforeFlush, inputs),
act(TlsFlushPhase::AfterFlush, inputs),
"pending records must FLUSH before the flush and RE-ARM after it"
);
assert_eq!(
act(TlsFlushPhase::BeforeFlush, inputs),
FinalizeAction::Flush
);
assert_eq!(
act(TlsFlushPhase::AfterFlush, inputs),
FinalizeAction::ReArm
);
}
}