1use crate::internal::ffi;
5#[allow(unused_imports)]
6use crate::plugin::interface::PicoContext;
7use crate::plugin::interface::ServiceId;
8use crate::util::tarantool_error_to_box_error;
9use std::cell::Cell;
10use std::rc::Rc;
11use std::time::Duration;
12use tarantool::error::BoxError;
13use tarantool::error::TarantoolErrorCode;
14use tarantool::fiber;
15use tarantool::fiber::FiberId;
16use tarantool::fiber::{Channel, RecvError};
17use tarantool::util::IntoClones;
18
19#[track_caller]
21pub fn register_job<F>(service_id: &ServiceId, job: F) -> Result<(), BoxError>
22where
23 F: FnOnce(CancellationToken) + 'static,
24{
25 let loc = std::panic::Location::caller();
26 let tag = format!("{}:{}", loc.file(), loc.line());
27
28 register_tagged_job(service_id, job, &tag)
29}
30
31pub fn register_tagged_job<F>(service_id: &ServiceId, job: F, tag: &str) -> Result<(), BoxError>
33where
34 F: FnOnce(CancellationToken) + 'static,
35{
36 let (token, handle) = CancellationToken::new();
37 let finish_event = Rc::clone(&handle.finish_event);
38
39 let fiber_id = fiber::Builder::new()
40 .name(tag)
41 .func(move || {
42 job(token);
43 finish_event.signal();
44 })
45 .start_non_joinable()
46 .map_err(tarantool_error_to_box_error)?;
47
48 let plugin = &service_id.plugin;
49 let service = &service_id.service;
50 let version = &service_id.version;
51
52 let token = FfiBackgroundJobCancellationToken::new(
53 fiber_id,
54 handle.cancel_channel,
55 handle.finish_event,
56 );
57 register_background_job_cancellation_token(plugin, service, version, tag, token)?;
58
59 Ok(())
60}
61
62fn register_background_job_cancellation_token(
63 plugin: &str,
64 service: &str,
65 version: &str,
66 job_tag: &str,
67 token: FfiBackgroundJobCancellationToken,
68) -> Result<(), BoxError> {
69 let rc = unsafe {
71 ffi::pico_ffi_background_register_job_cancellation_token(
72 plugin.into(),
73 service.into(),
74 version.into(),
75 job_tag.into(),
76 token,
77 )
78 };
79
80 if rc != 0 {
81 return Err(BoxError::last());
82 }
83
84 Ok(())
85}
86
87#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
93#[repr(C)]
94pub struct JobCancellationResult {
95 pub n_total: u32,
97 pub n_timeouts: u32,
99}
100
101impl JobCancellationResult {
102 #[inline(always)]
103 pub fn new(n_total: u32, n_timeouts: u32) -> Self {
104 Self {
105 n_total,
106 n_timeouts,
107 }
108 }
109}
110
111#[inline(always)]
113pub fn cancel_jobs_by_tag(
114 service_id: &ServiceId,
115 job_tag: &str,
116 timeout: Duration,
117) -> Result<JobCancellationResult, BoxError> {
118 cancel_background_jobs_by_tag_inner(
119 &service_id.plugin,
120 &service_id.service,
121 &service_id.version,
122 job_tag,
123 timeout,
124 )
125}
126
127pub fn cancel_background_jobs_by_tag_inner(
129 plugin: &str,
130 service: &str,
131 version: &str,
132 job_tag: &str,
133 timeout: Duration,
134) -> Result<JobCancellationResult, BoxError> {
135 let mut result = JobCancellationResult::default();
136 let rc = unsafe {
138 ffi::pico_ffi_background_cancel_jobs_by_tag(
139 plugin.into(),
140 service.into(),
141 version.into(),
142 job_tag.into(),
143 timeout.as_secs_f64(),
144 &mut result,
145 )
146 };
147
148 if rc != 0 {
149 return Err(BoxError::last());
150 }
151
152 Ok(result)
153}
154
155#[derive(thiserror::Error, Debug)]
156pub enum Error {
157 #[error("Some of jobs are not fully completed, expected: {0}, completed: {1}")]
158 PartialCompleted(usize, usize),
159 #[error("timeout")]
160 CancellationTimeout,
161}
162
163#[non_exhaustive]
169#[derive(Debug)]
170pub struct CancellationToken {
171 cancel_channel: Channel<()>,
172}
173
174impl CancellationToken {
175 pub fn new() -> (CancellationToken, CancellationTokenHandle) {
179 let (cancel_tx, cancel_rx) = Channel::new(1).into_clones();
180 (
181 CancellationToken {
182 cancel_channel: cancel_rx,
183 },
184 CancellationTokenHandle {
185 cancel_channel: cancel_tx,
186 finish_event: Rc::new(OnceEvent::new()),
187 },
188 )
189 }
190
191 pub fn wait_timeout(&self, timeout: Duration) -> Result<(), Error> {
201 match self.cancel_channel.recv_timeout(timeout) {
202 Err(RecvError::Timeout) => Err(Error::CancellationTimeout),
203 _ => Ok(()),
204 }
205 }
206}
207
208#[derive(Debug)]
209pub struct CancellationTokenHandle {
210 cancel_channel: Channel<()>,
211 finish_event: Rc<OnceEvent>,
212}
213
214impl CancellationTokenHandle {
215 pub fn cancel(self) -> Rc<OnceEvent> {
219 let Self {
220 cancel_channel,
221 finish_event,
222 } = self;
223 _ = cancel_channel.send(());
224 finish_event
225 }
226}
227
228pub fn set_jobs_shutdown_timeout(plugin: &str, service: &str, version: &str, timeout: Duration) {
240 let rc = unsafe {
242 ffi::pico_ffi_background_set_jobs_shutdown_timeout(
243 plugin.into(),
244 service.into(),
245 version.into(),
246 timeout.as_secs_f64(),
247 )
248 };
249 debug_assert!(
250 rc == 0,
251 "return code is only for future compatibility at the moment"
252 );
253}
254
255#[derive(Debug)]
262pub struct OnceEvent {
263 finished: Cell<bool>,
264 cond: fiber::Cond,
265}
266
267impl OnceEvent {
268 fn new() -> Self {
269 Self {
270 finished: Cell::new(false),
271 cond: fiber::Cond::new(),
272 }
273 }
274
275 pub fn signal(&self) {
277 self.finished.set(true);
278 self.cond.broadcast();
279 }
280
281 #[inline]
282 pub fn is_finished(&self) -> bool {
283 self.finished.get()
284 }
285
286 pub fn wait_timeout(&self, timeout: Duration) {
287 self.cond.wait_timeout(timeout);
288 }
289}
290
291#[derive(Debug)]
296pub struct CancellationCallbackState {
297 cancel_channel: Channel<()>,
298 finish_event: Rc<OnceEvent>,
299 status: Cell<CancellationCallbackStatus>,
300}
301
302#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
303pub enum CancellationCallbackStatus {
304 #[default]
305 Initial = 0,
306 JobCancelled = 1,
307 JobFinished = 2,
308}
309
310impl CancellationCallbackState {
311 fn new(cancel_channel: Channel<()>, finish_event: Rc<OnceEvent>) -> Self {
312 Self {
313 cancel_channel,
314 finish_event,
315 status: Cell::new(CancellationCallbackStatus::Initial),
316 }
317 }
318
319 fn cancellation_callback(&self, action: u64, timeout: Duration) -> Result<(), BoxError> {
320 use CancellationCallbackStatus::*;
321 let next_status = action;
322
323 if next_status == JobCancelled as u64 {
324 if self.status.get() == JobCancelled || self.status.get() == JobFinished {
326 return Ok(());
327 }
328 debug_assert_eq!(self.status.get(), Initial);
329 _ = self.cancel_channel.send(());
330
331 self.status.set(JobCancelled);
332 } else if next_status == JobFinished as u64 {
333 if self.status.get() != JobCancelled && self.status.get() != JobFinished {
334 return Err(BoxError::new(
335 TarantoolErrorCode::IllegalParams,
336 "cannot wait for job that wasn't cancelled",
337 ));
338 }
339
340 let deadline = fiber::clock().saturating_add(timeout);
341
342 loop {
343 if self.finish_event.is_finished() {
344 self.status.set(JobFinished);
345 return Ok(());
346 }
347
348 let remaining = deadline.duration_since(fiber::clock());
349 if remaining.is_zero() {
350 return Err(BoxError::new(TarantoolErrorCode::Timeout, "timeout"));
351 }
352
353 self.finish_event.wait_timeout(remaining);
354 }
355 } else {
356 return Err(BoxError::new(
357 TarantoolErrorCode::IllegalParams,
358 format!("unexpected action: {action}"),
359 ));
360 }
361
362 Ok(())
363 }
364}
365
366#[repr(C)]
372pub struct FfiBackgroundJobCancellationToken {
373 callback: extern "C-unwind" fn(data: *const Self, action: u64, timeout: f64) -> i32,
377 drop: extern "C-unwind" fn(*mut Self),
378
379 closure_pointer: *mut (),
381
382 pub fiber_id: FiberId,
384}
385
386impl Drop for FfiBackgroundJobCancellationToken {
387 #[inline(always)]
388 fn drop(&mut self) {
389 (self.drop)(self)
390 }
391}
392
393impl FfiBackgroundJobCancellationToken {
394 fn new(fiber_id: FiberId, cancel_channel: Channel<()>, finish_event: Rc<OnceEvent>) -> Self {
395 let callback_state = CancellationCallbackState::new(cancel_channel, finish_event);
396 let callback = move |action, timeout| {
397 let res = callback_state.cancellation_callback(action, timeout);
398 if let Err(e) = res {
399 e.set_last();
400 return -1;
401 }
402
403 0
404 };
405
406 Self::new_inner(fiber_id, callback)
407 }
408
409 fn new_inner<F>(fiber_id: FiberId, f: F) -> Self
416 where
417 F: FnMut(u64, Duration) -> i32,
418 {
419 let closure = Box::new(f);
420 let closure_pointer: *mut F = Box::into_raw(closure);
421
422 Self {
423 callback: Self::trampoline::<F>,
424 drop: Self::drop_handler::<F>,
425 closure_pointer: closure_pointer.cast(),
426
427 fiber_id,
428 }
429 }
430
431 extern "C-unwind" fn trampoline<F>(data: *const Self, action: u64, timeout: f64) -> i32
436 where
437 F: FnMut(u64, Duration) -> i32,
438 {
439 let closure_pointer: *mut F = unsafe { (*data).closure_pointer.cast::<F>() };
441 let closure = unsafe { &mut *closure_pointer };
442
443 closure(action, Duration::from_secs_f64(timeout))
444 }
445
446 extern "C-unwind" fn drop_handler<F>(handler: *mut Self) {
447 unsafe {
448 let closure_pointer: *mut F = (*handler).closure_pointer.cast::<F>();
449 let closure = Box::from_raw(closure_pointer);
450 drop(closure);
451
452 if cfg!(debug_assertions) {
453 (*handler).closure_pointer = 0xcccccccccccccccc_u64 as _;
455 }
456 }
457 }
458
459 #[inline(always)]
461 pub fn cancel_job(&self) {
462 let rc = (self.callback)(self, CancellationCallbackStatus::JobCancelled as _, 0.0);
463 debug_assert!(rc == 0);
464 }
465
466 #[inline(always)]
468 #[allow(clippy::result_unit_err)]
469 pub fn wait_job_finished(&self, timeout: Duration) -> Result<(), ()> {
470 let rc = (self.callback)(
471 self,
472 CancellationCallbackStatus::JobFinished as _,
473 timeout.as_secs_f64(),
474 );
475 if rc == -1 {
476 return Err(());
479 }
480
481 Ok(())
482 }
483}