nylon-ring-host 0.2.1

Dynamic plugin host for the nylon-ring ABI
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
use crate::types::{
    FastPendingMap, FastStateMap, Pending, ResponsePayload, StreamFrame, UnaryPending,
    UnaryResultSlot,
};
use dashmap::mapref::entry::Entry as DashEntry;
use nylon_ring::{NrHostExt, NrStatus};
use rustc_hash::FxBuildHasher;
use std::cell::Cell;
use std::collections::HashMap;
use std::future::poll_fn;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::Poll;

/// Number of shards for the pending requests.
const SHARD_COUNT: usize = 64;
const SHARD_MASK: usize = SHARD_COUNT - 1;

/// Host context shared with the plugin.
pub(crate) struct HostContext {
    /// Sharded pending storage is allocated only when a tracked call is made.
    pending_shards: OnceLock<Box<[FastPendingMap]>>,

    pub(crate) state_per_sid: FastStateMap,
    state_shard_counts: [AtomicUsize; SHARD_COUNT],
    pub(crate) host_ext: NrHostExt,
    stream_capacity: usize,

    /// Leased response buffers whose call the host abandoned (timeout,
    /// cancel, receiver drop) while the plugin may still write into them.
    /// They are freed on a late commit or when this context drops — by
    /// which point the plugin's shutdown contract forbids further writes.
    /// Only cold paths touch this lock.
    orphaned_leases: std::sync::Mutex<Vec<Vec<u8>>>,
}

impl HostContext {
    pub(crate) fn new(host_ext: NrHostExt, stream_capacity: usize) -> Self {
        Self {
            pending_shards: OnceLock::new(),
            state_per_sid: FastStateMap::with_hasher(FxBuildHasher),
            state_shard_counts: std::array::from_fn(|_| AtomicUsize::new(0)),
            host_ext,
            stream_capacity,
            orphaned_leases: std::sync::Mutex::new(Vec::new()),
        }
    }

    /// Keep an abandoned lease alive instead of freeing memory the plugin
    /// may still be writing to.
    pub(crate) fn park_orphan_lease(&self, lease: Vec<u8>) {
        self.orphaned_leases.lock().unwrap().push(lease);
    }

    /// Free the orphaned lease identified by `token` (its buffer address),
    /// if present. Called on a late commit — the plugin's commit is its
    /// promise that it will not touch the buffer again.
    pub(crate) fn release_orphan_lease(&self, token: u64) -> bool {
        let mut orphans = self.orphaned_leases.lock().unwrap();
        if let Some(index) = orphans
            .iter()
            .position(|lease| lease.as_ptr() as u64 == token)
        {
            orphans.swap_remove(index);
            true
        } else {
            false
        }
    }

    #[cfg(test)]
    pub(crate) fn orphaned_lease_count(&self) -> usize {
        self.orphaned_leases.lock().unwrap().len()
    }

    fn pending_shards(&self) -> &[FastPendingMap] {
        self.pending_shards.get_or_init(|| {
            let mut shards = Vec::with_capacity(SHARD_COUNT);
            for _ in 0..SHARD_COUNT {
                shards.push(FastPendingMap::with_hasher(FxBuildHasher));
            }
            shards.into_boxed_slice()
        })
    }

    pub(crate) fn pending_count(&self) -> usize {
        self.pending_shards
            .get()
            .map_or(0, |shards| shards.iter().map(FastPendingMap::len).sum())
    }

    pub(crate) fn stream_capacity(&self) -> usize {
        self.stream_capacity
    }

    pub(crate) fn set_state(&self, sid: u64, key: String, value: Vec<u8>) {
        match self.state_per_sid.entry(sid) {
            DashEntry::Occupied(mut entry) => {
                entry.get_mut().insert(key, value);
            }
            DashEntry::Vacant(entry) => {
                let mut state = HashMap::new();
                state.insert(key, value);
                self.state_shard_counts[(sid as usize) & SHARD_MASK]
                    .fetch_add(1, Ordering::Release);
                entry.insert(state);
            }
        }
    }

    /// Remove state only when the SID's occupancy shard can contain entries.
    /// The common no-state call path avoids locking the DashMap entirely.
    pub(crate) fn remove_state(&self, sid: u64) {
        let count = &self.state_shard_counts[(sid as usize) & SHARD_MASK];
        if count.load(Ordering::Acquire) == 0 {
            return;
        }
        if self.state_per_sid.remove(&sid).is_some() {
            count.fetch_sub(1, Ordering::AcqRel);
        }
    }

    pub(crate) fn state_count(&self) -> usize {
        self.state_shard_counts
            .iter()
            .map(|count| count.load(Ordering::Acquire))
            .sum()
    }
}

#[inline(always)]
fn get_shard(ctx: &HostContext, sid: u64) -> &FastPendingMap {
    // Shard by the SID's thread-local allocation block (blocks are 2^20 wide)
    // instead of the low bits. Consecutive SIDs from one thread then stay in
    // one shard, so a thread's insert/complete/remove traffic does not migrate
    // the same map cache lines across every core.
    &ctx.pending_shards()[((sid >> 20) as usize) & SHARD_MASK]
}

/// Insert a pending request.
pub(crate) fn insert_pending(ctx: &HostContext, sid: u64, pending: Pending) {
    get_shard(ctx, sid).insert(sid, pending);
}

/// Remove and return a pending request.
pub(crate) fn remove_pending(ctx: &HostContext, sid: u64) -> Option<Pending> {
    get_shard(ctx, sid).remove(&sid).map(|(_, pending)| pending)
}

/// Wait for a unary result stored in the pending map.
///
/// The synchronous response case reaches `Ready` before this future is first
/// polled, so it neither allocates a channel nor clones a waker. For a delayed
/// response, the callback takes and wakes the latest registered waker after
/// releasing the shard lock.
pub(crate) async fn wait_for_unary(
    ctx: &HostContext,
    sid: u64,
) -> Option<(NrStatus, ResponsePayload)> {
    poll_fn(|cx| {
        let mut replacement = None;
        loop {
            match get_shard(ctx, sid).entry(sid) {
                DashEntry::Vacant(entry) => {
                    drop(entry);
                    drop(replacement);
                    return Poll::Ready(None);
                }
                DashEntry::Occupied(mut entry) => match entry.get_mut() {
                    Pending::Unary(UnaryPending::Ready(_, _)) => {
                        let Pending::Unary(UnaryPending::Ready(status, data)) = entry.remove()
                        else {
                            unreachable!();
                        };
                        drop(replacement);
                        return Poll::Ready(Some((status, data)));
                    }
                    Pending::Unary(UnaryPending::Waiting { waker, .. }) => {
                        if waker
                            .as_ref()
                            .is_some_and(|waker| waker.will_wake(cx.waker()))
                        {
                            drop(entry);
                            drop(replacement);
                            return Poll::Pending;
                        }
                        if let Some(replacement) = replacement.take() {
                            let previous = waker.replace(replacement);
                            drop(entry);
                            drop(previous);
                            return Poll::Pending;
                        }
                        drop(entry);
                        replacement = Some(cx.waker().clone());
                    }
                    Pending::Stream(_) => {
                        drop(entry);
                        drop(replacement);
                        return Poll::Ready(None);
                    }
                },
            }
        }
    })
    .await
}

/// Remove all host-owned state associated with a completed SID.
///
/// An outstanding lease in the removed entry is parked, not freed: the
/// plugin may still write into it (see `HostContext::orphaned_leases`).
pub(crate) fn cleanup_sid(ctx: &HostContext, sid: u64) {
    if let Some(Pending::Unary(UnaryPending::Waiting {
        lease: Some(lease), ..
    })) = remove_pending(ctx, sid)
    {
        ctx.park_orphan_lease(lease);
    }
    ctx.remove_state(sid);
}

/// Deliver a result while holding one shard entry lock for the whole state
/// transition. This prevents remove/reinsert and terminal-frame races.
///
/// A foreign (plugin-owned) payload is stored as-is for unary requests; for
/// streams it is copied into the frame and its release runs after the shard
/// lock is dropped (release is plugin code and must not run under a lock).
pub(crate) fn dispatch_pending(
    ctx: &HostContext,
    sid: u64,
    status: NrStatus,
    payload: ResponsePayload,
) -> NrStatus {
    // Holds a foreign payload consumed by the stream arm so its release
    // callback runs after the entry lock below is released.
    let mut deferred_release = None;
    let dispatch_status = match get_shard(ctx, sid).entry(sid) {
        DashEntry::Vacant(_) => {
            deferred_release = Some(payload);
            NrStatus::Invalid
        }
        DashEntry::Occupied(mut entry) => {
            if let Pending::Unary(pending) = entry.get_mut() {
                let (waker, stale_lease) = match pending {
                    UnaryPending::Waiting { waker, lease } => (waker.take(), lease.take()),
                    UnaryPending::Ready(_, _) => {
                        drop(entry);
                        drop(payload);
                        return NrStatus::Invalid;
                    }
                };
                *pending = UnaryPending::Ready(status, payload);
                drop(entry);
                // A response through this path supersedes any outstanding
                // lease; park it in case the plugin still holds the pointer.
                if let Some(lease) = stale_lease {
                    ctx.park_orphan_lease(lease);
                }
                ctx.remove_state(sid);
                if let Some(waker) = waker {
                    waker.wake();
                }
                return NrStatus::Ok;
            }

            let terminal = status.is_terminal();
            let frame = StreamFrame {
                status,
                data: match payload {
                    ResponsePayload::Owned(data) => data,
                    ResponsePayload::Foreign(foreign) => {
                        let data = foreign.as_slice().to_vec();
                        deferred_release = Some(ResponsePayload::Foreign(foreign));
                        data
                    }
                },
            };
            // try_send borrows the sender in place; the borrow ends before
            // entry.remove(), so no per-frame Sender clone is needed. A live
            // pending entry implies a live receiver (its Drop removes the
            // entry first), so there is no closed-channel case here.
            let send_result = match entry.get() {
                Pending::Stream(tx) => tx.try_send(frame),
                Pending::Unary(_) => unreachable!(),
            };
            match send_result {
                Ok(()) => {
                    if terminal {
                        entry.remove();
                        ctx.remove_state(sid);
                    }
                    NrStatus::Ok
                }
                Err(_rejected_frame) => NrStatus::Backpressure,
            }
        }
    };
    drop(deferred_release);
    dispatch_status
}

/// Lease a host-owned response buffer for a pending unary request.
///
/// The buffer is allocated before the shard lock is taken and stored inside
/// the pending entry, so the same cleanup that removes the entry also
/// reclaims the lease. Fails for unknown sids, streaming sids, completed
/// requests, and sids that already hold an outstanding lease.
pub(crate) fn acquire_pending_lease(
    ctx: &HostContext,
    sid: u64,
    capacity: u64,
) -> nylon_ring::NrBufferLease {
    let Ok(capacity) = usize::try_from(capacity) else {
        return nylon_ring::NrBufferLease::failed();
    };
    let mut buffer: Vec<u8> = Vec::with_capacity(capacity);
    let granted = nylon_ring::NrBufferLease {
        ptr: buffer.as_mut_ptr(),
        cap: buffer.capacity() as u64,
        token: buffer.as_ptr() as u64,
    };
    match get_shard(ctx, sid).entry(sid) {
        DashEntry::Occupied(mut entry) => match entry.get_mut() {
            Pending::Unary(UnaryPending::Waiting {
                lease: lease @ None,
                ..
            }) => {
                *lease = Some(buffer);
                granted
            }
            _ => nylon_ring::NrBufferLease::failed(),
        },
        DashEntry::Vacant(_) => nylon_ring::NrBufferLease::failed(),
    }
}

/// Commit a leased buffer as the response to a pending unary request.
///
/// On a bad token or oversized `initialized_len` the lease stays stored (and
/// valid for a corrected retry); a commit for a sid the host has already
/// abandoned frees the matching orphaned lease and reports `Invalid`.
pub(crate) fn commit_pending_lease(
    ctx: &HostContext,
    sid: u64,
    status: NrStatus,
    token: u64,
    initialized_len: u64,
) -> NrStatus {
    let waker = match get_shard(ctx, sid).entry(sid) {
        DashEntry::Vacant(entry) => {
            drop(entry);
            ctx.release_orphan_lease(token);
            return NrStatus::Invalid;
        }
        DashEntry::Occupied(mut entry) => {
            let Pending::Unary(pending) = entry.get_mut() else {
                return NrStatus::Invalid;
            };
            let UnaryPending::Waiting { waker, lease } = pending else {
                return NrStatus::Invalid;
            };
            let Some(mut buffer) = lease.take() else {
                return NrStatus::Invalid;
            };
            if buffer.as_ptr() as u64 != token || initialized_len > buffer.capacity() as u64 {
                *lease = Some(buffer);
                return NrStatus::Invalid;
            }
            // SAFETY: initialized_len is within the buffer's capacity and
            // the plugin's commit asserts it wrote that prefix (ABI
            // contract; u8 needs no validity beyond initialization).
            unsafe { buffer.set_len(initialized_len as usize) };
            let waker = waker.take();
            *pending = UnaryPending::Ready(status, ResponsePayload::Owned(buffer));
            waker
        }
    };
    ctx.remove_state(sid);
    if let Some(waker) = waker {
        waker.wake();
    }
    NrStatus::Ok
}

// --- Thread Local Optimization for Unary Results ---
thread_local! {
    pub(crate) static CURRENT_UNARY_RESULT: Cell<*mut UnaryResultSlot> = const { Cell::new(std::ptr::null_mut()) };
}

/// Thread-local target for stream frames sent synchronously inside the
/// plugin's `handle` call: frames land directly in the channel without the
/// per-frame pending-map lookup. Frames sent from other threads (or after
/// `handle` returns) still route through the map.
pub(crate) struct StreamFrameSlot {
    pub(crate) sid: u64,
    pub(crate) chan: std::sync::Arc<crate::stream_channel::StreamChannel>,
    /// Set when a terminal frame went through this slot; the caller removes
    /// the pending entry after `handle` returns (the map path removes it
    /// inline instead).
    pub(crate) terminal_seen: bool,
}

thread_local! {
    pub(crate) static CURRENT_STREAM_FRAME: Cell<*mut StreamFrameSlot> = const { Cell::new(std::ptr::null_mut()) };
}