23
//! use async_trait::async_trait;
24
//! use netdata_plugin_error::Result;
25
//! use netdata_plugin_protocol::FunctionDeclaration;
26
-//! use rt::{FunctionHandler, PluginRuntime};
26
+//! use rt::{FunctionCallContext, FunctionHandler, PluginRuntime};
27
//! use serde::{Deserialize, Serialize};
28
//!
29
//! #[derive(Deserialize)]
43
//! type Request = MyRequest;
44
//! type Response = MyResponse;
45
//!
46
-//! async fn on_call(&self, request: Self::Request) -> Result<Self::Response> {
46
+//! async fn on_call(
47
+//! &self,
48
+//! _ctx: FunctionCallContext,
49
+//! request: Self::Request,
50
+//! ) -> Result<Self::Response> {
51
//! Ok(MyResponse {
52
//! greeting: format!("Hello, {}!", request.name),
53
//! })
54
//! }
55
//!
52
-//! async fn on_cancellation(&self) -> Result<Self::Response> {
53
-//! Err(netdata_plugin_error::NetdataPluginError::Other {
54
-//! message: "Operation cancelled".to_string(),
55
-//! })
56
-//! }
57
-//!
58
-//! async fn on_progress(&self) {
59
-//! // Report progress if needed
60
-//! }
61
-//!
56
//! fn declaration(&self) -> FunctionDeclaration {
57
//! FunctionDeclaration::new("greet", "A greeting function")
58
//! }
91
use futures::stream::FuturesUnordered;
92
use netdata_plugin_error::Result;
93
use netdata_plugin_protocol::{
100
- FunctionCall, FunctionCancel, FunctionDeclaration, FunctionProgress, FunctionResult, Message,
101
- MessageReader, MessageWriter,
94
+ FunctionCall, FunctionCancel, FunctionDeclaration, FunctionProgressRequest,
95
+ FunctionProgressResponse, FunctionResult, Message, MessageReader, MessageWriter,
96
};
97
use serde::Serialize;
98
use serde::de::DeserializeOwned;
99
use serde_json::json;
100
use std::collections::HashMap;
101
use std::sync::Arc;
102
+use std::sync::atomic::{AtomicUsize, Ordering};
103
use std::time::Duration;
104
use tokio::io::{AsyncRead, AsyncWrite};
105
use tokio::sync::{Mutex, mpsc};
106
use tokio_util::sync::CancellationToken;
112
-use tracing::{debug, error, info, instrument, warn};
107
+use tracing::{error, info, instrument, trace, warn};
108
109
// Charts module and re-exports
110
pub mod charts;
126
mod tracing_setup;
127
pub use tracing_setup::init_tracing;
128
134
-// Re-export foundational utilities
135
-pub use foundation::Timeout;
129
+/// Atomic progress state shared between handlers and the runtime ticker.
130
+///
131
+/// Handlers write counters from any context (async, `spawn_blocking`, rayon),
132
+/// and the runtime sends progress to the agent once per second.
133
+///
134
+/// # Example
135
+///
136
+/// ```ignore
137
+/// // Set total work items before handing the counter to workers.
138
+/// ctx.progress.set_total(files.len());
139
+///
140
+/// // Give the done counter to a rayon/blocking worker.
141
+/// let counter = ctx.progress.done_counter();
142
+/// rayon::spawn(move || {
143
+/// // ... process item ...
144
+/// counter.fetch_add(1, Ordering::Relaxed);
145
+/// });
146
+///
147
+/// // Or update both at once from async code.
148
+/// ctx.progress.update(done, total);
149
+/// ```
150
+#[derive(Clone)]
151
+pub struct ProgressState {
152
+ done: Arc<AtomicUsize>,
153
+ total: Arc<AtomicUsize>,
154
+}
155
+
156
+impl ProgressState {
157
+ fn new() -> Self {
158
+ Self {
159
+ done: Arc::new(AtomicUsize::new(0)),
160
+ total: Arc::new(AtomicUsize::new(0)),
161
+ }
162
+ }
163
+
164
+ /// Update both done and total. Safe from any context.
165
+ pub fn update(&self, done: usize, total: usize) {
166
+ self.done.store(done, Ordering::Relaxed);
167
+ self.total.store(total, Ordering::Relaxed);
168
+ }
169
+
170
+ /// Set the total work items (e.g. before handing `done_counter` to workers).
171
+ pub fn set_total(&self, total: usize) {
172
+ self.total.store(total, Ordering::Relaxed);
173
+ }
174
+
175
+ /// Get a clone of the done counter for sharing with worker threads.
176
+ /// Workers call `counter.fetch_add(1, Ordering::Relaxed)` directly.
177
+ pub fn done_counter(&self) -> Arc<AtomicUsize> {
178
+ self.done.clone()
179
+ }
180
+
181
+ fn load(&self) -> (usize, usize) {
182
+ (
183
+ self.done.load(Ordering::Relaxed),
184
+ self.total.load(Ordering::Relaxed),
185
+ )
186
+ }
187
+}
188
+
189
+/// Context provided to function handlers during execution.
190
+///
191
+/// Contains the transaction identifier, atomic progress state, and a
192
+/// cancellation token that signals when the function should stop.
193
+pub struct FunctionCallContext {
194
+ /// Unique identifier for this function call.
195
+ transaction: String,
196
+ /// Atomic progress state. The runtime reads these counters once per
197
+ /// second and sends progress to the agent automatically.
198
+ pub progress: ProgressState,
199
+ /// Token that signals when the function should stop.
200
+ /// Check `is_cancelled()` in sync code, or `await cancelled()` in async code.
201
+ pub cancellation: CancellationToken,
202
+}
203
137
-/// Internal control signals sent to running functions.
138
-enum RuntimeSignal {
139
- /// Signal to request progress update from a running function.
140
- Progress,
204
+impl FunctionCallContext {
205
+ /// Returns the transaction identifier for this function call.
206
+ pub fn transaction(&self) -> &str {
207
+ &self.transaction
208
+ }
209
}
210
211
/// Represents an active function call transaction.
212
///
213
/// Each transaction tracks a single function invocation, including its
146
-/// unique identifier, control channel for signals, and cancellation token.
214
+/// unique identifier and cancellation token.
215
struct Transaction {
216
/// Unique identifier for this transaction.
217
id: String,
150
- /// Channel for sending control signals to the running function.
151
- control_tx: mpsc::Sender<RuntimeSignal>,
218
/// Token for cancelling this specific function execution.
219
cancellation_token: CancellationToken,
220
}
221
156
-/// Execution context provided to function handlers.
222
+/// Execution context provided to the handler adapter layer.
223
///
158
-/// Contains all the information and control mechanisms needed for
159
-/// a function to execute, handle cancellation, and report progress.
224
+/// Contains all the information needed for a function to execute.
225
struct FunctionContext {
226
/// The original function call request from Netdata.
227
function_call: Box<FunctionCall>,
228
/// Token for detecting cancellation requests.
229
cancellation_token: CancellationToken,
165
- /// Receiver for runtime control signals (e.g., progress requests).
166
- signal_rx: Mutex<mpsc::Receiver<RuntimeSignal>>,
230
+ /// Sender for outbound messages (e.g., progress reports back to the agent).
231
+ outbound_tx: mpsc::UnboundedSender<Message>,
232
}
233
234
/// Type alias for a future that produces a function result.
270
/// type Request = AddRequest;
271
/// type Response = AddResponse;
272
///
208
-/// async fn on_call(&self, request: Self::Request) -> Result<Self::Response> {
273
+/// async fn on_call(
274
+/// &self,
275
+/// _ctx: FunctionCallContext,
276
+/// request: Self::Request,
277
+/// ) -> Result<Self::Response> {
278
/// Ok(AddResponse {
279
/// sum: request.a + request.b,
280
/// })
281
/// }
282
///
214
-/// async fn on_cancellation(&self) -> Result<Self::Response> {
215
-/// Err(netdata_plugin_error::NetdataPluginError::Other {
216
-/// message: "Addition cancelled".to_string(),
217
-/// })
218
-/// }
219
-///
220
-/// async fn on_progress(&self) {
221
-/// // Not needed for quick operations
222
-/// }
223
-///
283
/// fn declaration(&self) -> FunctionDeclaration {
284
/// FunctionDeclaration::new("add", "Adds two numbers")
285
/// }
302
/// Main function logic executed when the function is called.
303
///
304
/// This method contains the primary computation or operation that the
246
- /// function performs. It receives the deserialized request and should
247
- /// return either a successful response or an error.
305
+ /// function performs. It receives the deserialized request, a context
306
+ /// for progress reporting and cancellation, and should return either
307
+ /// a successful response or an error.
308
///
309
/// # Arguments
310
///
311
+ /// * `ctx` - Context with transaction ID, progress sender, and cancellation token
312
/// * `request` - The deserialized request payload
313
///
314
/// # Returns
317
///
318
/// # Cancellation
319
///
259
- /// This method may be interrupted if a cancellation is requested.
260
- /// When cancelled, the runtime will call [`on_cancellation`](Self::on_cancellation) instead.
261
- async fn on_call(&self, transaction: String, request: Self::Request) -> Result<Self::Response>;
262
-
263
- /// Handle cancellation requests while the function is running.
264
- ///
265
- /// Called when Netdata requests cancellation of a running function.
266
- /// This method should quickly return an appropriate error or partial result.
267
- ///
268
- /// # Returns
269
- ///
270
- /// Typically returns an error indicating the operation was cancelled,
271
- /// but may return a partial result if appropriate.
272
- async fn on_cancellation(&self, transaction: String) -> Result<Self::Response>;
273
-
274
- /// Handle progress report requests while the function is running.
275
- ///
276
- /// Called when Netdata requests a progress update from a long-running function.
277
- /// This method should log or report the current progress but doesn't need
278
- /// to return a value (progress is typically reported through logging).
279
- ///
280
- /// # Note
281
- ///
282
- /// This is called asynchronously while `on_call` is still running,
283
- /// so any shared state must be properly synchronized.
284
- async fn on_progress(&self, transaction: String);
320
+ /// When cancelled, the runtime cancels the token in `ctx.cancellation`
321
+ /// and drops this future. Check `ctx.cancellation.is_cancelled()` in
322
+ /// synchronous code paths.
323
+ async fn on_call(
324
+ &self,
325
+ ctx: FunctionCallContext,
326
+ request: Self::Request,
327
+ ) -> Result<Self::Response>;
328
329
/// Provide the function's declaration metadata.
330
///
405
},
406
};
407
365
- // Drive the handler with cancellation and progress handling
366
- let handler = self.handler.clone();
367
-
368
- let mut call_future = Box::pin(handler.on_call(transaction.clone(), payload));
369
- let mut signal_rx = ctx.signal_rx.lock().await;
408
+ // Build the function call context
409
+ let call_ctx = FunctionCallContext {
410
+ transaction: transaction.clone(),
411
+ progress: ProgressState::new(),
412
+ cancellation: ctx.cancellation_token.clone(),
413
+ };
414
371
- let result = loop {
372
- tokio::select! {
373
- // Poll the main computation
374
- result = &mut call_future => {
375
- break result;
376
- }
377
- // Handle progress requests
378
- Some(msg) = signal_rx.recv() => {
379
- match msg {
380
- RuntimeSignal::Progress => {
381
- handler.on_progress(transaction.clone()).await;
382
- }
415
+ // Spawn a background ticker that reads the atomic progress counters
416
+ // once per second and sends FunctionProgressResponse to the agent.
417
+ let progress = call_ctx.progress.clone();
418
+ let ticker_tx = ctx.outbound_tx.clone();
419
+ let ticker_transaction = transaction.clone();
420
+
421
+ let ticker = tokio::spawn(async move {
422
+ let mut interval = tokio::time::interval(Duration::from_secs(1));
423
+ interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
424
+ loop {
425
+ interval.tick().await;
426
+ let (done, total) = progress.load();
427
+ if total > 0 {
428
+ let msg =
429
+ Message::FunctionProgressResponse(Box::new(FunctionProgressResponse {
430
+ transaction: ticker_transaction.clone(),
431
+ done,
432
+ all: total,
433
+ }));
434
+ tracing::trace!(
435
+ "[{}] progress {}/{}",
436
+ ticker_transaction.clone(),
437
+ done,
438
+ total
439
+ );
440
+ if ticker_tx.send(msg).is_err() {
441
+ tracing::error!(
442
+ "[{}] outbound channel closed, stopping progress ticker",
443
+ ticker_transaction
444
+ );
445
+ break;
446
}
447
}
385
- // Handle cancellation
386
- _ = ctx.cancellation_token.cancelled() => {
387
- break handler.on_cancellation(transaction.clone()).await;
388
- }
448
+ }
449
+ });
450
+
451
+ let handler = self.handler.clone();
452
+
453
+ let result = tokio::select! {
454
+ result = handler.on_call(call_ctx, payload) => result,
455
+ _ = ctx.cancellation_token.cancelled() => {
456
+ Err(netdata_plugin_error::NetdataPluginError::Other {
457
+ message: "Function cancelled".to_string(),
458
+ })
459
}
460
};
461
462
+ ticker.abort();
463
+
464
let current_timestamp = std::time::SystemTime::now()
465
.duration_since(std::time::UNIX_EPOCH)
466
.expect("Time went backwards")
493
}
494
}
495
Err(e) => {
424
- error!("function handler error: {}", e);
496
+ if ctx.cancellation_token.is_cancelled() {
497
+ info!("function handler cancelled: {}", e);
498
+ } else {
499
+ error!("function handler error: {}", e);
500
+ }
501
let error_json = json!({
502
"error": format!("{}", e),
503
"status": 500
577
/// Token for initiating graceful shutdown.
578
shutdown_token: CancellationToken,
579
580
+ /// Sender for outbound messages (progress reports, function results).
581
+ outbound_tx: mpsc::UnboundedSender<Message>,
582
+ /// Receiver for outbound messages — consumed by the writer task.
583
+ outbound_rx: Option<mpsc::UnboundedReceiver<Message>>,
584
+
585
/// Optional chart registry for managing metrics emission.
586
chart_registry: Option<ChartRegistry<W>>,
587
/// Handle to chart registry background task.
647
/// }
648
/// ```
649
pub fn with_streams(name: &str, reader: R, writer: W) -> Self {
650
+ let (outbound_tx, outbound_rx) = mpsc::unbounded_channel();
651
+
652
Self {
653
plugin_name: String::from(name),
654
reader: MessageReader::new(reader),
659
futures: FuturesUnordered::new(),
660
661
shutdown_token: CancellationToken::default(),
662
+ outbound_tx,
663
+ outbound_rx: Some(outbound_rx),
664
chart_registry: None,
665
chart_registry_handle: None,
666
}
815
}
816
817
self.declare_functions().await?;
818
+
819
+ // Spawn a dedicated writer task so stdout I/O never blocks the
820
+ // main select loop (which must keep reading stdin).
821
+ let writer = Arc::clone(&self.writer);
822
+ let mut outbound_rx = self
823
+ .outbound_rx
824
+ .take()
825
+ .expect("outbound_rx consumed only once");
826
+
827
+ let writer_task = tokio::spawn(async move {
828
+ let mut keepalive =
829
+ tokio::time::interval(tokio::time::Duration::from_secs(60));
830
+
831
+ loop {
832
+ tokio::select! {
833
+ msg = outbound_rx.recv() => {
834
+ match msg {
835
+ Some(msg) => {
836
+ if let Err(e) = writer.lock().await.send(msg).await {
837
+ error!("outbound writer error: {}", e);
838
+ break;
839
+ }
840
+ }
841
+ None => break,
842
+ }
843
+ }
844
+ _ = keepalive.tick() => {
845
+ if let Err(e) = writer.lock().await.write_raw(b"PLUGIN_KEEPALIVE\n").await {
846
+ error!("keepalive write error: {}", e);
847
+ break;
848
+ }
849
+ }
850
+ }
851
+ }
852
+ });
853
+
854
self.process_messages().await?;
855
self.shutdown().await?;
856
857
+ // All outbound senders (including those in handler contexts) are now
858
+ // dropped, so the writer task will drain and exit.
859
+ drop(self);
860
+ let _ = writer_task.await;
861
+
862
Ok(())
863
}
864
931
932
loop {
933
tokio::select! {
808
- // Make the shutdown signal higher priority by putting it first
934
_ = self.shutdown_token.cancelled() => {
810
- info!("shutdown requested... Stop processing messages from stdin");
811
- // Reader will be dropped here, closing stdin
935
+ info!("shutdown requested, stop processing messages from stdin");
936
break;
937
}
938
+ Some((transaction, result)) = self.futures.next() => {
939
+ self.handle_completed(transaction, result).await?;
940
+ }
941
message = self.reader.next() => {
942
if self.handle_message(message).await? {
943
break;
944
}
945
}
819
- Some((transaction, result)) = self.futures.next() => {
820
- self.handle_completed(transaction, result).await?;
821
- }
946
}
947
}
948
962
Some(Ok(Message::FunctionCancel(function_cancel))) => {
963
self.handle_function_cancel(function_cancel.as_ref());
964
}
841
- Some(Ok(Message::FunctionProgress(function_progress))) => {
842
- self.handle_function_progress(&function_progress).await;
965
+ Some(Ok(Message::FunctionProgressRequest(req))) => {
966
+ trace!(transaction = %req.transaction, "ignoring inbound progress request");
967
}
968
Some(Ok(msg)) => {
845
- debug!("received message: {:?}", msg);
969
+ trace!("received message: {:?}", msg);
970
}
971
Some(Err(e)) => {
972
error!("error parsing message: {:?}", e);
974
None => {
975
info!("input stream ended");
976
self.shutdown_token.cancel();
853
- return Ok(true); // Signal to break the loop
977
+ return Ok(true);
978
}
979
}
980
+
981
Ok(false)
982
}
983
1032
};
1033
1034
// Create a new function context
910
- let (control_tx, control_rx) = mpsc::channel(4);
1035
let cancellation_token = CancellationToken::new();
1036
1037
let function_context = Arc::new(FunctionContext {
1038
function_call,
1039
cancellation_token: cancellation_token.clone(),
916
- signal_rx: Mutex::new(control_rx),
1040
+ outbound_tx: self.outbound_tx.clone(),
1041
});
1042
1043
// Create new transaction
1045
let transaction = Arc::new(Transaction {
1046
id,
1047
cancellation_token,
924
- control_tx,
1048
});
1049
self.transaction_registry
1050
.insert(transaction.id.clone(), transaction.clone());
1073
transaction.cancellation_token.cancel();
1074
}
1075
953
- /// Handle a progress report request.
954
- ///
955
- /// Sends a progress signal to the corresponding running function.
956
- async fn handle_function_progress(&mut self, function_progress: &FunctionProgress) {
957
- let Some(transaction) = self
958
- .transaction_registry
959
- .get(&function_progress.transaction)
960
- else {
961
- warn!(
962
- "can not get progress of non-existing transaction {}",
963
- function_progress.transaction
964
- );
965
- return;
966
- };
967
-
968
- info!(
969
- "requesting progress of transaction {}",
970
- function_progress.transaction
971
- );
972
- let _ = transaction.control_tx.send(RuntimeSignal::Progress).await;
973
- }
974
-
1076
/// Handle a completed function execution.
1077
///
977
- /// Removes the transaction from the registry and sends the result back to Netdata.
1078
+ /// Removes the transaction from the registry and sends the result
1079
+ /// through the outbound channel (written to stdout by the writer task).
1080
async fn handle_completed(
1081
&mut self,
1082
transaction: String,
1083
result: FunctionResult,
1084
) -> Result<()> {
1085
self.transaction_registry.remove(&transaction);
984
- self.writer
985
- .lock()
986
- .await
987
- .send(Message::FunctionResult(Box::new(result)))
988
- .await?;
1086
+ let msg = Message::FunctionResult(Box::new(result));
1087
+ if self.outbound_tx.send(msg).is_err() {
1088
+ error!(
1089
+ "outbound channel closed, cannot send result for transaction {}",
1090
+ transaction
1091
+ );
1092
+ }
1093
Ok(())
1094
}
1095