pub struct ClientTable<K: Eq + Hash + Ord + Clone> {
pub stats: ServerStats,
/* private fields */
}Expand description
Bounded most-recently-used client table.
The bound is the point: an unbounded map is a memory-exhaustion lever for anyone willing to spoof source addresses. When full, the least recently seen client is evicted — losing its interleaved state, which costs it one exchange, not correctness.
Eviction is indexed, not scanned. The obvious implementation — walk the
map for the oldest last_seen — is O(capacity) per admission, and a public
server facing more clients than the table holds evicts on nearly every
packet. TIMECORP S12b (100k clients into a 16k table) went from “runs” to
“does not finish” on exactly that, which is what the scenario is for. The
order index makes it O(log n) and deterministic, where a HashMap scan
depends on iteration order that differs between instances.
Fields§
§stats: ServerStatsImplementations§
Source§impl<K: Eq + Hash + Ord + Clone> ClientTable<K>
impl<K: Eq + Hash + Ord + Clone> ClientTable<K>
pub fn new(capacity: usize, config: RateLimitConfig) -> Self
Sourcepub fn bytes_per_client() -> usize
pub fn bytes_per_client() -> usize
The real per-client footprint: the record, the slot links that order it, and the index entry that finds it.
Reported by the type rather than estimated by the caller, because the
corpus quotes this number and an estimate drifts silently when the
structure changes. It did: the figure used to be size_of::<ClientRecord>()
alone, which stopped being the whole story the moment records moved
into slots.
pub fn len(&self) -> usize
pub fn is_empty(&self) -> bool
pub fn get(&self, key: &K) -> Option<&ClientRecord>
Sourcepub fn most_recent(&self, limit: usize) -> Vec<(K, ClientRecord)>
pub fn most_recent(&self, limit: usize) -> Vec<(K, ClientRecord)>
The MRU report: most recently seen first, at most limit entries.
Walks the recency list, which is already in this order — admit
moves a client to the front and sets last_seen in the same breath, so
list order and descending last_seen are the same thing.
The previous form cloned every record in the table into a Vec, sorted
all of them, and threw away all but limit. At the daemon’s capacity
of 16384 that is ~1.5 MiB copied and an O(n log n) sort to answer a
ten-row status query. This is O(limit) and allocates once, for exactly
the rows returned.
Sourcepub fn admit(&mut self, key: &K, now: f64) -> Disposition
pub fn admit(&mut self, key: &K, now: f64) -> Disposition
Admit one request: refill the client’s bucket, decide its fate, and
record it. now is monotonic seconds.
Sourcepub fn admit_handle(&mut self, key: &K, now: f64) -> (Disposition, ClientHandle)
pub fn admit_handle(&mut self, key: &K, now: f64) -> (Disposition, ClientHandle)
admit, also returning the handle that addresses this client, so the
rest of the request never has to hash the key again.
Sourcepub fn response_mode(
&mut self,
key: &K,
request_origin: NtpTimestamp,
) -> ResponseMode
pub fn response_mode( &mut self, key: &K, request_origin: NtpTimestamp, ) -> ResponseMode
Decide basic vs interleaved for an admitted request.
The client signals interleaved mode by setting its origin timestamp to the receive timestamp we sent last time, rather than the transmit timestamp. That is unforgeable in the useful sense: only a client that actually saw our last response knows it.
Sourcepub fn response_mode_at(
&mut self,
handle: ClientHandle,
request_origin: NtpTimestamp,
) -> ResponseMode
pub fn response_mode_at( &mut self, handle: ClientHandle, request_origin: NtpTimestamp, ) -> ResponseMode
response_mode addressed by handle — no hashing.
Sourcepub fn note_response(
&mut self,
key: &K,
receive: NtpTimestamp,
receive_sent: NtpTimestamp,
)
pub fn note_response( &mut self, key: &K, receive: NtpTimestamp, receive_sent: NtpTimestamp, )
Record what we received and what we told the client, after answering.
Sourcepub fn note_response_at(
&mut self,
handle: ClientHandle,
receive: NtpTimestamp,
receive_sent: NtpTimestamp,
)
pub fn note_response_at( &mut self, handle: ClientHandle, receive: NtpTimestamp, receive_sent: NtpTimestamp, )
note_response addressed by handle — no hashing.
Sourcepub fn note_transmit(&mut self, key: &K, transmit: NtpTimestamp)
pub fn note_transmit(&mut self, key: &K, transmit: NtpTimestamp)
Record the true transmit timestamp of the response just sent. Called
after send, which is the whole point of interleaved mode — this is a
timestamp the basic exchange cannot report because the packet has not
left yet when its own transmit field is written.
Sourcepub fn note_transmit_at(&mut self, handle: ClientHandle, transmit: NtpTimestamp)
pub fn note_transmit_at(&mut self, handle: ClientHandle, transmit: NtpTimestamp)
note_transmit addressed by handle — no hashing.
This is the one called after send, so a handle taken before the write
is used after it. The generation check is what makes that safe: if the
client was evicted in between, the update is dropped rather than landing
on whoever inherited the slot.