1#[cfg(feature = "wasm-workflows")]
4use crate::WasmWorkflowComponent;
5pub use crate::workflow_registry::WorkflowDefinitions;
6use crate::{
7 WorkerOptions,
8 activities::ActivityDefinitions,
9 interceptors::{ActivityInboundInterceptor, WorkerInterceptor},
10 workflow_interceptors::WorkflowInterceptorConstructor,
11 workflow_replayer::WorkflowReplayerOptions,
12};
13use std::{any::Any, sync::Arc};
14use temporalio_client::{
15 ClientInterceptor, ClientOptions, ClientPlugin, ConnectionOptions, ErasedClientPlugin,
16 PluginApplyError, PluginError, PluginTarget, WorkerPluginData,
17};
18use temporalio_common::data_converters::DataConverter;
19
20pub trait WorkerPlugin: Send + Sync + 'static {
26 fn name(&self) -> &str;
28
29 fn configure_worker_options(&self, _options: &mut WorkerOptions) -> Result<(), PluginError> {
34 Ok(())
35 }
36
37 fn configure_workflow_replayer_options(
42 &self,
43 _options: &mut WorkflowReplayerOptions,
44 ) -> Result<(), PluginError> {
45 Ok(())
46 }
47}
48
49#[derive(Clone)]
50struct PropagatedWorkerPlugin(Arc<dyn WorkerPlugin>);
51
52impl WorkerPluginData for PropagatedWorkerPlugin {}
53
54#[derive(Clone)]
81pub struct ClientAndWorkerPlugin {
82 client: ErasedClientPlugin,
83 worker: Arc<dyn WorkerPlugin>,
84}
85
86impl ClientAndWorkerPlugin {
87 pub fn new<P>(plugin: P) -> Self
90 where
91 P: ClientPlugin + WorkerPlugin,
92 {
93 let plugin = Arc::new(plugin);
94 let mut client = ErasedClientPlugin::new(SharedClientPlugin(plugin.clone()));
95 let worker: Arc<dyn WorkerPlugin> = Arc::new(SharedWorkerPlugin(plugin));
96 client = client.with_worker_plugin(PropagatedWorkerPlugin(worker.clone()));
97 Self { client, worker }
98 }
99}
100
101impl From<ClientAndWorkerPlugin> for ErasedClientPlugin {
102 fn from(plugin: ClientAndWorkerPlugin) -> Self {
103 plugin.client
104 }
105}
106
107impl WorkerPlugin for ClientAndWorkerPlugin {
108 fn name(&self) -> &str {
109 self.worker.name()
110 }
111
112 fn configure_worker_options(&self, options: &mut WorkerOptions) -> Result<(), PluginError> {
113 self.worker.configure_worker_options(options)
114 }
115
116 fn configure_workflow_replayer_options(
117 &self,
118 options: &mut WorkflowReplayerOptions,
119 ) -> Result<(), PluginError> {
120 self.worker.configure_workflow_replayer_options(options)
121 }
122}
123
124#[derive(Clone)]
131pub enum SimplePluginOption<T> {
132 Value(T),
134 Function(Arc<dyn Fn(Option<T>) -> T + Send + Sync>),
136}
137
138macro_rules! impl_simple_plugin_option_conversions {
139 ($type:ty) => {
140 impl From<$type> for SimplePluginOption<$type> {
141 fn from(value: $type) -> Self {
142 Self::Value(value)
143 }
144 }
145
146 impl<F> From<F> for SimplePluginOption<$type>
147 where
148 F: Fn(Option<$type>) -> $type + Send + Sync + 'static,
149 {
150 fn from(function: F) -> Self {
151 Self::Function(Arc::new(function))
152 }
153 }
154 };
155}
156
157impl_simple_plugin_option_conversions!(DataConverter);
158impl_simple_plugin_option_conversions!(Vec<Arc<dyn ClientInterceptor>>);
159impl_simple_plugin_option_conversions!(Vec<Arc<dyn WorkerInterceptor>>);
160impl_simple_plugin_option_conversions!(Vec<Arc<dyn ActivityInboundInterceptor>>);
161impl_simple_plugin_option_conversions!(Vec<WorkflowInterceptorConstructor>);
162impl_simple_plugin_option_conversions!(ActivityDefinitions);
163impl_simple_plugin_option_conversions!(WorkflowDefinitions);
164#[cfg(feature = "wasm-workflows")]
165impl_simple_plugin_option_conversions!(Vec<WasmWorkflowComponent>);
166
167fn apply_replacing<T: Clone>(target: &mut T, option: Option<&SimplePluginOption<T>>) {
168 let Some(option) = option else {
169 return;
170 };
171 *target = match option {
172 SimplePluginOption::Value(value) => value.clone(),
173 SimplePluginOption::Function(function) => function(Some(target.clone())),
174 };
175}
176
177fn apply_appending<T: Clone>(
178 target: &mut T,
179 option: Option<&SimplePluginOption<T>>,
180 append: impl Fn(&mut T, &T),
181) {
182 let Some(option) = option else {
183 return;
184 };
185 match option {
186 SimplePluginOption::Value(value) => append(target, value),
187 SimplePluginOption::Function(function) => *target = function(Some(target.clone())),
188 }
189}
190
191#[derive(Clone, bon::Builder)]
214#[builder(state_mod(vis = "pub"))]
215pub struct SimplePlugin {
216 #[builder(start_fn, into)]
217 name: String,
218 #[builder(into)]
219 data_converter: Option<SimplePluginOption<DataConverter>>,
220 #[builder(into)]
221 client_interceptors: Option<SimplePluginOption<Vec<Arc<dyn ClientInterceptor>>>>,
222 #[builder(into)]
223 worker_interceptors: Option<SimplePluginOption<Vec<Arc<dyn WorkerInterceptor>>>>,
224 #[builder(into)]
225 activity_inbound_interceptors:
226 Option<SimplePluginOption<Vec<Arc<dyn ActivityInboundInterceptor>>>>,
227 #[builder(into)]
228 workflow_interceptors: Option<SimplePluginOption<Vec<WorkflowInterceptorConstructor>>>,
229 #[builder(into)]
230 activities: Option<SimplePluginOption<ActivityDefinitions>>,
231 #[builder(into)]
232 workflows: Option<SimplePluginOption<WorkflowDefinitions>>,
233 #[cfg(feature = "wasm-workflows")]
234 #[builder(into)]
235 wasm_workflow_components: Option<SimplePluginOption<Vec<WasmWorkflowComponent>>>,
236}
237
238impl From<SimplePlugin> for ErasedClientPlugin {
239 fn from(plugin: SimplePlugin) -> Self {
240 ClientAndWorkerPlugin::new(plugin).into()
241 }
242}
243
244impl ClientPlugin for SimplePlugin {
245 fn name(&self) -> &str {
246 &self.name
247 }
248
249 fn configure_client_options(&self, options: &mut ClientOptions) -> Result<(), PluginError> {
250 apply_replacing(&mut options.data_converter, self.data_converter.as_ref());
251 apply_appending(
252 &mut options.client_interceptors,
253 self.client_interceptors.as_ref(),
254 |existing, value| existing.extend(value.iter().cloned()),
255 );
256 Ok(())
257 }
258}
259
260impl WorkerPlugin for SimplePlugin {
261 fn name(&self) -> &str {
262 &self.name
263 }
264
265 fn configure_worker_options(&self, options: &mut WorkerOptions) -> Result<(), PluginError> {
266 apply_appending(
267 &mut options.activities,
268 self.activities.as_ref(),
269 |existing, value| existing.extend(value),
270 );
271 if let Some(workflows) = &self.workflows {
272 match workflows {
273 SimplePluginOption::Value(value) => options.workflows.extend(value),
274 SimplePluginOption::Function(function) => {
275 let workflows = function(Some(options.workflows.clone()));
276 options.workflows.extend(&workflows)
277 }
278 }
279 .map_err(PluginError::new)?;
280 }
281 apply_appending(
282 &mut options.worker_interceptors,
283 self.worker_interceptors.as_ref(),
284 |existing, value| existing.extend(value.iter().cloned()),
285 );
286 apply_appending(
287 &mut options.activity_inbound_interceptors,
288 self.activity_inbound_interceptors.as_ref(),
289 |existing, value| existing.extend(value.iter().cloned()),
290 );
291 apply_appending(
292 &mut options.workflow_interceptor_constructors,
293 self.workflow_interceptors.as_ref(),
294 |existing, value| existing.extend(value.iter().cloned()),
295 );
296 #[cfg(feature = "wasm-workflows")]
297 apply_appending(
298 &mut options.wasm_workflow_components,
299 self.wasm_workflow_components.as_ref(),
300 |existing, value| existing.extend(value.iter().cloned()),
301 );
302 Ok(())
303 }
304
305 fn configure_workflow_replayer_options(
306 &self,
307 options: &mut WorkflowReplayerOptions,
308 ) -> Result<(), PluginError> {
309 apply_replacing(&mut options.data_converter, self.data_converter.as_ref());
310 if let Some(workflows) = &self.workflows {
311 match workflows {
312 SimplePluginOption::Value(value) => options.workflows.extend(value),
313 SimplePluginOption::Function(function) => {
314 let workflows = function(Some(options.workflows.clone()));
315 options.workflows.extend(&workflows)
316 }
317 }
318 .map_err(PluginError::new)?;
319 }
320 apply_appending(
321 &mut options.worker_interceptors,
322 self.worker_interceptors.as_ref(),
323 |existing, value| existing.extend(value.iter().cloned()),
324 );
325 apply_appending(
326 &mut options.workflow_interceptor_constructors,
327 self.workflow_interceptors.as_ref(),
328 |existing, value| existing.extend(value.iter().cloned()),
329 );
330 #[cfg(feature = "wasm-workflows")]
331 apply_appending(
332 &mut options.wasm_workflow_components,
333 self.wasm_workflow_components.as_ref(),
334 |existing, value| existing.extend(value.iter().cloned()),
335 );
336 Ok(())
337 }
338}
339
340struct SharedClientPlugin<P>(Arc<P>);
341
342impl<P> ClientPlugin for SharedClientPlugin<P>
343where
344 P: ClientPlugin,
345{
346 fn name(&self) -> &str {
347 ClientPlugin::name(self.0.as_ref())
348 }
349
350 fn configure_connection_options(
351 &self,
352 options: &mut ConnectionOptions,
353 ) -> Result<(), PluginError> {
354 self.0.configure_connection_options(options)
355 }
356
357 fn configure_client_options(&self, options: &mut ClientOptions) -> Result<(), PluginError> {
358 self.0.configure_client_options(options)
359 }
360}
361
362struct SharedWorkerPlugin<P>(Arc<P>);
363
364impl<P> WorkerPlugin for SharedWorkerPlugin<P>
365where
366 P: ClientPlugin + WorkerPlugin,
367{
368 fn name(&self) -> &str {
369 ClientPlugin::name(self.0.as_ref())
370 }
371
372 fn configure_worker_options(&self, options: &mut WorkerOptions) -> Result<(), PluginError> {
373 self.0.configure_worker_options(options)
374 }
375
376 fn configure_workflow_replayer_options(
377 &self,
378 options: &mut WorkflowReplayerOptions,
379 ) -> Result<(), PluginError> {
380 self.0.configure_workflow_replayer_options(options)
381 }
382}
383
384#[derive(Debug, Eq, PartialEq)]
385struct WorkerPluginWarning<'a> {
386 plugin_name: &'a str,
387 message: &'static str,
388}
389
390fn worker_plugin_warnings(
391 plugins: &[Arc<dyn WorkerPlugin>],
392) -> impl Iterator<Item = WorkerPluginWarning<'_>> {
393 plugins.iter().enumerate().filter_map(|(index, plugin)| {
394 plugins[index + 1..]
395 .iter()
396 .any(|other| plugin.name() == other.name())
397 .then_some(WorkerPluginWarning {
398 plugin_name: plugin.name(),
399 message: "Multiple worker plugins with the same name were registered",
400 })
401 })
402}
403
404pub(crate) fn apply_worker_plugins(
405 client_options: &ClientOptions,
406 options: &mut WorkerOptions,
407) -> Result<(), PluginApplyError> {
408 options.client_plugin_names = client_options
409 .plugins()
410 .iter()
411 .map(|plugin| plugin.name().to_owned())
412 .collect();
413 let mut plugins = client_options
414 .plugins()
415 .iter()
416 .flat_map(ErasedClientPlugin::worker_plugins)
417 .filter_map(|plugin| (plugin as &dyn Any).downcast_ref::<PropagatedWorkerPlugin>())
418 .map(|plugin| plugin.0.clone())
419 .collect::<Vec<_>>();
420 plugins.append(&mut options.worker_plugins);
421
422 for warning in worker_plugin_warnings(&plugins) {
423 warn!(plugin = warning.plugin_name, "{}", warning.message);
424 }
425
426 for registration in &plugins {
427 registration
428 .configure_worker_options(options)
429 .map_err(|source| {
430 PluginApplyError::new(registration.name(), PluginTarget::Worker, source)
431 })?;
432 }
433 options.worker_plugins = plugins;
434 Ok(())
435}
436
437pub(crate) fn apply_workflow_replayer_plugins(
438 options: &mut WorkflowReplayerOptions,
439) -> Result<(), PluginApplyError> {
440 let plugins = std::mem::take(&mut options.worker_plugins);
441
442 for warning in worker_plugin_warnings(&plugins) {
443 warn!(plugin = warning.plugin_name, "{}", warning.message);
444 }
445
446 for registration in &plugins {
447 registration
448 .configure_workflow_replayer_options(options)
449 .map_err(|source| {
450 PluginApplyError::new(registration.name(), PluginTarget::WorkflowReplayer, source)
451 })?;
452 }
453 options.worker_plugins = plugins;
454 Ok(())
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use std::{
461 collections::HashSet,
462 sync::{
463 Mutex,
464 atomic::{AtomicU8, Ordering},
465 },
466 };
467 use temporalio_client::ClientOptions;
468 use temporalio_common::protos::temporal::api::worker::v1::PluginInfo;
469
470 #[temporalio_macros::workflow]
471 #[derive(Default)]
472 struct PluginTestWorkflow;
473
474 #[temporalio_macros::workflow_methods]
475 impl PluginTestWorkflow {
476 #[run]
477 async fn run(_ctx: &mut crate::WorkflowContext<Self>) -> crate::WorkflowResult<()> {
478 Ok(())
479 }
480 }
481
482 struct RecordingCombinedPlugin {
483 order: Arc<Mutex<Vec<&'static str>>>,
484 }
485
486 impl ClientPlugin for RecordingCombinedPlugin {
487 fn name(&self) -> &str {
488 "combined"
489 }
490 }
491
492 impl WorkerPlugin for RecordingCombinedPlugin {
493 fn name(&self) -> &str {
494 "combined"
495 }
496
497 fn configure_worker_options(
498 &self,
499 _options: &mut WorkerOptions,
500 ) -> Result<(), PluginError> {
501 self.order.lock().unwrap().push("propagated");
502 Ok(())
503 }
504 }
505
506 struct RecordingWorkerPlugin {
507 name: &'static str,
508 value: &'static str,
509 order: Arc<Mutex<Vec<&'static str>>>,
510 }
511
512 impl WorkerPlugin for RecordingWorkerPlugin {
513 fn name(&self) -> &str {
514 self.name
515 }
516
517 fn configure_worker_options(
518 &self,
519 _options: &mut WorkerOptions,
520 ) -> Result<(), PluginError> {
521 self.order.lock().unwrap().push(self.value);
522 Ok(())
523 }
524
525 fn configure_workflow_replayer_options(
526 &self,
527 _options: &mut WorkflowReplayerOptions,
528 ) -> Result<(), PluginError> {
529 self.order.lock().unwrap().push(self.value);
530 Ok(())
531 }
532 }
533
534 #[test]
535 fn propagated_plugins_run_before_local_plugins() {
536 let order = Arc::new(Mutex::new(Vec::new()));
537 let combined = ClientAndWorkerPlugin::new(RecordingCombinedPlugin {
538 order: order.clone(),
539 });
540 let client_options = ClientOptions::new("namespace").plugin(combined).build();
541 let mut worker_options = WorkerOptions::new("queue")
542 .worker_plugin(RecordingWorkerPlugin {
543 name: "local",
544 value: "local",
545 order: order.clone(),
546 })
547 .build();
548
549 apply_worker_plugins(&client_options, &mut worker_options).unwrap();
550
551 assert_eq!(*order.lock().unwrap(), ["propagated", "local"]);
552 }
553
554 #[test]
555 fn replay_plugins_configure_in_registration_order() {
556 let order = Arc::new(Mutex::new(Vec::new()));
557 let mut options = WorkflowReplayerOptions::new()
558 .worker_plugin(RecordingWorkerPlugin {
559 name: "first",
560 value: "first",
561 order: order.clone(),
562 })
563 .worker_plugin(RecordingWorkerPlugin {
564 name: "second",
565 value: "second",
566 order: order.clone(),
567 })
568 .build();
569
570 apply_workflow_replayer_plugins(&mut options).unwrap();
571
572 assert_eq!(*order.lock().unwrap(), ["first", "second"]);
573 assert_eq!(options.worker_plugins.len(), 2);
574 }
575
576 #[test]
577 fn explicitly_reusing_a_combined_registration_warns_and_applies_both() {
578 let order = Arc::new(Mutex::new(Vec::new()));
579 let combined = ClientAndWorkerPlugin::new(RecordingCombinedPlugin {
580 order: order.clone(),
581 });
582 let client_options = ClientOptions::new("namespace")
583 .plugin(combined.clone())
584 .build();
585 let mut worker_options = WorkerOptions::new("queue").worker_plugin(combined).build();
586
587 apply_worker_plugins(&client_options, &mut worker_options).unwrap();
588
589 assert_eq!(
590 worker_plugin_warnings(&worker_options.worker_plugins).collect::<Vec<_>>(),
591 [WorkerPluginWarning {
592 plugin_name: "combined",
593 message: "Multiple worker plugins with the same name were registered",
594 }]
595 );
596 assert_eq!(*order.lock().unwrap(), ["propagated", "propagated"]);
597 }
598
599 struct ClientOnlyPlugin;
600
601 impl ClientPlugin for ClientOnlyPlugin {
602 fn name(&self) -> &str {
603 "client-only"
604 }
605 }
606
607 struct SameNameClientPlugin;
608
609 impl ClientPlugin for SameNameClientPlugin {
610 fn name(&self) -> &str {
611 "same-name"
612 }
613 }
614
615 struct UnrecognizedWorkerPluginExtension {
616 _registration: Arc<dyn WorkerPlugin>,
617 }
618
619 impl WorkerPluginData for UnrecognizedWorkerPluginExtension {}
620
621 #[test]
622 fn arbitrary_opaque_data_cannot_impersonate_propagated_plugin() {
623 let order = Arc::new(Mutex::new(Vec::new()));
624 let client_registration = ErasedClientPlugin::new(ClientOnlyPlugin).with_worker_plugin(
625 UnrecognizedWorkerPluginExtension {
626 _registration: Arc::new(RecordingWorkerPlugin {
627 name: "unrecognized",
628 value: "should-not-run",
629 order: order.clone(),
630 }),
631 },
632 );
633 let client_options = ClientOptions::new("namespace")
634 .plugin(client_registration)
635 .build();
636 let mut worker_options = WorkerOptions::new("queue").build();
637
638 apply_worker_plugins(&client_options, &mut worker_options).unwrap();
639
640 assert!(order.lock().unwrap().is_empty());
641 }
642
643 #[test]
644 fn duplicate_worker_plugin_names_warn_and_heartbeat_names_are_deduplicated() {
645 let order = Arc::new(Mutex::new(Vec::new()));
646 let client_options = ClientOptions::new("namespace")
647 .client_plugin(ClientOnlyPlugin)
648 .client_plugin(SameNameClientPlugin)
649 .build();
650 let mut worker_options = WorkerOptions::new("queue")
651 .register_workflow::<PluginTestWorkflow>()
652 .unwrap()
653 .worker_plugin(RecordingWorkerPlugin {
654 name: "same-name",
655 value: "first",
656 order: order.clone(),
657 })
658 .worker_plugin(RecordingWorkerPlugin {
659 name: "same-name",
660 value: "second",
661 order,
662 })
663 .build();
664 apply_worker_plugins(&client_options, &mut worker_options).unwrap();
665
666 assert_eq!(
667 worker_plugin_warnings(&worker_options.worker_plugins).collect::<Vec<_>>(),
668 [WorkerPluginWarning {
669 plugin_name: "same-name",
670 message: "Multiple worker plugins with the same name were registered",
671 }]
672 );
673 let core_options = worker_options
674 .to_core_options("namespace".to_owned(), "identity".to_owned())
675 .unwrap();
676
677 let expected_plugins: HashSet<_> = vec![
678 PluginInfo {
679 name: "client-only".into(),
680 version: "".into(),
681 },
682 PluginInfo {
683 name: "same-name".into(),
684 version: "".into(),
685 },
686 ]
687 .into_iter()
688 .collect();
689 assert_eq!(core_options.plugins, expected_plugins);
690 }
691
692 struct EmptyClientInterceptor;
693
694 impl ClientInterceptor for EmptyClientInterceptor {}
695
696 struct EmptyWorkerInterceptor;
697
698 #[async_trait::async_trait(?Send)]
699 impl WorkerInterceptor for EmptyWorkerInterceptor {}
700
701 #[test]
702 fn simple_plugin_applies_declarative_values() {
703 let plugin = SimplePlugin::builder("simple")
704 .client_interceptors(vec![
705 Arc::new(EmptyClientInterceptor) as Arc<dyn ClientInterceptor>
706 ])
707 .build();
708 let mut client_options = ClientOptions::new("namespace").build();
709 client_options
710 .client_interceptors
711 .push(Arc::new(EmptyClientInterceptor));
712 plugin
713 .configure_client_options(&mut client_options)
714 .unwrap();
715 assert_eq!(client_options.client_interceptors.len(), 2);
716
717 let plugin = SimplePlugin::builder("simple")
718 .worker_interceptors(vec![
719 Arc::new(EmptyWorkerInterceptor) as Arc<dyn WorkerInterceptor>
720 ])
721 .build();
722 let client_options = ClientOptions::new("namespace").build();
723 let mut worker_options = WorkerOptions::new("queue")
724 .worker_interceptor(EmptyWorkerInterceptor)
725 .worker_plugin(plugin)
726 .build();
727 apply_worker_plugins(&client_options, &mut worker_options).unwrap();
728
729 assert_eq!(worker_options.worker_interceptors.len(), 2);
730 }
731
732 #[test]
733 fn simple_plugin_options_accept_values_and_functions() {
734 let calls = Arc::new(AtomicU8::new(0));
735 let plugin = SimplePlugin::builder("simple")
736 .data_converter({
737 let calls = calls.clone();
738 move |existing: Option<DataConverter>| {
739 calls.fetch_add(1, Ordering::SeqCst);
740 existing.unwrap()
741 }
742 })
743 .client_interceptors({
744 let calls = calls.clone();
745 move |existing: Option<Vec<Arc<dyn ClientInterceptor>>>| {
746 calls.fetch_add(1, Ordering::SeqCst);
747 assert_eq!(existing.unwrap().len(), 1);
748 Vec::new()
749 }
750 })
751 .worker_interceptors({
752 let calls = calls.clone();
753 move |existing: Option<Vec<Arc<dyn WorkerInterceptor>>>| {
754 calls.fetch_add(1, Ordering::SeqCst);
755 assert_eq!(existing.unwrap().len(), 1);
756 Vec::new()
757 }
758 })
759 .activity_inbound_interceptors({
760 let calls = calls.clone();
761 move |existing: Option<Vec<Arc<dyn ActivityInboundInterceptor>>>| {
762 calls.fetch_add(1, Ordering::SeqCst);
763 existing.unwrap()
764 }
765 })
766 .workflow_interceptors({
767 let calls = calls.clone();
768 move |existing: Option<Vec<WorkflowInterceptorConstructor>>| {
769 calls.fetch_add(1, Ordering::SeqCst);
770 existing.unwrap()
771 }
772 })
773 .activities({
774 let calls = calls.clone();
775 move |existing: Option<ActivityDefinitions>| {
776 calls.fetch_add(1, Ordering::SeqCst);
777 existing.unwrap()
778 }
779 })
780 .workflows({
781 let calls = calls.clone();
782 move |existing: Option<WorkflowDefinitions>| {
783 calls.fetch_add(1, Ordering::SeqCst);
784 existing.unwrap()
785 }
786 })
787 .build();
788
789 let mut client_options = ClientOptions::new("namespace").build();
790 client_options
791 .client_interceptors
792 .push(Arc::new(EmptyClientInterceptor));
793 plugin
794 .configure_client_options(&mut client_options)
795 .unwrap();
796 assert!(client_options.client_interceptors.is_empty());
797
798 let mut worker_options = WorkerOptions::new("queue")
799 .worker_interceptor(EmptyWorkerInterceptor)
800 .worker_plugin(plugin)
801 .build();
802 apply_worker_plugins(
803 &ClientOptions::new("namespace").build(),
804 &mut worker_options,
805 )
806 .unwrap();
807 assert!(worker_options.worker_interceptors.is_empty());
808 assert_eq!(calls.load(Ordering::SeqCst), 7);
809 }
810
811 struct RecursivePlugin(Arc<AtomicU8>);
812
813 impl WorkerPlugin for RecursivePlugin {
814 fn name(&self) -> &str {
815 "recursive"
816 }
817
818 fn configure_worker_options(&self, options: &mut WorkerOptions) -> Result<(), PluginError> {
819 self.0.fetch_add(1, Ordering::SeqCst);
820 *options = WorkerOptions::new(options.task_queue.clone())
821 .worker_plugin(RecursivePlugin(self.0.clone()))
822 .worker_plugin(RecursivePlugin(self.0.clone()))
823 .build();
824 Ok(())
825 }
826 }
827
828 #[test]
829 fn test_plugins_cannot_recurse() {
830 let count = Arc::new(AtomicU8::new(0));
831 let mut worker_opts = WorkerOptions::new("my-task-queue")
832 .worker_plugin(RecursivePlugin(count.clone()))
833 .build();
834 apply_worker_plugins(&ClientOptions::new("my-ns").build(), &mut worker_opts).unwrap();
835 assert_eq!(count.load(Ordering::SeqCst), 1);
836 }
837}