Skip to main content

ClientTable

Struct ClientTable 

Source
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: ServerStats

Implementations§

Source§

impl<K: Eq + Hash + Ord + Clone> ClientTable<K>

Source

pub fn new(capacity: usize, config: RateLimitConfig) -> Self

Source

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.

Source

pub fn len(&self) -> usize

Source

pub fn is_empty(&self) -> bool

Source

pub fn get(&self, key: &K) -> Option<&ClientRecord>

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn response_mode_at( &mut self, handle: ClientHandle, request_origin: NtpTimestamp, ) -> ResponseMode

response_mode addressed by handle — no hashing.

Source

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.

Source

pub fn note_response_at( &mut self, handle: ClientHandle, receive: NtpTimestamp, receive_sent: NtpTimestamp, )

note_response addressed by handle — no hashing.

Source

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.

Source

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.

Source

pub fn note_refused(&mut self)

Auto Trait Implementations§

§

impl<K> Freeze for ClientTable<K>
where HashMap<K, u32, ClientHashBuilder>: Freeze, Vec<Slot<K>>: Freeze,

§

impl<K> RefUnwindSafe for ClientTable<K>

§

impl<K> Send for ClientTable<K>
where HashMap<K, u32, ClientHashBuilder>: Send, Vec<Slot<K>>: Send,

§

impl<K> Sync for ClientTable<K>
where HashMap<K, u32, ClientHashBuilder>: Sync, Vec<Slot<K>>: Sync,

§

impl<K> Unpin for ClientTable<K>
where HashMap<K, u32, ClientHashBuilder>: Unpin, Vec<Slot<K>>: Unpin,

§

impl<K> UnsafeUnpin for ClientTable<K>

§

impl<K> UnwindSafe for ClientTable<K>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.