use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
use std::sync::{Arc, OnceLock, Weak};
use std::thread::LocalKey;
use aws_config::SdkConfig;
use tokio::runtime::{Handle, Id};
pub(crate) struct Slot<Client> {
owner: Weak<()>,
own: Option<Rc<Client>>,
}
pub(crate) trait SdkClient: Sized + 'static {
fn build(config: &SdkConfig) -> Self;
fn slots() -> &'static LocalKey<RefCell<Vec<Slot<Self>>>>;
}
macro_rules! sdk_client {
($client:ty) => {
impl SdkClient for $client {
fn build(config: &SdkConfig) -> Self {
Self::new(config)
}
fn slots() -> &'static LocalKey<RefCell<Vec<Slot<Self>>>> {
thread_local! {
static SLOTS: RefCell<Vec<Slot<$client>>> = const { RefCell::new(Vec::new()) };
}
&SLOTS
}
}
};
}
sdk_client!(aws_sdk_kinesis::Client);
#[cfg(feature = "dynamodb-lease")]
sdk_client!(aws_sdk_dynamodb::Client);
pub(crate) struct RuntimeClients<Client> {
home: OnceLock<Id>,
client: Client,
config: SdkConfig,
token: Arc<()>,
}
impl<Client: SdkClient> RuntimeClients<Client> {
pub(crate) fn homed(config: SdkConfig, runtime: &Handle) -> Self {
let clients = Self::new(config);
clients.home.get_or_init(|| runtime.id());
clients
}
pub(crate) fn new(config: SdkConfig) -> Self {
Self {
home: OnceLock::new(),
client: Client::build(&config),
config,
token: Arc::new(()),
}
}
pub(crate) const fn home(&self) -> &Client {
&self.client
}
pub(crate) fn with<Output>(&self, request: impl FnOnce(&Client) -> Output) -> Output {
let owner = Arc::as_ptr(&self.token);
let found = Client::slots().try_with(|slots| {
let mut slots = slots.borrow_mut();
if let Some(slot) = slots.iter().find(|slot| slot.owner.as_ptr() == owner) {
return Some(slot.own.clone());
}
let current = Handle::try_current().ok()?.id();
let own = (*self.home.get_or_init(|| current) != current)
.then(|| Rc::new(Client::build(&self.config)));
slots.retain(|slot| slot.owner.strong_count() > 0);
slots.push(Slot {
owner: Arc::downgrade(&self.token),
own: own.clone(),
});
Some(own)
});
match found {
Ok(Some(Some(own))) => request(&own),
_ => request(&self.client),
}
}
}
impl<Client> fmt::Debug for RuntimeClients<Client> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RuntimeClients")
.field("home", &self.home.get())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use std::ptr;
use std::thread;
use aws_config::BehaviorVersion;
use tokio::runtime::{Builder, Runtime};
use super::*;
type Kinesis = aws_sdk_kinesis::Client;
fn config() -> SdkConfig {
SdkConfig::builder()
.behavior_version(BehaviorVersion::latest())
.build()
}
fn current_thread() -> Runtime {
Builder::new_current_thread()
.build()
.expect("a current-thread runtime builds")
}
fn chosen(clients: &RuntimeClients<Kinesis>) -> *const Kinesis {
clients.with(ptr::from_ref)
}
#[test]
fn the_home_runtime_uses_the_connect_client_on_every_worker() {
let runtime = Builder::new_multi_thread()
.worker_threads(2)
.build()
.expect("a multi-thread runtime builds");
let clients = Arc::new(RuntimeClients::<Kinesis>::homed(config(), runtime.handle()));
let home = ptr::from_ref(clients.home()) as usize;
let workers = runtime.block_on(async {
let tasks: Vec<_> = (0..8)
.map(|_| {
let clients = Arc::clone(&clients);
tokio::spawn(async move { chosen(&clients) as usize })
})
.collect();
let mut seen = Vec::new();
for task in tasks {
seen.push(task.await.expect("the task runs"));
}
seen
});
assert!(workers.iter().all(|&client| client == home));
}
#[test]
fn another_runtime_gets_its_own_client_once() {
let home_runtime = current_thread();
let clients = RuntimeClients::<Kinesis>::homed(config(), home_runtime.handle());
let home = ptr::from_ref(clients.home()) as usize;
assert_eq!(
home_runtime.block_on(async { chosen(&clients) as usize }),
home
);
let (first, second) = thread::scope(|scope| {
scope
.spawn(|| {
let runtime = current_thread();
let first = runtime.block_on(async { chosen(&clients) as usize });
let second = runtime.block_on(async { chosen(&clients) as usize });
(first, second)
})
.join()
.expect("the other thread finishes")
});
assert_ne!(first, home);
assert_eq!(first, second);
}
#[test]
fn two_selections_on_one_thread_do_not_share_a_slot() {
let home_runtime = current_thread();
let one = RuntimeClients::<Kinesis>::homed(config(), home_runtime.handle());
let two = RuntimeClients::<Kinesis>::homed(config(), home_runtime.handle());
let (from_one, from_two) = thread::scope(|scope| {
scope
.spawn(|| {
let runtime = current_thread();
runtime.block_on(async { (chosen(&one) as usize, chosen(&two) as usize) })
})
.join()
.expect("the other thread finishes")
});
assert_ne!(from_one, from_two);
assert_ne!(from_one, ptr::from_ref(one.home()) as usize);
assert_ne!(from_two, ptr::from_ref(two.home()) as usize);
}
#[test]
fn an_unhomed_selection_is_homed_by_its_first_request() {
let clients = RuntimeClients::<Kinesis>::new(config());
let home = ptr::from_ref(clients.home()) as usize;
let runtime = current_thread();
assert_eq!(runtime.block_on(async { chosen(&clients) as usize }), home);
let other = thread::scope(|scope| {
scope
.spawn(|| current_thread().block_on(async { chosen(&clients) as usize }))
.join()
.expect("the other thread finishes")
});
assert_ne!(other, home);
}
}