use once_cell::sync::{Lazy, OnceCell};
use rand::Rng;
use serde::{Deserialize, Serialize};
#[cfg(feature = "dial9")]
use std::path::PathBuf;
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::Duration;
use thread_local::ThreadLocal;
use tokio::runtime::{Builder, Handle};
use tokio::sync::oneshot::{channel, Sender};
#[cfg(feature = "dial9")]
pub const DEFAULT_DIAL9_MAX_FILE_SIZE: u64 = 100 * 1024 * 1024;
#[cfg(feature = "dial9")]
pub const DEFAULT_DIAL9_MAX_TOTAL_SIZE: u64 = 512 * 1024 * 1024;
#[derive(Debug, Clone, Default)]
pub struct BlockingPoolOpts {
pub max_threads: Option<usize>,
pub thread_keep_alive: Option<Duration>,
}
#[derive(Debug, Clone, Default)]
pub struct RuntimeMetricsOpts {
pub poll_time_histogram: bool,
pub poll_time_histogram_scale: Option<RuntimeMetricsPollTimeHistogramScale>,
pub poll_time_histogram_resolution: Option<Duration>,
pub poll_time_histogram_buckets: Option<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct RuntimeOpts {
pub metrics: RuntimeMetricsOpts,
pub enable_alt_timer: bool,
#[cfg(feature = "dial9")]
pub dial9: Option<Dial9RuntimeOpts>,
}
#[cfg(feature = "dial9")]
#[derive(Debug, Clone)]
pub struct Dial9RuntimeOpts {
pub trace_path: PathBuf,
pub max_file_size: u64,
pub max_total_size: u64,
pub rotation_period: Option<Duration>,
pub task_tracking: bool,
pub worker_poll_interval: Option<Duration>,
#[cfg(feature = "dial9-worker-s3")]
pub s3_upload: Option<Dial9S3UploadOpts>,
}
#[cfg(feature = "dial9")]
impl Dial9RuntimeOpts {
pub fn new(trace_path: impl Into<PathBuf>) -> Self {
Self {
trace_path: trace_path.into(),
max_file_size: DEFAULT_DIAL9_MAX_FILE_SIZE,
max_total_size: DEFAULT_DIAL9_MAX_TOTAL_SIZE,
rotation_period: None,
task_tracking: true,
worker_poll_interval: None,
#[cfg(feature = "dial9-worker-s3")]
s3_upload: None,
}
}
pub fn with_max_file_size(mut self, max_file_size: u64) -> Self {
self.max_file_size = max_file_size;
self
}
pub fn with_max_total_size(mut self, max_total_size: u64) -> Self {
self.max_total_size = max_total_size;
self
}
pub fn with_rotation_period(mut self, rotation_period: Duration) -> Self {
self.rotation_period = Some(rotation_period);
self
}
pub fn with_task_tracking(mut self, task_tracking: bool) -> Self {
self.task_tracking = task_tracking;
self
}
pub fn with_worker_poll_interval(mut self, worker_poll_interval: Duration) -> Self {
self.worker_poll_interval = Some(worker_poll_interval);
self
}
#[cfg(feature = "dial9-worker-s3")]
pub fn with_s3_upload(mut self, s3_upload: Dial9S3UploadOpts) -> Self {
self.s3_upload = Some(s3_upload);
self
}
}
#[cfg(all(feature = "dial9", feature = "dial9-worker-s3"))]
#[derive(Debug, Clone)]
pub struct Dial9S3UploadOpts {
pub bucket: String,
pub service_name: String,
pub prefix: Option<String>,
pub region: Option<String>,
pub instance_path: Option<String>,
pub client: Option<aws_sdk_s3::Client>,
}
#[cfg(all(feature = "dial9", feature = "dial9-worker-s3"))]
impl Dial9S3UploadOpts {
pub fn new(bucket: impl Into<String>, service_name: impl Into<String>) -> Self {
Self {
bucket: bucket.into(),
service_name: service_name.into(),
prefix: None,
region: None,
instance_path: None,
client: None,
}
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = Some(prefix.into());
self
}
pub fn with_region(mut self, region: impl Into<String>) -> Self {
self.region = Some(region.into());
self
}
pub fn with_instance_path(mut self, instance_path: impl Into<String>) -> Self {
self.instance_path = Some(instance_path.into());
self
}
pub fn with_client(mut self, client: aws_sdk_s3::Client) -> Self {
self.client = Some(client);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeMetricsPollTimeHistogramScale {
Linear,
Log,
}
pub enum Runtime {
Steal {
runtime: tokio::runtime::Runtime,
#[cfg(feature = "dial9")]
dial9_guard: Option<dial9_tokio_telemetry::telemetry::TelemetryGuard>,
},
NoSteal(NoStealRuntime),
}
fn apply_blocking_opts(builder: &mut Builder, opts: &BlockingPoolOpts) {
if let Some(max) = opts.max_threads {
builder.max_blocking_threads(max);
}
if let Some(ttl) = opts.thread_keep_alive {
builder.thread_keep_alive(ttl);
}
}
#[allow(deprecated)]
fn apply_metrics_opts(builder: &mut Builder, opts: &RuntimeMetricsOpts) {
#[cfg(tokio_unstable)]
if opts.poll_time_histogram {
builder.enable_metrics_poll_time_histogram();
if let Some(scale) = opts.poll_time_histogram_scale {
builder.metrics_poll_count_histogram_scale(match scale {
RuntimeMetricsPollTimeHistogramScale::Linear => {
tokio::runtime::HistogramScale::Linear
}
RuntimeMetricsPollTimeHistogramScale::Log => tokio::runtime::HistogramScale::Log,
});
}
if let Some(resolution) = opts
.poll_time_histogram_resolution
.filter(|resolution| !resolution.is_zero())
{
builder.metrics_poll_count_histogram_resolution(resolution);
}
if let Some(buckets) = opts
.poll_time_histogram_buckets
.filter(|buckets| *buckets > 0)
{
builder.metrics_poll_count_histogram_buckets(buckets);
}
}
#[cfg(not(tokio_unstable))]
let _ = (builder, opts);
}
fn apply_timer_opts(builder: &mut Builder, opts: &RuntimeOpts) {
#[cfg(tokio_unstable)]
if opts.enable_alt_timer {
builder.enable_alt_timer();
}
#[cfg(not(tokio_unstable))]
let _ = (builder, opts);
}
#[cfg(feature = "dial9")]
fn build_dial9_runtime(
builder: Builder,
runtime_name: &str,
opts: &Dial9RuntimeOpts,
) -> std::io::Result<(
tokio::runtime::Runtime,
dial9_tokio_telemetry::telemetry::TelemetryGuard,
)> {
use dial9_tokio_telemetry::telemetry::{RotatingWriter, TracedRuntime};
use std::io::{Error, ErrorKind};
if opts.max_file_size == 0 {
return Err(Error::new(
ErrorKind::InvalidInput,
"dial9 max_file_size must be greater than zero",
));
}
if opts.max_total_size == 0 {
return Err(Error::new(
ErrorKind::InvalidInput,
"dial9 max_total_size must be greater than zero",
));
}
if opts.max_file_size > opts.max_total_size {
return Err(Error::new(
ErrorKind::InvalidInput,
"dial9 max_file_size must be less than or equal to max_total_size",
));
}
if opts.worker_poll_interval == Some(Duration::ZERO) {
return Err(Error::new(
ErrorKind::InvalidInput,
"dial9 worker_poll_interval must be greater than zero",
));
}
if let Some(parent) = opts.trace_path.parent() {
std::fs::create_dir_all(parent)?;
}
let writer = RotatingWriter::builder()
.base_path(opts.trace_path.clone())
.max_file_size(opts.max_file_size)
.max_total_size(opts.max_total_size)
.maybe_rotation_period(opts.rotation_period)
.build()?;
let mut traced = TracedRuntime::builder()
.with_trace_path(opts.trace_path.clone())
.with_runtime_name(runtime_name)
.with_task_tracking(opts.task_tracking);
if let Some(worker_poll_interval) = opts.worker_poll_interval {
traced = traced.with_worker_poll_interval(worker_poll_interval);
}
#[cfg(feature = "dial9-worker-s3")]
if let Some(s3_upload) = &opts.s3_upload {
if s3_upload.bucket.trim().is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"dial9 s3 bucket must not be empty",
));
}
if s3_upload.service_name.trim().is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"dial9 s3 service_name must not be empty",
));
}
let s3_config = dial9_tokio_telemetry::background_task::s3::S3Config::builder()
.bucket(s3_upload.bucket.clone())
.service_name(s3_upload.service_name.clone())
.maybe_prefix(s3_upload.prefix.clone())
.maybe_region(s3_upload.region.clone())
.maybe_instance_path(s3_upload.instance_path.clone());
let traced = traced.with_s3_uploader(s3_config.build());
if let Some(client) = s3_upload.client.clone() {
return traced
.with_s3_client(client)
.build_and_start(builder, writer);
}
return traced.build_and_start(builder, writer);
}
traced.build_and_start(builder, writer)
}
pub struct RuntimeBuilder {
threads: usize,
name: String,
work_steal: bool,
blocking_pool_opts: BlockingPoolOpts,
runtime_opts: RuntimeOpts,
}
impl RuntimeBuilder {
pub fn new(threads: usize, name: &str) -> Self {
Self {
threads,
name: name.to_string(),
work_steal: true,
blocking_pool_opts: BlockingPoolOpts::default(),
runtime_opts: RuntimeOpts::default(),
}
}
pub fn work_steal(mut self, enabled: bool) -> Self {
self.work_steal = enabled;
self
}
pub fn blocking_pool_opts(mut self, opts: BlockingPoolOpts) -> Self {
self.blocking_pool_opts = opts;
self
}
pub fn metrics_opts(mut self, opts: RuntimeMetricsOpts) -> Self {
self.runtime_opts.metrics = opts;
self
}
pub fn runtime_opts(mut self, opts: RuntimeOpts) -> Self {
self.runtime_opts = opts;
self
}
pub fn enable_alt_timer(mut self, enabled: bool) -> Self {
self.runtime_opts.enable_alt_timer = enabled;
self
}
fn build_work_stealing_tokio_builder(&self) -> Builder {
let mut builder = Builder::new_multi_thread();
builder
.enable_all()
.worker_threads(self.threads)
.thread_name(&self.name);
apply_blocking_opts(&mut builder, &self.blocking_pool_opts);
apply_metrics_opts(&mut builder, &self.runtime_opts.metrics);
apply_timer_opts(&mut builder, &self.runtime_opts);
builder
}
pub fn build(self) -> Runtime {
if self.work_steal {
let mut builder = self.build_work_stealing_tokio_builder();
#[cfg(feature = "dial9")]
let dial9_guard = if let Some(dial9_opts) = &self.runtime_opts.dial9 {
let runtime_name = self.name.clone();
match build_dial9_runtime(builder, &runtime_name, dial9_opts) {
Ok((runtime, guard)) => {
return Runtime::Steal {
runtime,
dial9_guard: Some(guard),
};
}
Err(e) => {
log::warn!(
"failed to initialize dial9 runtime telemetry for {runtime_name}: {e}"
);
builder = self.build_work_stealing_tokio_builder();
None
}
}
} else {
None
};
let runtime = builder
.build()
.expect("failed to build work-stealing Tokio runtime");
Runtime::Steal {
runtime,
#[cfg(feature = "dial9")]
dial9_guard,
}
} else {
#[cfg(feature = "dial9")]
if self.runtime_opts.dial9.is_some() {
log::warn!("dial9 runtime telemetry is ignored when work stealing is disabled");
}
Runtime::NoSteal(NoStealRuntime::new(
self.threads,
&self.name,
self.blocking_pool_opts,
self.runtime_opts,
))
}
}
}
impl Runtime {
pub fn new_steal(threads: usize, name: &str) -> Self {
RuntimeBuilder::new(threads, name).build()
}
pub fn new_no_steal(threads: usize, name: &str) -> Self {
RuntimeBuilder::new(threads, name).work_steal(false).build()
}
pub fn get_handle(&self) -> &Handle {
match self {
Self::Steal { runtime, .. } => runtime.handle(),
Self::NoSteal(r) => r.get_runtime(),
}
}
pub fn shutdown_timeout(self, timeout: Duration) {
match self {
Self::Steal {
runtime,
#[cfg(feature = "dial9")]
dial9_guard,
} => {
#[cfg(feature = "dial9")]
drop(dial9_guard);
runtime.shutdown_timeout(timeout);
}
Self::NoSteal(r) => r.shutdown_timeout(timeout),
}
}
}
static CURRENT_HANDLE: Lazy<ThreadLocal<Pools>> = Lazy::new(ThreadLocal::new);
pub fn current_handle() -> Handle {
if let Some(pools) = CURRENT_HANDLE.get() {
let pools = pools.get().unwrap();
let mut rng = rand::thread_rng();
let index = rng.gen_range(0..pools.len());
pools[index].clone()
} else {
Handle::current()
}
}
type Control = (Sender<Duration>, JoinHandle<()>);
type Pools = Arc<OnceCell<Box<[Handle]>>>;
pub struct NoStealRuntime {
threads: usize,
name: String,
blocking_opts: BlockingPoolOpts,
runtime_opts: RuntimeOpts,
pools: Pools,
controls: OnceCell<Vec<Control>>,
}
impl NoStealRuntime {
pub fn new(
threads: usize,
name: &str,
blocking_opts: BlockingPoolOpts,
runtime_opts: RuntimeOpts,
) -> Self {
assert!(threads != 0);
NoStealRuntime {
threads,
name: name.to_string(),
blocking_opts,
runtime_opts,
pools: Arc::new(OnceCell::new()),
controls: OnceCell::new(),
}
}
fn init_pools(&self) -> (Box<[Handle]>, Vec<Control>) {
let mut pools = Vec::with_capacity(self.threads);
let mut controls = Vec::with_capacity(self.threads);
for _ in 0..self.threads {
let mut builder = Builder::new_current_thread();
builder.enable_all();
apply_blocking_opts(&mut builder, &self.blocking_opts);
apply_metrics_opts(&mut builder, &self.runtime_opts.metrics);
let rt = builder
.build()
.expect("failed to build no-steal Tokio runtime worker");
let handler = rt.handle().clone();
let (tx, rx) = channel::<Duration>();
let pools_ref = self.pools.clone();
let join = std::thread::Builder::new()
.name(self.name.clone())
.spawn(move || {
CURRENT_HANDLE.get_or(|| pools_ref);
if let Ok(timeout) = rt.block_on(rx) {
rt.shutdown_timeout(timeout);
} })
.unwrap();
pools.push(handler);
controls.push((tx, join));
}
(pools.into_boxed_slice(), controls)
}
pub fn get_runtime(&self) -> &Handle {
let mut rng = rand::thread_rng();
let index = rng.gen_range(0..self.threads);
self.get_runtime_at(index)
}
pub fn threads(&self) -> usize {
self.threads
}
fn get_pools(&self) -> &[Handle] {
if let Some(p) = self.pools.get() {
p
} else {
let (pools, controls) = self.init_pools();
match self.pools.try_insert(pools) {
Ok(p) => {
self.controls.set(controls).unwrap();
p
}
Err((p, _my_pools)) => p,
}
}
}
pub fn get_runtime_at(&self, index: usize) -> &Handle {
let pools = self.get_pools();
&pools[index]
}
pub fn shutdown_timeout(mut self, timeout: Duration) {
if let Some(controls) = self.controls.take() {
let (txs, joins): (Vec<Sender<_>>, Vec<JoinHandle<()>>) = controls.into_iter().unzip();
for tx in txs {
let _ = tx.send(timeout); }
for join in joins {
let _ = join.join(); }
} }
}
#[test]
fn test_steal_runtime() {
use tokio::time::{sleep, Duration};
let threads = 2;
let rt = Runtime::new_steal(threads, "test");
let handle = rt.get_handle();
let ret = handle.block_on(async {
sleep(Duration::from_secs(1)).await;
let handle = current_handle();
let join = handle.spawn(async {
sleep(Duration::from_secs(1)).await;
});
join.await.unwrap();
1
});
#[cfg(target_os = "linux")]
assert_eq!(handle.metrics().num_workers(), threads);
assert_eq!(ret, 1);
}
#[test]
fn test_no_steal_runtime() {
use tokio::time::{sleep, Duration};
let rt = Runtime::new_no_steal(2, "test");
let handle = rt.get_handle();
let ret = handle.block_on(async {
sleep(Duration::from_secs(1)).await;
let handle = current_handle();
let join = handle.spawn(async {
sleep(Duration::from_secs(1)).await;
});
join.await.unwrap();
1
});
assert_eq!(ret, 1);
}
#[test]
fn test_no_steal_shutdown() {
use tokio::time::{sleep, Duration};
let rt = Runtime::new_no_steal(2, "test");
let handle = rt.get_handle();
let ret = handle.block_on(async {
sleep(Duration::from_secs(1)).await;
let handle = current_handle();
let join = handle.spawn(async {
sleep(Duration::from_secs(1)).await;
});
join.await.unwrap();
1
});
assert_eq!(ret, 1);
rt.shutdown_timeout(Duration::from_secs(1));
}
#[cfg(feature = "dial9")]
#[test]
fn test_dial9_zero_worker_poll_interval_is_rejected() {
let mut opts = Dial9RuntimeOpts::new("trace");
opts.worker_poll_interval = Some(Duration::ZERO);
let err = match build_dial9_runtime(Builder::new_multi_thread(), "test", &opts) {
Ok(_) => panic!("zero worker poll interval should be rejected"),
Err(err) => err,
};
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
err.to_string(),
"dial9 worker_poll_interval must be greater than zero"
);
}