1mod call_tracker;
9mod callbacks;
10mod context;
11mod error;
12mod extensions;
13#[cfg(test)]
14mod loom_tests;
15mod sid;
16mod types;
17
18use call_tracker::CallTracker;
19use callbacks::{get_state_callback, send_result_vec_callback, set_state_callback};
20use context::{CURRENT_UNARY_RESULT, HostContext};
21use libloading::{Library, Symbol};
22use nylon_ring::{
23 ABI_VERSION, NrBytes, NrHostExt, NrHostVTable, NrPluginInfo, NrPluginVTable, NrStr,
24};
25use sid::next_sid;
26use std::collections::HashMap;
27use std::ffi::c_void;
28use std::sync::Arc;
29use std::time::Duration;
30
31pub use error::NylonRingHostError;
32pub use extensions::Extensions;
33pub use nylon_ring::NrStatus;
34pub use types::{Result, StreamFrame, StreamReceiver};
35
36pub const DEFAULT_STREAM_CAPACITY: usize = 64;
38
39#[derive(Debug, Copy, Clone, PartialEq, Eq)]
41pub struct HostMetrics {
42 pub loaded_plugins: usize,
43 pub pending_requests: usize,
44 pub state_sessions: usize,
45 pub in_flight_calls: usize,
46}
47
48struct PendingGuard<'a> {
49 host_ctx: &'a HostContext,
50 sid: u64,
51 armed: bool,
52}
53
54impl<'a> PendingGuard<'a> {
55 fn new(host_ctx: &'a HostContext, sid: u64) -> Self {
56 Self {
57 host_ctx,
58 sid,
59 armed: true,
60 }
61 }
62
63 fn disarm(&mut self) {
64 self.armed = false;
65 }
66}
67
68impl Drop for PendingGuard<'_> {
69 fn drop(&mut self) {
70 if self.armed {
71 context::cleanup_sid(self.host_ctx, self.sid);
72 }
73 }
74}
75
76struct FastSlotBinding;
77
78impl FastSlotBinding {
79 fn bind(slot: &mut types::UnaryResultSlot) -> Result<Self> {
80 let bound = CURRENT_UNARY_RESULT.with(|cell| {
81 if !cell.get().is_null() {
82 return false;
83 }
84 cell.set(slot as *mut _);
85 true
86 });
87 if bound {
88 Ok(Self)
89 } else {
90 Err(NylonRingHostError::FastPathReentrant)
91 }
92 }
93}
94
95impl Drop for FastSlotBinding {
96 fn drop(&mut self) {
97 CURRENT_UNARY_RESULT.with(|cell| cell.set(std::ptr::null_mut()));
98 }
99}
100
101pub struct LoadedPlugin {
103 _lib: Library,
104 vtable: &'static NrPluginVTable,
105 host_ctx: Arc<HostContext>,
106 path: String,
107 call_tracker: CallTracker,
108}
109
110impl LoadedPlugin {
111 fn begin_call(&self) -> Result<BorrowedPluginCallGuard<'_>> {
112 let shard = self
113 .call_tracker
114 .try_begin()
115 .ok_or(NylonRingHostError::PluginUnloaded)?;
116 Ok(BorrowedPluginCallGuard {
117 tracker: &self.call_tracker,
118 shard,
119 })
120 }
121
122 fn begin_owned_call(self: &Arc<Self>) -> Result<PluginCallGuard> {
123 let shard = self
124 .call_tracker
125 .try_begin()
126 .ok_or(NylonRingHostError::PluginUnloaded)?;
127 Ok(PluginCallGuard {
128 plugin: self.clone(),
129 shard,
130 })
131 }
132
133 fn stop_accepting_calls(&self) {
134 self.call_tracker.stop();
135 }
136}
137
138struct BorrowedPluginCallGuard<'a> {
139 tracker: &'a CallTracker,
140 shard: usize,
141}
142
143impl Drop for BorrowedPluginCallGuard<'_> {
144 fn drop(&mut self) {
145 self.tracker.finish(self.shard);
146 }
147}
148
149pub(crate) struct PluginCallGuard {
150 plugin: Arc<LoadedPlugin>,
151 shard: usize,
152}
153
154impl Drop for PluginCallGuard {
155 fn drop(&mut self) {
156 self.plugin.call_tracker.finish(self.shard);
157 }
158}
159
160impl Drop for LoadedPlugin {
161 fn drop(&mut self) {
162 if let Some(shutdown_fn) = self.vtable.shutdown {
163 unsafe {
164 shutdown_fn();
165 }
166 }
167 }
168}
169
170#[derive(Clone)]
172pub struct PluginHandle {
173 plugin: Arc<LoadedPlugin>,
174}
175
176impl PluginHandle {
177 fn status_error(status: NrStatus) -> NylonRingHostError {
178 if status == NrStatus::Panic {
179 NylonRingHostError::PluginPanicked
180 } else {
181 NylonRingHostError::PluginHandleFailed(status)
182 }
183 }
184
185 pub async fn call_response(&self, entry: &str, payload: &[u8]) -> Result<(NrStatus, Vec<u8>)> {
187 let _call_guard = self.plugin.begin_call()?;
188 let sid = next_sid();
189
190 context::insert_pending(
191 &self.plugin.host_ctx,
192 sid,
193 types::Pending::Unary(types::UnaryPending::waiting()),
194 );
195 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
196
197 let payload_bytes = NrBytes::from_slice(payload);
198 let handle_raw_fn = match self.plugin.vtable.handle {
199 Some(f) => f,
200 None => return Err(NylonRingHostError::MissingRequiredFunctions),
201 };
202
203 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
204
205 if status != NrStatus::Ok {
206 return Err(Self::status_error(status));
207 }
208
209 let result = context::wait_for_unary(&self.plugin.host_ctx, sid)
210 .await
211 .ok_or(NylonRingHostError::PluginUnloaded);
212 if result.is_ok() {
213 pending_guard.disarm();
214 }
215 result
216 }
217
218 pub async fn call_response_timeout(
225 &self,
226 entry: &str,
227 payload: &[u8],
228 timeout: std::time::Duration,
229 ) -> Result<(NrStatus, Vec<u8>)> {
230 let _call_guard = self.plugin.begin_call()?;
231 let sid = next_sid();
232
233 context::insert_pending(
234 &self.plugin.host_ctx,
235 sid,
236 types::Pending::Unary(types::UnaryPending::waiting()),
237 );
238 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
239
240 let payload_bytes = NrBytes::from_slice(payload);
241 let handle_raw_fn = match self.plugin.vtable.handle {
242 Some(f) => f,
243 None => return Err(NylonRingHostError::MissingRequiredFunctions),
244 };
245
246 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
247 if status != NrStatus::Ok {
248 return Err(Self::status_error(status));
249 }
250
251 match tokio::time::timeout(timeout, context::wait_for_unary(&self.plugin.host_ctx, sid))
252 .await
253 {
254 Ok(Some(v)) => {
255 pending_guard.disarm();
256 Ok(v)
257 }
258 Ok(None) => Err(NylonRingHostError::PluginUnloaded),
259 Err(_) => Err(NylonRingHostError::Timeout),
260 }
261 }
262
263 pub async fn call_response_fast(
265 &self,
266 entry: &str,
267 payload: &[u8],
268 ) -> Result<(NrStatus, Vec<u8>)> {
269 let _call_guard = self.plugin.begin_call()?;
270 let sid = next_sid();
271 let mut slot = types::UnaryResultSlot { sid, result: None };
272
273 let binding = FastSlotBinding::bind(&mut slot)?;
274
275 let payload_bytes = NrBytes::from_slice(payload);
276
277 let handle_raw_fn = match self.plugin.vtable.handle {
278 Some(f) => f,
279 None => return Err(NylonRingHostError::MissingRequiredFunctions),
280 };
281
282 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
283
284 drop(binding);
285 self.plugin.host_ctx.remove_state(sid);
286
287 if status != NrStatus::Ok {
288 return Err(Self::status_error(status));
289 }
290
291 match slot.result {
292 Some((st, data)) => Ok((st, data)),
293 None => Err(NylonRingHostError::MissingSynchronousResponse),
294 }
295 }
296
297 pub async fn call(&self, entry: &str, payload: &[u8]) -> Result<NrStatus> {
299 let _call_guard = self.plugin.begin_call()?;
300 let sid = next_sid();
302
303 let payload_bytes = NrBytes::from_slice(payload);
304 let handle_raw_fn = match self.plugin.vtable.handle {
305 Some(f) => f,
306 None => {
307 return Err(NylonRingHostError::MissingRequiredFunctions);
308 }
309 };
310
311 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
312
313 self.plugin.host_ctx.remove_state(sid);
316
317 if status != NrStatus::Ok {
318 return Err(Self::status_error(status));
319 }
320 Ok(status)
321 }
322
323 pub async fn call_stream(&self, entry: &str, payload: &[u8]) -> Result<(u64, StreamReceiver)> {
325 let call_guard = self.plugin.begin_owned_call()?;
326 let sid = next_sid();
327
328 let (tx, rx) =
329 tokio::sync::mpsc::channel::<StreamFrame>(self.plugin.host_ctx.stream_capacity());
330
331 context::insert_pending(&self.plugin.host_ctx, sid, types::Pending::Stream(tx));
333
334 let payload_bytes = NrBytes::from_slice(payload);
335
336 let handle_raw_fn = match self.plugin.vtable.handle {
337 Some(f) => f,
338 None => {
339 context::cleanup_sid(&self.plugin.host_ctx, sid);
340 return Err(NylonRingHostError::MissingRequiredFunctions);
341 }
342 };
343
344 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
345
346 if status != NrStatus::Ok {
347 context::cleanup_sid(&self.plugin.host_ctx, sid);
348 return Err(Self::status_error(status));
349 }
350
351 Ok((
352 sid,
353 StreamReceiver::new(rx, self.plugin.host_ctx.clone(), sid, Some(call_guard)),
354 ))
355 }
356
357 pub fn send_stream_data(&self, sid: u64, data: &[u8]) -> Result<NrStatus> {
359 let stream_data_fn = match self.plugin.vtable.stream_data {
360 Some(f) => f,
361 None => return Err(NylonRingHostError::MissingRequiredFunctions),
362 };
363 let payload = NrBytes::from_slice(data);
364 Ok(unsafe { stream_data_fn(sid, payload) })
365 }
366
367 pub fn close_stream(&self, sid: u64) -> Result<NrStatus> {
369 let stream_close_fn = match self.plugin.vtable.stream_close {
370 Some(f) => f,
371 None => return Err(NylonRingHostError::MissingRequiredFunctions),
372 };
373 Ok(unsafe { stream_close_fn(sid) })
374 }
375}
376
377pub struct NylonRingHost {
379 plugins: HashMap<String, Arc<LoadedPlugin>>,
380 host_ctx: Arc<HostContext>,
381 host_vtable: Box<NrHostVTable>,
382}
383
384impl Default for NylonRingHost {
385 fn default() -> Self {
386 Self::new()
387 }
388}
389
390impl NylonRingHost {
391 pub fn new() -> Self {
393 Self::with_stream_capacity(DEFAULT_STREAM_CAPACITY)
394 }
395
396 pub fn with_stream_capacity(stream_capacity: usize) -> Self {
398 assert!(
399 stream_capacity > 0,
400 "stream capacity must be greater than zero"
401 );
402 let host_ctx = Arc::new(HostContext::new(
403 NrHostExt {
404 set_state: set_state_callback,
405 get_state: get_state_callback,
406 },
407 stream_capacity,
408 ));
409
410 let host_vtable = Box::new(NrHostVTable {
411 send_result: send_result_vec_callback,
412 });
413
414 Self {
415 plugins: HashMap::new(),
416 host_ctx,
417 host_vtable,
418 }
419 }
420
421 pub fn load(&mut self, name: &str, path: &str) -> Result<()> {
423 unsafe {
424 let lib = Library::new(path).map_err(NylonRingHostError::FailedToLoadLibrary)?;
425
426 let get_plugin: Symbol<extern "C" fn() -> *const NrPluginInfo> =
427 lib.get(b"nylon_ring_get_plugin_v1\0").map_err(|_| {
428 NylonRingHostError::MissingSymbol("nylon_ring_get_plugin_v1".to_string())
429 })?;
430
431 let info_ptr = get_plugin();
432 if info_ptr.is_null() {
433 return Err(NylonRingHostError::NullPluginInfo);
434 }
435 let abi_version = std::ptr::read_unaligned(info_ptr.cast::<u32>());
436 let struct_size = std::ptr::read_unaligned(info_ptr.cast::<u32>().add(1));
437
438 if abi_version != ABI_VERSION {
439 return Err(NylonRingHostError::IncompatibleAbiVersion {
440 expected: ABI_VERSION,
441 actual: abi_version,
442 });
443 }
444
445 let expected_size = std::mem::size_of::<NrPluginInfo>() as u32;
446 if struct_size < expected_size {
447 return Err(NylonRingHostError::IncompatiblePluginInfoSize {
448 expected: expected_size,
449 actual: struct_size,
450 });
451 }
452
453 let info = &*info_ptr;
454
455 if info.vtable.is_null() {
456 return Err(NylonRingHostError::NullPluginVTable);
457 }
458 let plugin_vtable = &*info.vtable;
459
460 if plugin_vtable.init.is_none() || plugin_vtable.handle.is_none() {
461 return Err(NylonRingHostError::MissingRequiredFunctions);
462 }
463
464 if let Some(init_fn) = plugin_vtable.init {
466 let status = init_fn(
467 Arc::as_ptr(&self.host_ctx) as *mut c_void,
468 &*self.host_vtable,
469 );
470 if status != NrStatus::Ok {
471 return Err(NylonRingHostError::PluginInitFailed(status));
472 }
473 }
474
475 let loaded = LoadedPlugin {
476 _lib: lib,
477 vtable: plugin_vtable,
478 host_ctx: self.host_ctx.clone(),
479 path: path.to_string(),
480 call_tracker: CallTracker::new(),
481 };
482
483 if let Some(previous) = self.plugins.insert(name.to_string(), Arc::new(loaded)) {
484 previous.stop_accepting_calls();
485 }
486 Ok(())
487 }
488 }
489
490 pub fn unload(&mut self, name: &str) -> Result<()> {
492 let plugin = self
493 .plugins
494 .remove(name)
495 .ok_or_else(|| NylonRingHostError::PluginNotFound(name.to_owned()))?;
496 plugin.stop_accepting_calls();
497 Ok(())
498 }
499
500 pub async fn unload_with_grace(&mut self, name: &str, grace: Duration) -> Result<()> {
502 let plugin = self
503 .plugins
504 .remove(name)
505 .ok_or_else(|| NylonRingHostError::PluginNotFound(name.to_owned()))?;
506 plugin.stop_accepting_calls();
507 Self::wait_for_drain(&plugin, grace).await
508 }
509
510 pub fn reload(&mut self) -> Result<()> {
512 let active_calls: usize = self
513 .plugins
514 .values()
515 .map(|plugin| plugin.call_tracker.active_calls())
516 .sum();
517 if active_calls != 0 {
518 return Err(NylonRingHostError::PluginBusy { active_calls });
519 }
520 let mut plugins_to_reload = Vec::new();
521 for (name, plugin) in &self.plugins {
522 plugins_to_reload.push((name.clone(), plugin.path.clone()));
523 }
524
525 for (name, path) in plugins_to_reload {
528 self.load(&name, &path)?;
529 }
530
531 Ok(())
532 }
533
534 pub async fn reload_with_grace(&mut self, grace: Duration) -> Result<()> {
536 let old_plugins: Vec<_> = self
537 .plugins
538 .drain()
539 .map(|(name, plugin)| {
540 plugin.stop_accepting_calls();
541 (name, plugin)
542 })
543 .collect();
544 for (_, plugin) in &old_plugins {
545 Self::wait_for_drain(plugin, grace).await?;
546 }
547 let plugins_to_reload: Vec<_> = old_plugins
548 .iter()
549 .map(|(name, plugin)| (name.clone(), plugin.path.clone()))
550 .collect();
551 drop(old_plugins);
552 for (name, path) in plugins_to_reload {
553 self.load(&name, &path)?;
554 }
555 Ok(())
556 }
557
558 async fn wait_for_drain(plugin: &LoadedPlugin, grace: Duration) -> Result<()> {
559 let deadline = tokio::time::Instant::now() + grace;
560 while plugin.call_tracker.active_calls() != 0 {
561 if tokio::time::Instant::now() >= deadline {
562 return Err(NylonRingHostError::DrainTimeout {
563 remaining: plugin.call_tracker.active_calls(),
564 });
565 }
566 tokio::time::sleep(Duration::from_millis(1)).await;
567 }
568 Ok(())
569 }
570
571 pub fn plugin(&self, name: &str) -> Option<PluginHandle> {
573 self.plugins
574 .get(name)
575 .map(|p| PluginHandle { plugin: p.clone() })
576 }
577
578 pub fn metrics(&self) -> HostMetrics {
580 HostMetrics {
581 loaded_plugins: self.plugins.len(),
582 pending_requests: self.host_ctx.pending_count(),
583 state_sessions: self.host_ctx.state_count(),
584 in_flight_calls: self
585 .plugins
586 .values()
587 .map(|plugin| plugin.call_tracker.active_calls())
588 .sum(),
589 }
590 }
591
592 pub unsafe fn get_host_ext(host_ctx: *mut c_void) -> *const NrHostExt {
599 if host_ctx.is_null() {
600 return std::ptr::null();
601 }
602 let ctx = unsafe { &*host_ctx.cast::<HostContext>() };
603 &ctx.host_ext as *const NrHostExt
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 struct WakeFlag(Arc<std::sync::atomic::AtomicBool>);
612
613 impl std::task::Wake for WakeFlag {
614 fn wake(self: Arc<Self>) {
615 self.0.store(true, std::sync::atomic::Ordering::Release);
616 }
617
618 fn wake_by_ref(self: &Arc<Self>) {
619 self.0.store(true, std::sync::atomic::Ordering::Release);
620 }
621 }
622
623 struct ReentrantWakerState {
624 host_ctx: Arc<HostContext>,
625 clones: Arc<std::sync::atomic::AtomicUsize>,
626 drops: Arc<std::sync::atomic::AtomicUsize>,
627 }
628
629 unsafe fn clone_reentrant_waker(data: *const ()) -> std::task::RawWaker {
630 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
631 state.host_ctx.pending_count();
632 state
633 .clones
634 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
635 let clone = state.clone();
636 let _ = Arc::into_raw(state);
637 std::task::RawWaker::new(Arc::into_raw(clone).cast(), &REENTRANT_WAKER_VTABLE)
638 }
639
640 unsafe fn wake_reentrant_waker(data: *const ()) {
641 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
642 state.host_ctx.pending_count();
643 }
644
645 unsafe fn wake_reentrant_waker_by_ref(data: *const ()) {
646 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
647 state.host_ctx.pending_count();
648 let _ = Arc::into_raw(state);
649 }
650
651 unsafe fn drop_reentrant_waker(data: *const ()) {
652 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
653 state.host_ctx.pending_count();
654 state
655 .drops
656 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
657 }
658
659 static REENTRANT_WAKER_VTABLE: std::task::RawWakerVTable = std::task::RawWakerVTable::new(
660 clone_reentrant_waker,
661 wake_reentrant_waker,
662 wake_reentrant_waker_by_ref,
663 drop_reentrant_waker,
664 );
665
666 fn reentrant_waker(
667 host_ctx: Arc<HostContext>,
668 clones: Arc<std::sync::atomic::AtomicUsize>,
669 drops: Arc<std::sync::atomic::AtomicUsize>,
670 ) -> std::task::Waker {
671 let state = Arc::new(ReentrantWakerState {
672 host_ctx,
673 clones,
674 drops,
675 });
676 let raw = std::task::RawWaker::new(Arc::into_raw(state).cast(), &REENTRANT_WAKER_VTABLE);
677 unsafe { std::task::Waker::from_raw(raw) }
678 }
679
680 fn example_plugin_path() -> Option<std::path::PathBuf> {
681 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
682 .parent()?
683 .parent()?;
684 let manifest = workspace_root.join("examples/ex-nyring-plugin/Cargo.toml");
685 if !manifest.is_file() {
686 return None;
687 }
688 let status = std::process::Command::new("cargo")
689 .args(["build", "--release", "--manifest-path"])
690 .arg(&manifest)
691 .status()
692 .ok()?;
693 if !status.success() {
694 return None;
695 }
696 let filename = if cfg!(target_os = "macos") {
697 "libex_nyring_plugin.dylib"
698 } else if cfg!(target_os = "windows") {
699 "ex_nyring_plugin.dll"
700 } else {
701 "libex_nyring_plugin.so"
702 };
703 Some(workspace_root.join("target/release").join(filename))
704 }
705
706 fn c_plugin_path() -> Option<std::path::PathBuf> {
707 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
708 .parent()?
709 .parent()?;
710 let source = workspace_root.join("examples/c-plugin/plugin.c");
711 if !source.is_file() {
712 return None;
713 }
714 let extension = if cfg!(target_os = "macos") {
715 "dylib"
716 } else if cfg!(target_os = "windows") {
717 "dll"
718 } else {
719 "so"
720 };
721 let output = workspace_root
722 .join("target/c-example")
723 .join(format!("libnylon_ring_c_example.{extension}"));
724 std::fs::create_dir_all(output.parent()?).ok()?;
725 let status = std::process::Command::new("cc")
726 .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-shared"])
727 .args((!cfg!(target_os = "windows")).then_some("-fPIC"))
728 .arg(&source)
729 .arg("-o")
730 .arg(&output)
731 .status()
732 .ok()?;
733 status.success().then_some(output)
734 }
735
736 #[test]
737 fn dropping_pending_guard_unregisters_unary_request() {
738 let host = NylonRingHost::new();
739 let sid = 42;
740 context::insert_pending(
741 &host.host_ctx,
742 sid,
743 types::Pending::Unary(types::UnaryPending::waiting()),
744 );
745 host.host_ctx.set_state(sid, "key".into(), vec![1]);
746
747 drop(PendingGuard::new(&host.host_ctx, sid));
748
749 assert!(context::remove_pending(&host.host_ctx, sid).is_none());
750 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
751 }
752
753 #[test]
754 fn unary_completion_wakes_latest_waiter_across_threads() {
755 let host = NylonRingHost::new();
756 let sid = 46;
757 context::insert_pending(
758 &host.host_ctx,
759 sid,
760 types::Pending::Unary(types::UnaryPending::waiting()),
761 );
762 host.host_ctx.set_state(sid, "key".into(), vec![1]);
763
764 let first_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
765 let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
766 let first_waker = std::task::Waker::from(Arc::new(WakeFlag(first_woken.clone())));
767 let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken.clone())));
768 let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
769
770 let mut first_context = std::task::Context::from_waker(&first_waker);
771 assert!(matches!(
772 std::future::Future::poll(future.as_mut(), &mut first_context),
773 std::task::Poll::Pending
774 ));
775 let mut second_context = std::task::Context::from_waker(&second_waker);
776 assert!(matches!(
777 std::future::Future::poll(future.as_mut(), &mut second_context),
778 std::task::Poll::Pending
779 ));
780
781 let host_ctx = host.host_ctx.clone();
782 let dispatch_status = std::thread::spawn(move || {
783 context::dispatch_pending(
784 &host_ctx,
785 sid,
786 StreamFrame {
787 status: NrStatus::Ok,
788 data: vec![7],
789 },
790 )
791 })
792 .join()
793 .unwrap();
794 assert_eq!(dispatch_status, NrStatus::Ok);
795 assert!(!first_woken.load(std::sync::atomic::Ordering::Acquire));
796 assert!(second_woken.load(std::sync::atomic::Ordering::Acquire));
797 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
798 assert_eq!(host.metrics().pending_requests, 1);
799
800 assert_eq!(
801 context::dispatch_pending(
802 &host.host_ctx,
803 sid,
804 StreamFrame {
805 status: NrStatus::Ok,
806 data: vec![8],
807 },
808 ),
809 NrStatus::Invalid
810 );
811
812 match std::future::Future::poll(future.as_mut(), &mut second_context) {
813 std::task::Poll::Ready(Some((status, data))) => {
814 assert_eq!(status, NrStatus::Ok);
815 assert_eq!(data, vec![7]);
816 }
817 result => panic!("unexpected unary completion result: {result:?}"),
818 }
819 assert_eq!(host.metrics().pending_requests, 0);
820 }
821
822 #[test]
823 fn unary_waker_callbacks_run_outside_pending_lock() {
824 let host = NylonRingHost::new();
825 let sid = 47;
826 context::insert_pending(
827 &host.host_ctx,
828 sid,
829 types::Pending::Unary(types::UnaryPending::waiting()),
830 );
831
832 let clones = Arc::new(std::sync::atomic::AtomicUsize::new(0));
833 let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
834 let first_waker = reentrant_waker(host.host_ctx.clone(), clones.clone(), drops.clone());
835 let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
836 {
837 let mut context = std::task::Context::from_waker(&first_waker);
838 assert!(matches!(
839 std::future::Future::poll(future.as_mut(), &mut context),
840 std::task::Poll::Pending
841 ));
842 }
843 assert_eq!(clones.load(std::sync::atomic::Ordering::Relaxed), 1);
844
845 let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
846 let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken)));
847 let mut context = std::task::Context::from_waker(&second_waker);
848 assert!(matches!(
849 std::future::Future::poll(future.as_mut(), &mut context),
850 std::task::Poll::Pending
851 ));
852 assert_eq!(drops.load(std::sync::atomic::Ordering::Relaxed), 1);
853
854 drop(future);
855 context::cleanup_sid(&host.host_ctx, sid);
856 }
857
858 #[test]
859 fn dropping_stream_receiver_unregisters_stream() {
860 let host = NylonRingHost::new();
861 let sid = 43;
862 let (tx, rx) = tokio::sync::mpsc::channel(1);
863 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
864 host.host_ctx.set_state(sid, "key".into(), vec![1]);
865
866 drop(StreamReceiver::new(rx, host.host_ctx.clone(), sid, None));
867
868 assert!(context::remove_pending(&host.host_ctx, sid).is_none());
869 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
870 }
871
872 #[test]
873 fn fast_slot_reentry_is_rejected_and_binding_is_cleared() {
874 let mut first = types::UnaryResultSlot {
875 sid: 1,
876 result: None,
877 };
878 let mut second = types::UnaryResultSlot {
879 sid: 2,
880 result: None,
881 };
882 let binding = FastSlotBinding::bind(&mut first).unwrap();
883 assert!(matches!(
884 FastSlotBinding::bind(&mut second),
885 Err(NylonRingHostError::FastPathReentrant)
886 ));
887 drop(binding);
888 assert!(FastSlotBinding::bind(&mut second).is_ok());
889 }
890
891 #[test]
892 fn bounded_stream_reports_backpressure_and_removes_terminal_once() {
893 let host = NylonRingHost::with_stream_capacity(1);
894 let sid = 44;
895 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
896 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
897
898 assert_eq!(
899 context::dispatch_pending(
900 &host.host_ctx,
901 sid,
902 StreamFrame {
903 status: NrStatus::Ok,
904 data: vec![1],
905 },
906 ),
907 NrStatus::Ok
908 );
909 assert_eq!(
910 context::dispatch_pending(
911 &host.host_ctx,
912 sid,
913 StreamFrame {
914 status: NrStatus::Ok,
915 data: vec![2],
916 },
917 ),
918 NrStatus::Backpressure
919 );
920 assert_eq!(rx.try_recv().unwrap().data, vec![1]);
921 assert_eq!(
922 context::dispatch_pending(
923 &host.host_ctx,
924 sid,
925 StreamFrame {
926 status: NrStatus::StreamEnd,
927 data: vec![],
928 },
929 ),
930 NrStatus::Ok
931 );
932 assert_eq!(host.metrics().pending_requests, 0);
933 assert_eq!(rx.try_recv().unwrap().status, NrStatus::StreamEnd);
934 }
935
936 #[test]
937 fn concurrent_terminal_frames_complete_stream_once() {
938 let host = NylonRingHost::with_stream_capacity(2);
939 let sid = 45;
940 let (tx, mut rx) = tokio::sync::mpsc::channel(2);
941 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
942 let barrier = Arc::new(std::sync::Barrier::new(3));
943
944 let results: Vec<_> = std::thread::scope(|scope| {
945 let handles: Vec<_> = (0..2)
946 .map(|value| {
947 let ctx = host.host_ctx.clone();
948 let barrier = barrier.clone();
949 scope.spawn(move || {
950 barrier.wait();
951 context::dispatch_pending(
952 &ctx,
953 sid,
954 StreamFrame {
955 status: NrStatus::StreamEnd,
956 data: vec![value],
957 },
958 )
959 })
960 })
961 .collect();
962 barrier.wait();
963 handles
964 .into_iter()
965 .map(|handle| handle.join().unwrap())
966 .collect()
967 });
968
969 assert_eq!(
970 results
971 .iter()
972 .filter(|&&status| status == NrStatus::Ok)
973 .count(),
974 1
975 );
976 assert_eq!(
977 results
978 .iter()
979 .filter(|&&status| status == NrStatus::Invalid)
980 .count(),
981 1
982 );
983 assert!(rx.try_recv().is_ok());
984 assert!(rx.try_recv().is_err());
985 assert_eq!(host.metrics().pending_requests, 0);
986 }
987
988 #[test]
989 fn unload_defers_library_drop_and_rejects_new_calls_on_existing_handle() {
990 let Some(path) = example_plugin_path() else {
991 return;
992 };
993 let mut host = NylonRingHost::new();
994 host.load("example", path.to_str().unwrap()).unwrap();
995 let handle = host.plugin("example").unwrap();
996 let runtime = tokio::runtime::Runtime::new().unwrap();
997 assert!(matches!(
998 runtime.block_on(handle.call_response_timeout(
999 "benchmark_without_response",
1000 b"",
1001 Duration::from_millis(1),
1002 )),
1003 Err(NylonRingHostError::Timeout)
1004 ));
1005 assert_eq!(host.metrics().pending_requests, 0);
1006
1007 let guard = handle.plugin.begin_call().unwrap();
1008 assert_eq!(host.metrics().in_flight_calls, 1);
1009
1010 host.unload("example").unwrap();
1011 assert!(matches!(
1012 runtime.block_on(handle.call("benchmark_without_response", b"")),
1013 Err(NylonRingHostError::PluginUnloaded)
1014 ));
1015 drop(guard);
1016 drop(handle);
1017 }
1018
1019 #[test]
1020 fn example_plugin_fire_and_forget_entry_returns_ok() {
1021 let Some(path) = example_plugin_path() else {
1022 return;
1023 };
1024 let mut host = NylonRingHost::new();
1025 host.load("example", path.to_str().unwrap()).unwrap();
1026 let handle = host.plugin("example").unwrap();
1027 let runtime = tokio::runtime::Runtime::new().unwrap();
1028
1029 assert!(matches!(
1030 runtime.block_on(handle.call("notify", b"fire and forget")),
1031 Ok(NrStatus::Ok)
1032 ));
1033 }
1034
1035 #[test]
1036 fn c_plugin_layout_round_trips_through_host() {
1037 let Some(path) = c_plugin_path() else {
1038 return;
1039 };
1040 let mut host = NylonRingHost::new();
1041 host.load("c-example", path.to_str().unwrap()).unwrap();
1042 let handle = host.plugin("c-example").unwrap();
1043 let runtime = tokio::runtime::Runtime::new().unwrap();
1044 let (status, response) = runtime
1045 .block_on(handle.call_response("echo", b"from-c"))
1046 .unwrap();
1047 assert_eq!(status, NrStatus::Ok);
1048 assert_eq!(response, b"from-c");
1049 }
1050}