1mod call_tracker;
9mod callbacks;
10mod context;
11mod error;
12mod extensions;
13#[cfg(test)]
14mod loom_tests;
15mod sid;
16mod stream_channel;
17mod types;
18
19use call_tracker::CallTracker;
20use callbacks::{
21 acquire_result_buffer_callback, commit_result_buffer_callback, get_state_callback,
22 send_result_owned_callback, send_result_vec_callback, set_state_callback,
23};
24use context::{CURRENT_UNARY_RESULT, HostContext};
25use libloading::{Library, Symbol};
26use nylon_ring::{ABI_VERSION, NrBytes, NrHostExt, NrHostVTable, NrPluginInfo, NrStr};
27use sid::next_sid;
28use std::collections::HashMap;
29use std::ffi::c_void;
30use std::sync::Arc;
31use std::time::Duration;
32
33pub use error::NylonRingHostError;
34pub use extensions::Extensions;
35pub use nylon_ring::NrStatus;
36pub use types::{ResponseBytes, Result, StreamFrame, StreamReceiver};
37
38pub const DEFAULT_STREAM_CAPACITY: usize = 64;
40
41#[derive(Debug, Copy, Clone, PartialEq, Eq)]
43pub struct HostMetrics {
44 pub loaded_plugins: usize,
45 pub pending_requests: usize,
46 pub state_sessions: usize,
47 pub in_flight_calls: usize,
48}
49
50struct PendingGuard<'a> {
51 host_ctx: &'a HostContext,
52 sid: u64,
53 armed: bool,
54}
55
56impl<'a> PendingGuard<'a> {
57 fn new(host_ctx: &'a HostContext, sid: u64) -> Self {
58 Self {
59 host_ctx,
60 sid,
61 armed: true,
62 }
63 }
64
65 fn disarm(&mut self) {
66 self.armed = false;
67 }
68}
69
70impl Drop for PendingGuard<'_> {
71 fn drop(&mut self) {
72 if self.armed {
73 context::cleanup_sid(self.host_ctx, self.sid);
74 }
75 }
76}
77
78struct FastSlotBinding;
79
80impl FastSlotBinding {
81 fn bind(slot: &mut types::UnaryResultSlot) -> Result<Self> {
82 let bound = CURRENT_UNARY_RESULT.with(|cell| {
83 if !cell.get().is_null() {
84 return false;
85 }
86 cell.set(slot as *mut _);
87 true
88 });
89 if bound {
90 Ok(Self)
91 } else {
92 Err(NylonRingHostError::FastPathReentrant)
93 }
94 }
95}
96
97impl Drop for FastSlotBinding {
98 fn drop(&mut self) {
99 CURRENT_UNARY_RESULT.with(|cell| cell.set(std::ptr::null_mut()));
100 }
101}
102
103#[derive(Copy, Clone)]
106struct PluginDispatch {
107 handle: Option<unsafe extern "C" fn(NrStr, u64, NrBytes) -> NrStatus>,
108 shutdown: Option<unsafe extern "C" fn()>,
109 stream_data: Option<unsafe extern "C" fn(u64, NrBytes) -> NrStatus>,
110 stream_close: Option<unsafe extern "C" fn(u64) -> NrStatus>,
111 resolve_entry: Option<unsafe extern "C" fn(NrStr) -> u32>,
112 handle_by_id: Option<unsafe extern "C" fn(u32, u64, NrBytes) -> NrStatus>,
113}
114
115impl PluginDispatch {
116 fn from_vtable(vtable: &nylon_ring::NrPluginVTable) -> Self {
117 Self {
118 handle: vtable.handle,
119 shutdown: vtable.shutdown,
120 stream_data: vtable.stream_data,
121 stream_close: vtable.stream_close,
122 resolve_entry: vtable.resolve_entry,
123 handle_by_id: vtable.handle_by_id,
124 }
125 }
126}
127
128pub struct LoadedPlugin {
130 _lib: Library,
131 dispatch: PluginDispatch,
132 host_ctx: Arc<HostContext>,
133 path: String,
134 call_tracker: CallTracker,
135}
136
137static RETIRED_PLUGINS: std::sync::Mutex<Vec<Arc<LoadedPlugin>>> =
146 std::sync::Mutex::new(Vec::new());
147
148fn retire_plugin(plugin: Arc<LoadedPlugin>) {
153 plugin.stop_accepting_calls();
154 RETIRED_PLUGINS.lock().unwrap().push(plugin);
155 sweep_retired_plugins();
158}
159
160fn sweep_retired_plugins() {
171 std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
172 let drained: Vec<Arc<LoadedPlugin>> = {
173 let mut retired = RETIRED_PLUGINS.lock().unwrap();
174 let (drained, live) = std::mem::take(&mut *retired)
175 .into_iter()
176 .partition(|plugin| plugin.call_tracker.active_calls() == 0);
177 *retired = live;
178 drained
179 };
180 drop(drained);
183}
184
185impl LoadedPlugin {
186 fn begin_call(&self) -> Result<BorrowedPluginCallGuard<'_>> {
187 let shard = self
188 .call_tracker
189 .try_begin()
190 .ok_or(NylonRingHostError::PluginUnloaded)?;
191 Ok(BorrowedPluginCallGuard {
192 tracker: &self.call_tracker,
193 shard,
194 })
195 }
196
197 fn begin_owned_call(self: &Arc<Self>) -> Result<PluginCallGuard> {
198 let shard = self
199 .call_tracker
200 .try_begin()
201 .ok_or(NylonRingHostError::PluginUnloaded)?;
202 Ok(PluginCallGuard {
203 plugin: Arc::as_ptr(self),
204 shard,
205 })
206 }
207
208 fn stop_accepting_calls(&self) {
209 self.call_tracker.stop();
210 }
211}
212
213struct BorrowedPluginCallGuard<'a> {
214 tracker: &'a CallTracker,
215 shard: usize,
216}
217
218impl Drop for BorrowedPluginCallGuard<'_> {
219 fn drop(&mut self) {
220 if self.tracker.finish(self.shard) {
221 sweep_retired_plugins();
222 }
223 }
224}
225
226pub(crate) struct PluginCallGuard {
236 plugin: *const LoadedPlugin,
237 shard: usize,
238}
239
240unsafe impl Send for PluginCallGuard {}
244unsafe impl Sync for PluginCallGuard {}
245
246impl PluginCallGuard {
247 pub(crate) fn plugin(&self) -> &LoadedPlugin {
248 unsafe { &*self.plugin }
251 }
252}
253
254impl Drop for PluginCallGuard {
255 fn drop(&mut self) {
256 if self.plugin().call_tracker.finish(self.shard) {
259 sweep_retired_plugins();
260 }
261 }
262}
263
264impl Drop for LoadedPlugin {
265 fn drop(&mut self) {
266 if let Some(shutdown_fn) = self.dispatch.shutdown {
267 unsafe {
268 shutdown_fn();
269 }
270 }
271 }
272}
273
274#[derive(Clone)]
276pub struct PluginHandle {
277 plugin: Arc<LoadedPlugin>,
278}
279
280impl PluginHandle {
281 fn status_error(status: NrStatus) -> NylonRingHostError {
282 if status == NrStatus::Panic {
283 NylonRingHostError::PluginPanicked
284 } else {
285 NylonRingHostError::PluginHandleFailed(status)
286 }
287 }
288
289 pub async fn call_response(&self, entry: &str, payload: &[u8]) -> Result<(NrStatus, Vec<u8>)> {
291 let _call_guard = self.plugin.begin_call()?;
292 let sid = next_sid();
293
294 context::insert_pending(
295 &self.plugin.host_ctx,
296 sid,
297 types::Pending::Unary(types::UnaryPending::waiting()),
298 );
299 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
300
301 let payload_bytes = NrBytes::from_slice(payload);
302 let handle_raw_fn = match self.plugin.dispatch.handle {
303 Some(f) => f,
304 None => return Err(NylonRingHostError::MissingRequiredFunctions),
305 };
306
307 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
308
309 if status != NrStatus::Ok {
310 return Err(Self::status_error(status));
311 }
312
313 match context::wait_for_unary(&self.plugin.host_ctx, sid).await {
314 Some((status, payload)) => {
315 pending_guard.disarm();
316 Ok((status, payload.into_vec()))
319 }
320 None => Err(NylonRingHostError::PluginUnloaded),
321 }
322 }
323
324 pub async fn call_response_bytes(
332 &self,
333 entry: &str,
334 payload: &[u8],
335 ) -> Result<(NrStatus, ResponseBytes)> {
336 let call_guard = self.plugin.begin_owned_call()?;
337 let sid = next_sid();
338
339 context::insert_pending(
340 &self.plugin.host_ctx,
341 sid,
342 types::Pending::Unary(types::UnaryPending::waiting()),
343 );
344 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
345
346 let payload_bytes = NrBytes::from_slice(payload);
347 let handle_raw_fn = match self.plugin.dispatch.handle {
348 Some(f) => f,
349 None => return Err(NylonRingHostError::MissingRequiredFunctions),
350 };
351
352 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
353
354 if status != NrStatus::Ok {
355 return Err(Self::status_error(status));
356 }
357
358 match context::wait_for_unary(&self.plugin.host_ctx, sid).await {
359 Some((status, payload)) => {
360 pending_guard.disarm();
361 let guard =
364 matches!(payload, types::ResponsePayload::Foreign(_)).then_some(call_guard);
365 Ok((status, ResponseBytes::new(payload, guard)))
366 }
367 None => Err(NylonRingHostError::PluginUnloaded),
368 }
369 }
370
371 pub async fn call_response_timeout(
378 &self,
379 entry: &str,
380 payload: &[u8],
381 timeout: std::time::Duration,
382 ) -> Result<(NrStatus, Vec<u8>)> {
383 let _call_guard = self.plugin.begin_call()?;
384 let sid = next_sid();
385
386 context::insert_pending(
387 &self.plugin.host_ctx,
388 sid,
389 types::Pending::Unary(types::UnaryPending::waiting()),
390 );
391 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
392
393 let payload_bytes = NrBytes::from_slice(payload);
394 let handle_raw_fn = match self.plugin.dispatch.handle {
395 Some(f) => f,
396 None => return Err(NylonRingHostError::MissingRequiredFunctions),
397 };
398
399 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
400 if status != NrStatus::Ok {
401 return Err(Self::status_error(status));
402 }
403
404 match tokio::time::timeout(timeout, context::wait_for_unary(&self.plugin.host_ctx, sid))
405 .await
406 {
407 Ok(Some((status, payload))) => {
408 pending_guard.disarm();
409 Ok((status, payload.into_vec()))
410 }
411 Ok(None) => Err(NylonRingHostError::PluginUnloaded),
412 Err(_) => Err(NylonRingHostError::Timeout),
413 }
414 }
415
416 pub async fn call_response_fast(
418 &self,
419 entry: &str,
420 payload: &[u8],
421 ) -> Result<(NrStatus, Vec<u8>)> {
422 let _call_guard = self.plugin.begin_call()?;
423 let sid = next_sid();
424 let mut slot = types::UnaryResultSlot {
425 sid,
426 result: None,
427 lease: None,
428 };
429
430 let binding = FastSlotBinding::bind(&mut slot)?;
431
432 let payload_bytes = NrBytes::from_slice(payload);
433
434 let handle_raw_fn = match self.plugin.dispatch.handle {
435 Some(f) => f,
436 None => return Err(NylonRingHostError::MissingRequiredFunctions),
437 };
438
439 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
440
441 drop(binding);
442 if let Some(lease) = slot.lease.take() {
446 self.plugin.host_ctx.park_orphan_lease(lease);
447 }
448 self.plugin.host_ctx.remove_state(sid);
449
450 if status != NrStatus::Ok {
451 return Err(Self::status_error(status));
452 }
453
454 match slot.result {
455 Some((st, data)) => Ok((st, data)),
456 None => Err(NylonRingHostError::MissingSynchronousResponse),
457 }
458 }
459
460 pub async fn call(&self, entry: &str, payload: &[u8]) -> Result<NrStatus> {
462 let _call_guard = self.plugin.begin_call()?;
463 let sid = next_sid();
465
466 let payload_bytes = NrBytes::from_slice(payload);
467 let handle_raw_fn = match self.plugin.dispatch.handle {
468 Some(f) => f,
469 None => {
470 return Err(NylonRingHostError::MissingRequiredFunctions);
471 }
472 };
473
474 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
475
476 self.plugin.host_ctx.remove_state(sid);
479
480 if status != NrStatus::Ok {
481 return Err(Self::status_error(status));
482 }
483 Ok(status)
484 }
485
486 pub async fn call_stream(&self, entry: &str, payload: &[u8]) -> Result<(u64, StreamReceiver)> {
488 let call_guard = self.plugin.begin_owned_call()?;
489 let sid = next_sid();
490
491 let (tx, rx) = stream_channel::acquire(self.plugin.host_ctx.stream_capacity());
492
493 context::insert_pending(&self.plugin.host_ctx, sid, types::Pending::Stream(tx));
495
496 let payload_bytes = NrBytes::from_slice(payload);
497
498 let handle_raw_fn = match self.plugin.dispatch.handle {
499 Some(f) => f,
500 None => {
501 context::cleanup_sid(&self.plugin.host_ctx, sid);
502 return Err(NylonRingHostError::MissingRequiredFunctions);
503 }
504 };
505
506 let status = unsafe { handle_raw_fn(NrStr::new(entry), sid, payload_bytes) };
507
508 if status != NrStatus::Ok {
509 context::cleanup_sid(&self.plugin.host_ctx, sid);
510 return Err(Self::status_error(status));
511 }
512
513 Ok((sid, StreamReceiver::new(rx, None, sid, Some(call_guard))))
514 }
515
516 pub fn send_stream_data(&self, sid: u64, data: &[u8]) -> Result<NrStatus> {
518 let stream_data_fn = match self.plugin.dispatch.stream_data {
519 Some(f) => f,
520 None => return Err(NylonRingHostError::MissingRequiredFunctions),
521 };
522 let payload = NrBytes::from_slice(data);
523 Ok(unsafe { stream_data_fn(sid, payload) })
524 }
525
526 pub fn close_stream(&self, sid: u64) -> Result<NrStatus> {
528 let stream_close_fn = match self.plugin.dispatch.stream_close {
529 Some(f) => f,
530 None => return Err(NylonRingHostError::MissingRequiredFunctions),
531 };
532 Ok(unsafe { stream_close_fn(sid) })
533 }
534
535 pub fn entry(&self, entry: &str) -> Result<PluginEntry> {
545 let (Some(resolve_fn), Some(_)) = (
546 self.plugin.dispatch.resolve_entry,
547 self.plugin.dispatch.handle_by_id,
548 ) else {
549 return Err(NylonRingHostError::EntryDispatchUnsupported);
550 };
551 let _call_guard = self.plugin.begin_call()?;
553 let id = unsafe { resolve_fn(NrStr::new(entry)) };
554 if id == nylon_ring::NR_ENTRY_UNKNOWN {
555 return Err(NylonRingHostError::EntryNotFound(entry.to_string()));
556 }
557 Ok(PluginEntry {
558 plugin: self.plugin.clone(),
559 id,
560 })
561 }
562}
563
564#[derive(Clone)]
568pub struct PluginEntry {
569 plugin: Arc<LoadedPlugin>,
570 id: u32,
571}
572
573impl PluginEntry {
574 fn handle_by_id_fn(&self) -> Result<unsafe extern "C" fn(u32, u64, NrBytes) -> NrStatus> {
575 self.plugin
576 .dispatch
577 .handle_by_id
578 .ok_or(NylonRingHostError::EntryDispatchUnsupported)
579 }
580
581 pub async fn call(&self, payload: &[u8]) -> Result<NrStatus> {
583 let _call_guard = self.plugin.begin_call()?;
584 let sid = next_sid();
585
586 let handle_by_id_fn = self.handle_by_id_fn()?;
587 let status = unsafe { handle_by_id_fn(self.id, sid, NrBytes::from_slice(payload)) };
588
589 self.plugin.host_ctx.remove_state(sid);
590
591 if status != NrStatus::Ok {
592 return Err(PluginHandle::status_error(status));
593 }
594 Ok(status)
595 }
596
597 pub async fn call_response(&self, payload: &[u8]) -> Result<(NrStatus, Vec<u8>)> {
599 let _call_guard = self.plugin.begin_call()?;
600 let sid = next_sid();
601
602 context::insert_pending(
603 &self.plugin.host_ctx,
604 sid,
605 types::Pending::Unary(types::UnaryPending::waiting()),
606 );
607 let mut pending_guard = PendingGuard::new(&self.plugin.host_ctx, sid);
608
609 let handle_by_id_fn = self.handle_by_id_fn()?;
610 let status = unsafe { handle_by_id_fn(self.id, sid, NrBytes::from_slice(payload)) };
611
612 if status != NrStatus::Ok {
613 return Err(PluginHandle::status_error(status));
614 }
615
616 match context::wait_for_unary(&self.plugin.host_ctx, sid).await {
617 Some((status, payload)) => {
618 pending_guard.disarm();
619 Ok((status, payload.into_vec()))
620 }
621 None => Err(NylonRingHostError::PluginUnloaded),
622 }
623 }
624
625 pub async fn call_response_fast(&self, payload: &[u8]) -> Result<(NrStatus, Vec<u8>)> {
627 let _call_guard = self.plugin.begin_call()?;
628 let sid = next_sid();
629 let mut slot = types::UnaryResultSlot {
630 sid,
631 result: None,
632 lease: None,
633 };
634
635 let binding = FastSlotBinding::bind(&mut slot)?;
636
637 let handle_by_id_fn = self.handle_by_id_fn()?;
638 let status = unsafe { handle_by_id_fn(self.id, sid, NrBytes::from_slice(payload)) };
639
640 drop(binding);
641 if let Some(lease) = slot.lease.take() {
642 self.plugin.host_ctx.park_orphan_lease(lease);
643 }
644 self.plugin.host_ctx.remove_state(sid);
645
646 if status != NrStatus::Ok {
647 return Err(PluginHandle::status_error(status));
648 }
649
650 match slot.result {
651 Some((st, data)) => Ok((st, data)),
652 None => Err(NylonRingHostError::MissingSynchronousResponse),
653 }
654 }
655
656 pub async fn call_stream(&self, payload: &[u8]) -> Result<(u64, StreamReceiver)> {
658 let call_guard = self.plugin.begin_owned_call()?;
659 let sid = next_sid();
660
661 let (tx, rx) = stream_channel::acquire(self.plugin.host_ctx.stream_capacity());
662 context::insert_pending(&self.plugin.host_ctx, sid, types::Pending::Stream(tx));
663
664 let handle_by_id_fn = match self.handle_by_id_fn() {
665 Ok(f) => f,
666 Err(error) => {
667 context::cleanup_sid(&self.plugin.host_ctx, sid);
668 return Err(error);
669 }
670 };
671 let status = unsafe { handle_by_id_fn(self.id, sid, NrBytes::from_slice(payload)) };
672
673 if status != NrStatus::Ok {
674 context::cleanup_sid(&self.plugin.host_ctx, sid);
675 return Err(PluginHandle::status_error(status));
676 }
677
678 Ok((sid, StreamReceiver::new(rx, None, sid, Some(call_guard))))
679 }
680}
681
682pub struct NylonRingHost {
684 plugins: HashMap<String, Arc<LoadedPlugin>>,
685 host_ctx: Arc<HostContext>,
686 host_vtable: Box<NrHostVTable>,
687}
688
689impl Default for NylonRingHost {
690 fn default() -> Self {
691 Self::new()
692 }
693}
694
695impl Drop for NylonRingHost {
696 fn drop(&mut self) {
697 for (_, plugin) in self.plugins.drain() {
701 retire_plugin(plugin);
702 }
703 }
704}
705
706impl NylonRingHost {
707 pub fn new() -> Self {
709 Self::with_stream_capacity(DEFAULT_STREAM_CAPACITY)
710 }
711
712 pub fn with_stream_capacity(stream_capacity: usize) -> Self {
714 assert!(
715 stream_capacity > 0,
716 "stream capacity must be greater than zero"
717 );
718 let host_ctx = Arc::new(HostContext::new(
719 NrHostExt {
720 set_state: set_state_callback,
721 get_state: get_state_callback,
722 },
723 stream_capacity,
724 ));
725
726 let host_vtable = Box::new(NrHostVTable {
727 send_result: send_result_vec_callback,
728 send_result_owned: send_result_owned_callback,
729 acquire_result_buffer: acquire_result_buffer_callback,
730 commit_result_buffer: commit_result_buffer_callback,
731 });
732
733 Self {
734 plugins: HashMap::new(),
735 host_ctx,
736 host_vtable,
737 }
738 }
739
740 pub fn load(&mut self, name: &str, path: &str) -> Result<()> {
745 unsafe {
746 let lib = Library::new(path).map_err(NylonRingHostError::FailedToLoadLibrary)?;
747
748 let get_plugin: Symbol<extern "C" fn() -> *const NrPluginInfo> =
749 lib.get(b"nylon_ring_get_plugin\0").map_err(|_| {
750 NylonRingHostError::MissingSymbol("nylon_ring_get_plugin".to_string())
751 })?;
752 let info_ptr = get_plugin();
753 Self::validate_plugin_info::<NrPluginInfo>(info_ptr.cast(), ABI_VERSION)?;
754 let info = &*info_ptr;
755 if info.vtable.is_null() {
756 return Err(NylonRingHostError::NullPluginVTable);
757 }
758 let plugin_vtable = &*info.vtable;
759 if plugin_vtable.init.is_none() || plugin_vtable.handle.is_none() {
760 return Err(NylonRingHostError::MissingRequiredFunctions);
761 }
762 if let Some(init_fn) = plugin_vtable.init {
763 let status = init_fn(
764 Arc::as_ptr(&self.host_ctx) as *mut c_void,
765 &*self.host_vtable,
766 );
767 if status != NrStatus::Ok {
768 return Err(NylonRingHostError::PluginInitFailed(status));
769 }
770 }
771 let dispatch = PluginDispatch::from_vtable(plugin_vtable);
772
773 let loaded = LoadedPlugin {
774 _lib: lib,
775 dispatch,
776 host_ctx: self.host_ctx.clone(),
777 path: path.to_string(),
778 call_tracker: CallTracker::new(),
779 };
780
781 if let Some(previous) = self.plugins.insert(name.to_string(), Arc::new(loaded)) {
782 retire_plugin(previous);
783 }
784 Ok(())
785 }
786 }
787
788 unsafe fn validate_plugin_info<T>(info_ptr: *const u32, expected_version: u32) -> Result<()> {
796 if info_ptr.is_null() {
797 return Err(NylonRingHostError::NullPluginInfo);
798 }
799 let abi_version = unsafe { std::ptr::read_unaligned(info_ptr) };
800 let struct_size = unsafe { std::ptr::read_unaligned(info_ptr.add(1)) };
801 if abi_version != expected_version {
802 return Err(NylonRingHostError::IncompatibleAbiVersion {
803 expected: expected_version,
804 actual: abi_version,
805 });
806 }
807 let expected_size = std::mem::size_of::<T>() as u32;
808 if struct_size < expected_size {
809 return Err(NylonRingHostError::IncompatiblePluginInfoSize {
810 expected: expected_size,
811 actual: struct_size,
812 });
813 }
814 Ok(())
815 }
816
817 pub fn unload(&mut self, name: &str) -> Result<()> {
819 let plugin = self
820 .plugins
821 .remove(name)
822 .ok_or_else(|| NylonRingHostError::PluginNotFound(name.to_owned()))?;
823 retire_plugin(plugin);
824 Ok(())
825 }
826
827 pub async fn unload_with_grace(&mut self, name: &str, grace: Duration) -> Result<()> {
829 let plugin = self
830 .plugins
831 .remove(name)
832 .ok_or_else(|| NylonRingHostError::PluginNotFound(name.to_owned()))?;
833 retire_plugin(plugin.clone());
836 Self::wait_for_drain(&plugin, grace).await
837 }
838
839 pub fn reload(&mut self) -> Result<()> {
841 let active_calls: usize = self
842 .plugins
843 .values()
844 .map(|plugin| plugin.call_tracker.active_calls())
845 .sum();
846 if active_calls != 0 {
847 return Err(NylonRingHostError::PluginBusy { active_calls });
848 }
849 let mut plugins_to_reload = Vec::new();
850 for (name, plugin) in &self.plugins {
851 plugins_to_reload.push((name.clone(), plugin.path.clone()));
852 }
853
854 for (name, path) in plugins_to_reload {
857 self.load(&name, &path)?;
858 }
859
860 Ok(())
861 }
862
863 pub async fn reload_with_grace(&mut self, grace: Duration) -> Result<()> {
865 let old_plugins: Vec<_> = self
866 .plugins
867 .drain()
868 .map(|(name, plugin)| {
869 retire_plugin(plugin.clone());
870 (name, plugin)
871 })
872 .collect();
873 for (_, plugin) in &old_plugins {
874 Self::wait_for_drain(plugin, grace).await?;
875 }
876 let plugins_to_reload: Vec<_> = old_plugins
877 .iter()
878 .map(|(name, plugin)| (name.clone(), plugin.path.clone()))
879 .collect();
880 drop(old_plugins);
881 for (name, path) in plugins_to_reload {
882 self.load(&name, &path)?;
883 }
884 Ok(())
885 }
886
887 async fn wait_for_drain(plugin: &LoadedPlugin, grace: Duration) -> Result<()> {
888 let deadline = tokio::time::Instant::now() + grace;
889 while plugin.call_tracker.active_calls() != 0 {
890 if tokio::time::Instant::now() >= deadline {
891 return Err(NylonRingHostError::DrainTimeout {
892 remaining: plugin.call_tracker.active_calls(),
893 });
894 }
895 tokio::time::sleep(Duration::from_millis(1)).await;
896 }
897 Ok(())
898 }
899
900 pub fn plugin(&self, name: &str) -> Option<PluginHandle> {
902 self.plugins
903 .get(name)
904 .map(|p| PluginHandle { plugin: p.clone() })
905 }
906
907 pub fn metrics(&self) -> HostMetrics {
909 HostMetrics {
910 loaded_plugins: self.plugins.len(),
911 pending_requests: self.host_ctx.pending_count(),
912 state_sessions: self.host_ctx.state_count(),
913 in_flight_calls: self
914 .plugins
915 .values()
916 .map(|plugin| plugin.call_tracker.active_calls())
917 .sum(),
918 }
919 }
920
921 pub unsafe fn get_host_ext(host_ctx: *mut c_void) -> *const NrHostExt {
928 if host_ctx.is_null() {
929 return std::ptr::null();
930 }
931 let ctx = unsafe { &*host_ctx.cast::<HostContext>() };
932 &ctx.host_ext as *const NrHostExt
933 }
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939
940 struct WakeFlag(Arc<std::sync::atomic::AtomicBool>);
941
942 impl std::task::Wake for WakeFlag {
943 fn wake(self: Arc<Self>) {
944 self.0.store(true, std::sync::atomic::Ordering::Release);
945 }
946
947 fn wake_by_ref(self: &Arc<Self>) {
948 self.0.store(true, std::sync::atomic::Ordering::Release);
949 }
950 }
951
952 struct ReentrantWakerState {
953 host_ctx: Arc<HostContext>,
954 clones: Arc<std::sync::atomic::AtomicUsize>,
955 drops: Arc<std::sync::atomic::AtomicUsize>,
956 }
957
958 unsafe fn clone_reentrant_waker(data: *const ()) -> std::task::RawWaker {
959 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
960 state.host_ctx.pending_count();
961 state
962 .clones
963 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
964 let clone = state.clone();
965 let _ = Arc::into_raw(state);
966 std::task::RawWaker::new(Arc::into_raw(clone).cast(), &REENTRANT_WAKER_VTABLE)
967 }
968
969 unsafe fn wake_reentrant_waker(data: *const ()) {
970 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
971 state.host_ctx.pending_count();
972 }
973
974 unsafe fn wake_reentrant_waker_by_ref(data: *const ()) {
975 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
976 state.host_ctx.pending_count();
977 let _ = Arc::into_raw(state);
978 }
979
980 unsafe fn drop_reentrant_waker(data: *const ()) {
981 let state = unsafe { Arc::<ReentrantWakerState>::from_raw(data.cast()) };
982 state.host_ctx.pending_count();
983 state
984 .drops
985 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
986 }
987
988 static REENTRANT_WAKER_VTABLE: std::task::RawWakerVTable = std::task::RawWakerVTable::new(
989 clone_reentrant_waker,
990 wake_reentrant_waker,
991 wake_reentrant_waker_by_ref,
992 drop_reentrant_waker,
993 );
994
995 fn reentrant_waker(
996 host_ctx: Arc<HostContext>,
997 clones: Arc<std::sync::atomic::AtomicUsize>,
998 drops: Arc<std::sync::atomic::AtomicUsize>,
999 ) -> std::task::Waker {
1000 let state = Arc::new(ReentrantWakerState {
1001 host_ctx,
1002 clones,
1003 drops,
1004 });
1005 let raw = std::task::RawWaker::new(Arc::into_raw(state).cast(), &REENTRANT_WAKER_VTABLE);
1006 unsafe { std::task::Waker::from_raw(raw) }
1007 }
1008
1009 fn plugin_test_lock() -> std::sync::MutexGuard<'static, ()> {
1015 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1016 LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
1017 }
1018
1019 fn example_plugin_path() -> Option<std::path::PathBuf> {
1020 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1021 .parent()?
1022 .parent()?;
1023 let manifest = workspace_root.join("examples/ex-nyring-plugin/Cargo.toml");
1024 if !manifest.is_file() {
1025 return None;
1026 }
1027 let status = std::process::Command::new("cargo")
1028 .args(["build", "--release", "--manifest-path"])
1029 .arg(&manifest)
1030 .status()
1031 .ok()?;
1032 if !status.success() {
1033 return None;
1034 }
1035 let filename = if cfg!(target_os = "macos") {
1036 "libex_nyring_plugin.dylib"
1037 } else if cfg!(target_os = "windows") {
1038 "ex_nyring_plugin.dll"
1039 } else {
1040 "libex_nyring_plugin.so"
1041 };
1042 Some(workspace_root.join("target/release").join(filename))
1043 }
1044
1045 fn c_plugin_path() -> Option<std::path::PathBuf> {
1046 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1047 .parent()?
1048 .parent()?;
1049 let source = workspace_root.join("examples/c-plugin/plugin.c");
1050 if !source.is_file() {
1051 return None;
1052 }
1053 let extension = if cfg!(target_os = "macos") {
1054 "dylib"
1055 } else if cfg!(target_os = "windows") {
1056 "dll"
1057 } else {
1058 "so"
1059 };
1060 let output = workspace_root
1061 .join("target/c-example")
1062 .join(format!("libnylon_ring_c_example.{extension}"));
1063 std::fs::create_dir_all(output.parent()?).ok()?;
1064 let status = std::process::Command::new("cc")
1065 .args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-shared"])
1066 .args((!cfg!(target_os = "windows")).then_some("-fPIC"))
1067 .arg(&source)
1068 .arg("-o")
1069 .arg(&output)
1070 .status()
1071 .ok()?;
1072 status.success().then_some(output)
1073 }
1074
1075 #[test]
1076 fn dropping_pending_guard_unregisters_unary_request() {
1077 let host = NylonRingHost::new();
1078 let sid = 42;
1079 context::insert_pending(
1080 &host.host_ctx,
1081 sid,
1082 types::Pending::Unary(types::UnaryPending::waiting()),
1083 );
1084 host.host_ctx.set_state(sid, "key".into(), vec![1]);
1085
1086 drop(PendingGuard::new(&host.host_ctx, sid));
1087
1088 assert!(context::remove_pending(&host.host_ctx, sid).is_none());
1089 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
1090 }
1091
1092 #[test]
1093 fn unary_completion_wakes_latest_waiter_across_threads() {
1094 let host = NylonRingHost::new();
1095 let sid = 46;
1096 context::insert_pending(
1097 &host.host_ctx,
1098 sid,
1099 types::Pending::Unary(types::UnaryPending::waiting()),
1100 );
1101 host.host_ctx.set_state(sid, "key".into(), vec![1]);
1102
1103 let first_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
1104 let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
1105 let first_waker = std::task::Waker::from(Arc::new(WakeFlag(first_woken.clone())));
1106 let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken.clone())));
1107 let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
1108
1109 let mut first_context = std::task::Context::from_waker(&first_waker);
1110 assert!(matches!(
1111 std::future::Future::poll(future.as_mut(), &mut first_context),
1112 std::task::Poll::Pending
1113 ));
1114 let mut second_context = std::task::Context::from_waker(&second_waker);
1115 assert!(matches!(
1116 std::future::Future::poll(future.as_mut(), &mut second_context),
1117 std::task::Poll::Pending
1118 ));
1119
1120 let host_ctx = host.host_ctx.clone();
1121 let dispatch_status = std::thread::spawn(move || {
1122 context::dispatch_pending(
1123 &host_ctx,
1124 sid,
1125 NrStatus::Ok,
1126 types::ResponsePayload::Owned(vec![7]),
1127 )
1128 })
1129 .join()
1130 .unwrap();
1131 assert_eq!(dispatch_status, NrStatus::Ok);
1132 assert!(!first_woken.load(std::sync::atomic::Ordering::Acquire));
1133 assert!(second_woken.load(std::sync::atomic::Ordering::Acquire));
1134 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
1135 assert_eq!(host.metrics().pending_requests, 1);
1136
1137 assert_eq!(
1138 context::dispatch_pending(
1139 &host.host_ctx,
1140 sid,
1141 NrStatus::Ok,
1142 types::ResponsePayload::Owned(vec![8]),
1143 ),
1144 NrStatus::Invalid
1145 );
1146
1147 match std::future::Future::poll(future.as_mut(), &mut second_context) {
1148 std::task::Poll::Ready(Some((status, data))) => {
1149 assert_eq!(status, NrStatus::Ok);
1150 assert_eq!(data.as_slice(), [7]);
1151 }
1152 result => panic!("unexpected unary completion result: {result:?}"),
1153 }
1154 assert_eq!(host.metrics().pending_requests, 0);
1155 }
1156
1157 #[test]
1158 fn unary_waker_callbacks_run_outside_pending_lock() {
1159 let host = NylonRingHost::new();
1160 let sid = 47;
1161 context::insert_pending(
1162 &host.host_ctx,
1163 sid,
1164 types::Pending::Unary(types::UnaryPending::waiting()),
1165 );
1166
1167 let clones = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1168 let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1169 let first_waker = reentrant_waker(host.host_ctx.clone(), clones.clone(), drops.clone());
1170 let mut future = Box::pin(context::wait_for_unary(&host.host_ctx, sid));
1171 {
1172 let mut context = std::task::Context::from_waker(&first_waker);
1173 assert!(matches!(
1174 std::future::Future::poll(future.as_mut(), &mut context),
1175 std::task::Poll::Pending
1176 ));
1177 }
1178 assert_eq!(clones.load(std::sync::atomic::Ordering::Relaxed), 1);
1179
1180 let second_woken = Arc::new(std::sync::atomic::AtomicBool::new(false));
1181 let second_waker = std::task::Waker::from(Arc::new(WakeFlag(second_woken)));
1182 let mut context = std::task::Context::from_waker(&second_waker);
1183 assert!(matches!(
1184 std::future::Future::poll(future.as_mut(), &mut context),
1185 std::task::Poll::Pending
1186 ));
1187 assert_eq!(drops.load(std::sync::atomic::Ordering::Relaxed), 1);
1188
1189 drop(future);
1190 context::cleanup_sid(&host.host_ctx, sid);
1191 }
1192
1193 #[test]
1194 fn dropping_stream_receiver_unregisters_stream() {
1195 let host = NylonRingHost::new();
1196 let sid = 43;
1197 let (tx, rx) = stream_channel::acquire(1);
1198 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
1199 host.host_ctx.set_state(sid, "key".into(), vec![1]);
1200
1201 drop(StreamReceiver::new(
1202 rx,
1203 Some(host.host_ctx.clone()),
1204 sid,
1205 None,
1206 ));
1207
1208 assert!(context::remove_pending(&host.host_ctx, sid).is_none());
1209 assert!(!host.host_ctx.state_per_sid.contains_key(&sid));
1210 }
1211
1212 #[test]
1213 fn fast_slot_reentry_is_rejected_and_binding_is_cleared() {
1214 let mut first = types::UnaryResultSlot {
1215 sid: 1,
1216 result: None,
1217 lease: None,
1218 };
1219 let mut second = types::UnaryResultSlot {
1220 sid: 2,
1221 result: None,
1222 lease: None,
1223 };
1224 let binding = FastSlotBinding::bind(&mut first).unwrap();
1225 assert!(matches!(
1226 FastSlotBinding::bind(&mut second),
1227 Err(NylonRingHostError::FastPathReentrant)
1228 ));
1229 drop(binding);
1230 assert!(FastSlotBinding::bind(&mut second).is_ok());
1231 }
1232
1233 #[test]
1234 fn bounded_stream_reports_backpressure_and_removes_terminal_once() {
1235 let host = NylonRingHost::with_stream_capacity(1);
1236 let sid = 44;
1237 let (tx, mut rx) = stream_channel::acquire(1);
1238 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
1239
1240 assert_eq!(
1241 context::dispatch_pending(
1242 &host.host_ctx,
1243 sid,
1244 NrStatus::Ok,
1245 types::ResponsePayload::Owned(vec![1]),
1246 ),
1247 NrStatus::Ok
1248 );
1249 assert_eq!(
1250 context::dispatch_pending(
1251 &host.host_ctx,
1252 sid,
1253 NrStatus::Ok,
1254 types::ResponsePayload::Owned(vec![2]),
1255 ),
1256 NrStatus::Backpressure
1257 );
1258 assert_eq!(rx.try_recv().unwrap().data, vec![1]);
1259 assert_eq!(
1260 context::dispatch_pending(
1261 &host.host_ctx,
1262 sid,
1263 NrStatus::StreamEnd,
1264 types::ResponsePayload::Owned(vec![]),
1265 ),
1266 NrStatus::Ok
1267 );
1268 assert_eq!(host.metrics().pending_requests, 0);
1269 assert_eq!(rx.try_recv().unwrap().status, NrStatus::StreamEnd);
1270 }
1271
1272 #[test]
1273 fn concurrent_terminal_frames_complete_stream_once() {
1274 let host = NylonRingHost::with_stream_capacity(2);
1275 let sid = 45;
1276 let (tx, mut rx) = stream_channel::acquire(2);
1277 context::insert_pending(&host.host_ctx, sid, types::Pending::Stream(tx));
1278 let barrier = Arc::new(std::sync::Barrier::new(3));
1279
1280 let results: Vec<_> = std::thread::scope(|scope| {
1281 let handles: Vec<_> = (0..2)
1282 .map(|value| {
1283 let ctx = host.host_ctx.clone();
1284 let barrier = barrier.clone();
1285 scope.spawn(move || {
1286 barrier.wait();
1287 context::dispatch_pending(
1288 &ctx,
1289 sid,
1290 NrStatus::StreamEnd,
1291 types::ResponsePayload::Owned(vec![value]),
1292 )
1293 })
1294 })
1295 .collect();
1296 barrier.wait();
1297 handles
1298 .into_iter()
1299 .map(|handle| handle.join().unwrap())
1300 .collect()
1301 });
1302
1303 assert_eq!(
1304 results
1305 .iter()
1306 .filter(|&&status| status == NrStatus::Ok)
1307 .count(),
1308 1
1309 );
1310 assert_eq!(
1311 results
1312 .iter()
1313 .filter(|&&status| status == NrStatus::Invalid)
1314 .count(),
1315 1
1316 );
1317 assert!(rx.try_recv().is_some());
1318 assert!(rx.try_recv().is_none());
1319 assert_eq!(host.metrics().pending_requests, 0);
1320 }
1321
1322 #[test]
1323 fn unload_defers_library_drop_and_rejects_new_calls_on_existing_handle() {
1324 let _plugin_lock = plugin_test_lock();
1325 let Some(path) = example_plugin_path() else {
1326 return;
1327 };
1328 let mut host = NylonRingHost::new();
1329 host.load("example", path.to_str().unwrap()).unwrap();
1330 let handle = host.plugin("example").unwrap();
1331 let runtime = tokio::runtime::Runtime::new().unwrap();
1332 assert!(matches!(
1333 runtime.block_on(handle.call_response_timeout(
1334 "benchmark_without_response",
1335 b"",
1336 Duration::from_millis(1),
1337 )),
1338 Err(NylonRingHostError::Timeout)
1339 ));
1340 assert_eq!(host.metrics().pending_requests, 0);
1341
1342 let guard = handle.plugin.begin_call().unwrap();
1343 assert_eq!(host.metrics().in_flight_calls, 1);
1344
1345 host.unload("example").unwrap();
1346 assert!(matches!(
1347 runtime.block_on(handle.call("benchmark_without_response", b"")),
1348 Err(NylonRingHostError::PluginUnloaded)
1349 ));
1350 drop(guard);
1351 drop(handle);
1352 }
1353
1354 #[test]
1355 fn stream_receiver_outlives_unload_and_drains_frames() {
1356 let _plugin_lock = plugin_test_lock();
1357 let Some(path) = example_plugin_path() else {
1358 return;
1359 };
1360 let mut host = NylonRingHost::new();
1361 host.load("example", path.to_str().unwrap()).unwrap();
1362 let handle = host.plugin("example").unwrap();
1363 let runtime = tokio::runtime::Runtime::new().unwrap();
1364
1365 let (_sid, mut receiver) = runtime
1366 .block_on(handle.call_stream("benchmark_stream", b""))
1367 .unwrap();
1368 host.unload("example").unwrap();
1369 drop(handle);
1370
1371 let frames = runtime.block_on(async {
1374 let mut frames = 0u32;
1375 while receiver.recv().await.is_some() {
1376 frames += 1;
1377 }
1378 frames
1379 });
1380 assert_eq!(frames, 9);
1381 drop(receiver);
1382 }
1383
1384 #[test]
1385 fn stream_receiver_outlives_host_drop() {
1386 let _plugin_lock = plugin_test_lock();
1387 let Some(path) = example_plugin_path() else {
1388 return;
1389 };
1390 let mut host = NylonRingHost::new();
1391 host.load("example", path.to_str().unwrap()).unwrap();
1392 let handle = host.plugin("example").unwrap();
1393 let runtime = tokio::runtime::Runtime::new().unwrap();
1394
1395 let (_sid, mut receiver) = runtime
1396 .block_on(handle.call_stream("benchmark_stream", b""))
1397 .unwrap();
1398 drop(host);
1399 drop(handle);
1400
1401 let frames = runtime.block_on(async {
1402 let mut frames = 0u32;
1403 while receiver.recv().await.is_some() {
1404 frames += 1;
1405 }
1406 frames
1407 });
1408 assert_eq!(frames, 9);
1409 }
1410
1411 #[test]
1412 fn owned_response_round_trips_and_releases_exactly_once() {
1413 let _plugin_lock = plugin_test_lock();
1414 let Some(path) = example_plugin_path() else {
1415 return;
1416 };
1417 let mut host = NylonRingHost::new();
1418 host.load("example", path.to_str().unwrap()).unwrap();
1419 let handle = host.plugin("example").unwrap();
1420 let runtime = tokio::runtime::Runtime::new().unwrap();
1421
1422 async fn release_count(handle: &PluginHandle) -> u64 {
1423 let (status, data) = handle
1424 .call_response("owned_release_count", b"")
1425 .await
1426 .unwrap();
1427 assert_eq!(status, NrStatus::Ok);
1428 u64::from_le_bytes(data.try_into().unwrap())
1429 }
1430
1431 runtime.block_on(async {
1432 let (status, data) = handle
1434 .call_response_bytes("benchmark_owned", &[0u8; 128])
1435 .await
1436 .unwrap();
1437 assert_eq!(status, NrStatus::Ok);
1438 assert_eq!(data.len(), 128);
1439 assert!(data.iter().all(|&byte| byte == 42));
1440
1441 let before = release_count(&handle).await;
1442
1443 let (status, held) = handle
1445 .call_response_bytes("echo_owned", b"held")
1446 .await
1447 .unwrap();
1448 assert_eq!(status, NrStatus::Ok);
1449 assert_eq!(&*held, b"held");
1450 assert_eq!(release_count(&handle).await, before);
1451 drop(held);
1452 assert_eq!(release_count(&handle).await, before + 1);
1453
1454 let (status, data) = handle.call_response("echo_owned", b"copied").await.unwrap();
1456 assert_eq!(status, NrStatus::Ok);
1457 assert_eq!(data, b"copied");
1458 assert_eq!(release_count(&handle).await, before + 2);
1459
1460 let (_, view) = handle
1462 .call_response_bytes("echo_owned", b"vec")
1463 .await
1464 .unwrap();
1465 assert_eq!(view.into_vec(), b"vec");
1466 assert_eq!(release_count(&handle).await, before + 3);
1467
1468 let (status, data) = handle
1470 .call_response_fast("echo_owned", b"fast")
1471 .await
1472 .unwrap();
1473 assert_eq!(status, NrStatus::Ok);
1474 assert_eq!(data, b"fast");
1475 assert_eq!(release_count(&handle).await, before + 4);
1476 });
1477 }
1478
1479 #[test]
1480 fn owned_response_keeps_plugin_alive_after_unload() {
1481 let _plugin_lock = plugin_test_lock();
1482 let Some(path) = example_plugin_path() else {
1483 return;
1484 };
1485 let mut host = NylonRingHost::new();
1486 host.load("example", path.to_str().unwrap()).unwrap();
1487 let handle = host.plugin("example").unwrap();
1488 let runtime = tokio::runtime::Runtime::new().unwrap();
1489
1490 let (status, response) = runtime
1491 .block_on(handle.call_response_bytes("benchmark_owned", &[0u8; 64]))
1492 .unwrap();
1493 assert_eq!(status, NrStatus::Ok);
1494
1495 host.unload("example").unwrap();
1498 drop(handle);
1499 drop(host);
1500 assert_eq!(response.len(), 64);
1501 assert!(response.iter().all(|&byte| byte == 42));
1502 drop(response);
1503 }
1504
1505 #[test]
1506 fn lease_round_trips_and_rejects_misuse() {
1507 let _plugin_lock = plugin_test_lock();
1508 let Some(path) = example_plugin_path() else {
1509 return;
1510 };
1511 let mut host = NylonRingHost::new();
1512 host.load("example", path.to_str().unwrap()).unwrap();
1513 let handle = host.plugin("example").unwrap();
1514 let runtime = tokio::runtime::Runtime::new().unwrap();
1515
1516 runtime.block_on(async {
1517 let (status, data) = handle
1519 .call_response("echo_lease", b"through the lease")
1520 .await
1521 .unwrap();
1522 assert_eq!(status, NrStatus::Ok);
1523 assert_eq!(data, b"through the lease");
1524
1525 let (status, data) = handle.call_response("echo_lease", b"").await.unwrap();
1527 assert_eq!(status, NrStatus::Ok);
1528 assert!(data.is_empty());
1529
1530 let (status, data) = handle
1532 .call_response_fast("echo_lease", b"fast lease")
1533 .await
1534 .unwrap();
1535 assert_eq!(status, NrStatus::Ok);
1536 assert_eq!(data, b"fast lease");
1537
1538 let (status, data) = handle
1542 .call_response("lease_misuse", b"contract")
1543 .await
1544 .unwrap();
1545 assert_eq!(status, NrStatus::Ok);
1546 assert_eq!(data, b"contract");
1547 });
1548
1549 assert_eq!(host.host_ctx.orphaned_lease_count(), 0);
1551 assert_eq!(host.host_ctx.pending_count(), 0);
1552 }
1553
1554 #[test]
1555 fn abandoned_leases_are_parked_not_freed() {
1556 let _plugin_lock = plugin_test_lock();
1557 let Some(path) = example_plugin_path() else {
1558 return;
1559 };
1560 let mut host = NylonRingHost::new();
1561 host.load("example", path.to_str().unwrap()).unwrap();
1562 let handle = host.plugin("example").unwrap();
1563 let runtime = tokio::runtime::Runtime::new().unwrap();
1564
1565 runtime.block_on(async {
1566 let result = handle
1570 .call_response_timeout("lease_abandon", b"", std::time::Duration::from_millis(50))
1571 .await;
1572 assert!(matches!(result, Err(NylonRingHostError::Timeout)));
1573 assert_eq!(host.host_ctx.orphaned_lease_count(), 1);
1574 assert_eq!(host.host_ctx.pending_count(), 0);
1575
1576 let result = handle.call_response_fast("lease_abandon", b"").await;
1578 assert!(matches!(
1579 result,
1580 Err(NylonRingHostError::MissingSynchronousResponse)
1581 ));
1582 assert_eq!(host.host_ctx.orphaned_lease_count(), 2);
1583 });
1584 }
1585
1586 #[test]
1587 fn example_plugin_fire_and_forget_entry_returns_ok() {
1588 let _plugin_lock = plugin_test_lock();
1589 let Some(path) = example_plugin_path() else {
1590 return;
1591 };
1592 let mut host = NylonRingHost::new();
1593 host.load("example", path.to_str().unwrap()).unwrap();
1594 let handle = host.plugin("example").unwrap();
1595 let runtime = tokio::runtime::Runtime::new().unwrap();
1596
1597 assert!(matches!(
1598 runtime.block_on(handle.call("notify", b"fire and forget")),
1599 Ok(NrStatus::Ok)
1600 ));
1601 }
1602
1603 #[test]
1604 fn c_plugin_layout_round_trips_through_host() {
1605 let _plugin_lock = plugin_test_lock();
1606 let Some(path) = c_plugin_path() else {
1607 return;
1608 };
1609 let mut host = NylonRingHost::new();
1610 host.load("c-example", path.to_str().unwrap()).unwrap();
1611 let handle = host.plugin("c-example").unwrap();
1612 let runtime = tokio::runtime::Runtime::new().unwrap();
1613 let (status, response) = runtime
1614 .block_on(handle.call_response("echo", b"from-c"))
1615 .unwrap();
1616 assert_eq!(status, NrStatus::Ok);
1617 assert_eq!(response, b"from-c");
1618
1619 assert!(matches!(
1622 handle.entry("echo"),
1623 Err(NylonRingHostError::EntryDispatchUnsupported)
1624 ));
1625 }
1626
1627 #[test]
1628 fn entry_id_dispatch_round_trips_all_call_shapes() {
1629 let _plugin_lock = plugin_test_lock();
1630 let Some(path) = example_plugin_path() else {
1631 return;
1632 };
1633 let mut host = NylonRingHost::new();
1634 host.load("example", path.to_str().unwrap()).unwrap();
1635 let handle = host.plugin("example").unwrap();
1636 let runtime = tokio::runtime::Runtime::new().unwrap();
1637
1638 assert!(matches!(
1639 handle.entry("no_such_entry"),
1640 Err(NylonRingHostError::EntryNotFound(_))
1641 ));
1642
1643 let echo = handle.entry("benchmark").unwrap();
1644 let notify = handle.entry("benchmark_without_response").unwrap();
1645 let stream = handle.entry("benchmark_stream").unwrap();
1646
1647 runtime.block_on(async {
1648 let (status, data) = echo.call_response(b"by-id").await.unwrap();
1649 assert_eq!(status, NrStatus::Ok);
1650 assert_eq!(data, b"by-id");
1651
1652 let (status, data) = echo.call_response_fast(b"fast-by-id").await.unwrap();
1653 assert_eq!(status, NrStatus::Ok);
1654 assert_eq!(data, b"fast-by-id");
1655
1656 assert_eq!(notify.call(b"").await.unwrap(), NrStatus::Ok);
1657
1658 let (_sid, mut receiver) = stream.call_stream(b"").await.unwrap();
1659 let mut frames = 0u32;
1660 while receiver.recv().await.is_some() {
1661 frames += 1;
1662 }
1663 assert_eq!(frames, 9);
1664 });
1665 }
1666}