Skip to main content

temporalio_sdk_core/
lib.rs

1#![warn(missing_docs)] // error if there are missing docs
2#![allow(clippy::upper_case_acronyms)]
3
4//! This crate provides a basis for creating new Temporal SDKs without completely starting from
5//! scratch. APIs provided by this crate are not considered stable and may break at any time.
6//!
7//! If you are looking for the Temporal Rust SDK, please use `temporalio-sdk`.
8
9#[cfg(test)]
10#[macro_use]
11pub extern crate assert_matches;
12#[macro_use]
13extern crate tracing;
14extern crate core;
15
16mod abstractions;
17#[cfg(feature = "antithesis_assertions")]
18mod antithesis;
19#[cfg(feature = "debug-plugin")]
20pub mod debug_client;
21mod environment;
22#[cfg(feature = "ephemeral-server")]
23pub mod ephemeral_server;
24mod internal_flags;
25mod pollers;
26mod protosext;
27pub mod replay;
28pub(crate) mod retry_logic;
29pub mod telemetry;
30mod worker;
31
32#[cfg(test)]
33mod core_tests;
34#[cfg(any(feature = "test-utilities", test))]
35#[macro_use]
36pub mod test_help;
37
38pub use crate::worker::client::{
39    PollActivityOptions, PollOptions, PollWorkflowOptions, WorkerClient, WorkflowTaskCompletion,
40};
41pub use pollers::{Client, ClientOptions, ClientTlsOptions, RetryOptions, TlsOptions};
42pub use temporalio_common::protos::TaskToken;
43pub use url::Url;
44pub use worker::{
45    ActivitySlotKind, CompleteActivityError, CompleteNexusError, CompleteWfError,
46    FixedSizeSlotSupplier, LocalActivitySlotKind, NamespaceCapabilities, NexusSlotKind, PollError,
47    PollerBehavior, ResourceBasedSlotsOptions, ResourceBasedSlotsOptionsBuilder,
48    ResourceBasedTuner, ResourceBasedTunerConfig, ResourceController, ResourceSlotOptions,
49    SlotInfo, SlotInfoTrait, SlotKind, SlotKindType, SlotMarkUsedContext, SlotReleaseContext,
50    SlotReservationContext, SlotSupplier, SlotSupplierOptions, SlotSupplierPermit, TunerBuilder,
51    TunerHolder, TunerHolderOptions, TunerHolderOptionsBuilder, Worker, WorkerConfig,
52    WorkerConfigBuilder, WorkerTuner, WorkerValidationError, WorkerVersioningStrategy,
53    WorkflowErrorType, WorkflowSlotKind,
54};
55
56use crate::{
57    replay::{HistoryForReplay, ReplayWorkerInput},
58    telemetry::metrics::MetricsContext,
59    worker::client::WorkerClientBag,
60};
61use anyhow::bail;
62use futures_util::Stream;
63use std::{sync::Arc, time::Duration};
64use temporalio_client::{Connection, SharedReplaceableClient};
65use temporalio_common::{
66    protos::{
67        coresdk::ActivityHeartbeat,
68        temporal::api::worker::v1::{EnvironmentInfo, environment_info::Runtime},
69    },
70    telemetry::{
71        TelemetryInstance, TelemetryOptions, remove_trace_subscriber_for_current_thread,
72        set_trace_subscriber_for_current_thread, telemetry_init,
73    },
74};
75
76/// Initialize a worker bound to a task queue.
77///
78/// You will need to have already initialized a [CoreRuntime] which will be used for this worker.
79/// After the worker is initialized, you should use [CoreRuntime::tokio_handle] to run the worker's
80/// async functions.
81///
82/// Lang implementations must pass in a [Client] When they do so, this function will always
83/// overwrite the client retry configuration, force the client to use the namespace defined in the
84/// worker config, and set the client identity appropriately.
85pub fn init_worker(
86    runtime: &CoreRuntime,
87    worker_config: WorkerConfig,
88    mut connection: Connection,
89) -> Result<Worker, anyhow::Error> {
90    let namespace = worker_config.namespace.clone();
91    if namespace.is_empty() {
92        bail!("Worker namespace cannot be empty");
93    }
94
95    *connection.retry_options_mut() = RetryOptions::default();
96    init_worker_client(
97        &mut connection,
98        worker_config.client_identity_override.clone(),
99    );
100    let client = SharedReplaceableClient::new(connection);
101    let client_ident = client.inner_cow().identity().to_owned();
102    if client_ident.is_empty() {
103        bail!("Client identity cannot be empty. Either lang or user should be setting this value");
104    }
105    let sticky_q = sticky_q_name_for_worker(&client_ident, worker_config.max_cached_workflows);
106
107    let worker_instance_key = uuid::Uuid::new_v4();
108    let client_bag = Arc::new(WorkerClientBag::new(
109        client,
110        namespace.clone(),
111        worker_config.versioning_strategy.clone(),
112        worker_instance_key,
113    ));
114
115    Worker::new(
116        worker_config.clone(),
117        sticky_q,
118        client_bag.clone(),
119        Some(&runtime.telemetry),
120        runtime.heartbeat_interval,
121        runtime.environment_info.clone(),
122    )
123}
124
125/// Create a worker for replaying one or more existing histories. It will auto-shutdown as soon as
126/// all histories have finished being replayed.
127///
128/// You do not necessarily need a [CoreRuntime] for replay workers, but it's advisable to create
129/// one and use it to run the replay worker's async functions the same way you would for a normal
130/// worker.
131pub fn init_replay_worker<I>(rwi: ReplayWorkerInput<I>) -> Result<Worker, anyhow::Error>
132where
133    I: Stream<Item = HistoryForReplay> + Send + 'static,
134{
135    info!(
136        task_queue = rwi.config.task_queue.as_str(),
137        "Registering replay worker"
138    );
139    rwi.into_core_worker()
140}
141
142pub(crate) fn init_worker_client(
143    connection: &mut Connection,
144    client_identity_override: Option<String>,
145) {
146    if let Some(ref id_override) = client_identity_override {
147        connection.identity_mut().clone_from(id_override);
148    }
149}
150
151/// Creates a unique sticky queue name for a worker, iff the config allows for 1 or more cached
152/// workflows.
153pub(crate) fn sticky_q_name_for_worker(
154    process_identity: &str,
155    max_cached_workflows: usize,
156) -> Option<String> {
157    if max_cached_workflows > 0 {
158        Some(format!(
159            "{}-{}",
160            &process_identity,
161            uuid::Uuid::new_v4().simple()
162        ))
163    } else {
164        None
165    }
166}
167
168/// Holds shared state/components needed to back instances of workers and clients. More than one
169/// may be instantiated, but typically only one is needed. More than one runtime instance may be
170/// useful if multiple different telemetry settings are required.
171pub struct CoreRuntime {
172    telemetry: TelemetryInstance,
173    runtime: Option<tokio::runtime::Runtime>,
174    runtime_handle: tokio::runtime::Handle,
175    heartbeat_interval: Option<Duration>,
176    environment_info: Option<Arc<EnvironmentInfo>>,
177}
178
179/// Holds telemetry and process-wide worker options. Construct with [RuntimeOptions::builder].
180#[derive(bon::Builder)]
181#[builder(finish_fn(vis = "", name = build_internal))]
182#[non_exhaustive]
183pub struct RuntimeOptions {
184    /// Telemetry configuration options.
185    #[builder(default)]
186    telemetry_options: TelemetryOptions,
187    /// Optional worker heartbeat interval - This configures the heartbeat setting of all
188    /// workers created using this runtime.
189    ///
190    /// Interval must be between 1s and 60s, inclusive.
191    #[builder(required, default = Some(Duration::from_secs(60)))]
192    heartbeat_interval: Option<Duration>,
193    /// Disable including runtime, hosting, and platform information in worker heartbeats.
194    #[builder(default)]
195    disable_environment_info: bool,
196    /// Runtime information supplied by language SDK bridges.
197    #[doc(hidden)]
198    #[builder(skip = vec![environment::native_runtime()])]
199    runtimes: Vec<Runtime>,
200}
201
202impl RuntimeOptions {
203    /// Supplies runtime information from a language SDK bridge.
204    #[doc(hidden)]
205    pub fn with_runtimes(mut self, runtimes: Vec<Runtime>) -> Self {
206        self.runtimes = runtimes;
207        self
208    }
209}
210
211impl Default for RuntimeOptions {
212    fn default() -> Self {
213        Self::builder().build().expect("builder defaults are valid")
214    }
215}
216
217impl<S: runtime_options_builder::State> RuntimeOptionsBuilder<S> {
218    /// Builds the RuntimeOptions
219    ///
220    /// # Errors
221    /// Returns an error if heartbeat_interval is set but not between 1s and 60s inclusive.
222    pub fn build(self) -> Result<RuntimeOptions, String> {
223        let options = self.build_internal();
224        {
225            if let Some(interval) = options.heartbeat_interval
226                && (interval < Duration::from_secs(1) || interval > Duration::from_secs(60))
227            {
228                return Err(format!(
229                    "heartbeat_interval ({interval:?}) must be between 1s and 60s",
230                ));
231            }
232
233            Ok(options)
234        }
235    }
236}
237
238/// Wraps a [tokio::runtime::Builder] to allow layering multiple on_thread_start functions
239pub struct TokioRuntimeBuilder<F> {
240    /// The underlying tokio runtime builder
241    pub inner: tokio::runtime::Builder,
242    /// A function to be called when setting the runtime builder's on thread start
243    pub lang_on_thread_start: Option<F>,
244}
245
246impl Default for TokioRuntimeBuilder<Box<dyn Fn() + Send + Sync>> {
247    fn default() -> Self {
248        TokioRuntimeBuilder {
249            inner: tokio::runtime::Builder::new_multi_thread(),
250            lang_on_thread_start: None,
251        }
252    }
253}
254
255impl CoreRuntime {
256    /// Create a new core runtime with the provided telemetry options and tokio runtime builder.
257    /// Also initialize telemetry for the thread this is being called on.
258    ///
259    /// Note that this function will call the [tokio::runtime::Builder::enable_all] builder option
260    /// on the Tokio runtime builder, and will call [tokio::runtime::Builder::on_thread_start] to
261    /// ensure telemetry subscribers are set on every tokio thread.
262    ///
263    /// **Important**: You need to call this *before* calling any async functions on workers or
264    /// clients, otherwise the tracing subscribers will not be properly attached.
265    ///
266    /// # Panics
267    /// If a tokio runtime has already been initialized. To re-use an existing runtime, call
268    /// [CoreRuntime::new_assume_tokio].
269    pub fn new<F>(
270        runtime_options: RuntimeOptions,
271        mut tokio_builder: TokioRuntimeBuilder<F>,
272    ) -> Result<Self, anyhow::Error>
273    where
274        F: Fn() + Send + Sync + 'static,
275    {
276        let RuntimeOptions {
277            telemetry_options,
278            heartbeat_interval,
279            disable_environment_info,
280            runtimes,
281        } = runtime_options;
282        let telemetry = telemetry_init(telemetry_options)?;
283        let subscriber = telemetry.trace_subscriber();
284        let runtime = tokio_builder
285            .inner
286            .enable_all()
287            .on_thread_start(move || {
288                if let Some(sub) = subscriber.as_ref() {
289                    set_trace_subscriber_for_current_thread(sub.clone());
290                }
291                if let Some(lang_on_thread_start) = tokio_builder.lang_on_thread_start.as_ref() {
292                    lang_on_thread_start();
293                }
294            })
295            .build()?;
296        let _rg = runtime.enter();
297        let mut me = Self::new_assume_tokio_initialized_telem_with_environment(
298            telemetry,
299            heartbeat_interval,
300            disable_environment_info,
301            runtimes,
302        );
303        me.runtime = Some(runtime);
304        Ok(me)
305    }
306
307    /// Initialize telemetry for the thread this is being called on, assuming a tokio runtime is
308    /// already active and this call exists in its context. See [Self::new] for more.
309    ///
310    /// # Panics
311    /// If there is no currently active Tokio runtime
312    pub fn new_assume_tokio(runtime_options: RuntimeOptions) -> Result<Self, anyhow::Error> {
313        let RuntimeOptions {
314            telemetry_options,
315            heartbeat_interval,
316            disable_environment_info,
317            runtimes,
318        } = runtime_options;
319        let telemetry = telemetry_init(telemetry_options)?;
320        Ok(Self::new_assume_tokio_initialized_telem_with_environment(
321            telemetry,
322            heartbeat_interval,
323            disable_environment_info,
324            runtimes,
325        ))
326    }
327
328    /// Construct a runtime from an already-initialized telemetry instance, assuming a tokio runtime
329    /// is already active and this call exists in its context. See [Self::new] for more.
330    ///
331    /// # Panics
332    /// If there is no currently active Tokio runtime
333    pub fn new_assume_tokio_initialized_telem(
334        telemetry: TelemetryInstance,
335        heartbeat_interval: Option<Duration>,
336    ) -> Self {
337        Self::new_assume_tokio_initialized_telem_with_environment(
338            telemetry,
339            heartbeat_interval,
340            false,
341            vec![environment::native_runtime()],
342        )
343    }
344
345    fn new_assume_tokio_initialized_telem_with_environment(
346        telemetry: TelemetryInstance,
347        heartbeat_interval: Option<Duration>,
348        disable_environment_info: bool,
349        runtimes: Vec<Runtime>,
350    ) -> Self {
351        let runtime_handle = tokio::runtime::Handle::current();
352        if let Some(sub) = telemetry.trace_subscriber() {
353            set_trace_subscriber_for_current_thread(sub);
354        }
355        let environment_info = heartbeat_interval
356            .filter(|_| !disable_environment_info)
357            .map(|_| Arc::new(environment::detect(runtimes)));
358        Self {
359            telemetry,
360            runtime: None,
361            runtime_handle,
362            heartbeat_interval,
363            environment_info,
364        }
365    }
366
367    /// Get a handle to the tokio runtime used by this Core runtime.
368    pub fn tokio_handle(&self) -> tokio::runtime::Handle {
369        self.runtime_handle.clone()
370    }
371
372    /// Return a reference to the owned [TelemetryInstance]
373    pub fn telemetry(&self) -> &TelemetryInstance {
374        &self.telemetry
375    }
376
377    /// Return a mutable reference to the owned [TelemetryInstance]
378    pub fn telemetry_mut(&mut self) -> &mut TelemetryInstance {
379        &mut self.telemetry
380    }
381}
382
383impl Drop for CoreRuntime {
384    fn drop(&mut self) {
385        remove_trace_subscriber_for_current_thread();
386    }
387}
388
389#[cfg(test)]
390mod test {
391    use super::*;
392    #[test]
393    fn runtime_options_default_matches_builder_default() {
394        let default = RuntimeOptions::default();
395        let built = RuntimeOptions::builder().build().unwrap();
396        assert_eq!(default.heartbeat_interval, built.heartbeat_interval);
397    }
398}