mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
569 lines
20 KiB
Rust
569 lines
20 KiB
Rust
use std::future::Future;
|
|||
|
|
use std::sync::Arc;
|
||
|
|
use std::time::Duration;
|
||
|
|
|
||
|
|
use aether_runtime_state::{RuntimeLockLease, RuntimeState};
|
||
|
|
use aether_task_runtime::TaskSupervisorMetrics;
|
||
|
|
use tokio::task::JoinHandle;
|
||
|
|
use tokio::time::MissedTickBehavior;
|
||
|
|
use tracing::warn;
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
|
|
pub struct SingletonWorkerConfig {
|
||
|
|
pub lease_ttl: Duration,
|
||
|
|
pub renew_interval: Duration,
|
||
|
|
pub retry_interval: Duration,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for SingletonWorkerConfig {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
lease_ttl: Duration::from_secs(90),
|
||
|
|
renew_interval: Duration::from_secs(30),
|
||
|
|
retry_interval: Duration::from_secs(10),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
|
|
pub struct SingletonWorkerContext {
|
||
|
|
task_key: &'static str,
|
||
|
|
owner: String,
|
||
|
|
fencing_token: u64,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl SingletonWorkerContext {
|
||
|
|
pub const fn task_key(&self) -> &'static str {
|
||
|
|
self.task_key
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn owner(&self) -> &str {
|
||
|
|
&self.owner
|
||
|
|
}
|
||
|
|
|
||
|
|
pub const fn fencing_token(&self) -> u64 {
|
||
|
|
self.fencing_token
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct SingletonLeaseGuard {
|
||
|
|
runtime_state: Arc<RuntimeState>,
|
||
|
|
lease: Option<RuntimeLockLease>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Drop for SingletonLeaseGuard {
|
||
|
|
fn drop(&mut self) {
|
||
|
|
let Some(lease) = self.lease.take() else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let runtime_state = self.runtime_state.clone();
|
||
|
|
let Ok(handle) = tokio::runtime::Handle::try_current() else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
handle.spawn(async move {
|
||
|
|
let _ = runtime_state.lock_release(&lease).await;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn spawn_singleton_worker<F, Fut>(
|
||
|
|
runtime_state: Arc<RuntimeState>,
|
||
|
|
metrics: TaskSupervisorMetrics,
|
||
|
|
owner: String,
|
||
|
|
task_key: &'static str,
|
||
|
|
config: SingletonWorkerConfig,
|
||
|
|
worker: F,
|
||
|
|
) -> JoinHandle<()>
|
||
|
|
where
|
||
|
|
F: Fn() -> Fut + Send + 'static,
|
||
|
|
Fut: Future<Output = ()> + Send + 'static,
|
||
|
|
{
|
||
|
|
spawn_singleton_worker_with_context(
|
||
|
|
runtime_state,
|
||
|
|
metrics,
|
||
|
|
owner,
|
||
|
|
task_key,
|
||
|
|
config,
|
||
|
|
move |_| worker(),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn spawn_singleton_worker_with_context<F, Fut>(
|
||
|
|
runtime_state: Arc<RuntimeState>,
|
||
|
|
metrics: TaskSupervisorMetrics,
|
||
|
|
owner: String,
|
||
|
|
task_key: &'static str,
|
||
|
|
config: SingletonWorkerConfig,
|
||
|
|
worker: F,
|
||
|
|
) -> JoinHandle<()>
|
||
|
|
where
|
||
|
|
F: Fn(SingletonWorkerContext) -> Fut + Send + 'static,
|
||
|
|
Fut: Future<Output = ()> + Send + 'static,
|
||
|
|
{
|
||
|
|
tokio::spawn(async move {
|
||
|
|
let lock_key = format!("task_runtime:singleton:{task_key}");
|
||
|
|
loop {
|
||
|
|
let lease = loop {
|
||
|
|
match runtime_state
|
||
|
|
.lock_try_acquire(&lock_key, &owner, config.lease_ttl)
|
||
|
|
.await
|
||
|
|
{
|
||
|
|
Ok(Some(lease)) => break lease,
|
||
|
|
Ok(None) => {
|
||
|
|
metrics.record_singleton_lease_contention(task_key);
|
||
|
|
tokio::time::sleep(config.retry_interval).await;
|
||
|
|
}
|
||
|
|
Err(error) => {
|
||
|
|
metrics.record_singleton_lease_error(task_key);
|
||
|
|
warn!(
|
||
|
|
event_name = "singleton_task_lease_acquire_failed",
|
||
|
|
log_type = "ops",
|
||
|
|
task = task_key,
|
||
|
|
owner = %owner,
|
||
|
|
error = ?error,
|
||
|
|
"singleton worker could not acquire lease"
|
||
|
|
);
|
||
|
|
tokio::time::sleep(config.retry_interval).await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
let worker_context = SingletonWorkerContext {
|
||
|
|
task_key,
|
||
|
|
owner: owner.clone(),
|
||
|
|
fencing_token: lease.fencing_token,
|
||
|
|
};
|
||
|
|
let mut lease_guard = SingletonLeaseGuard {
|
||
|
|
runtime_state: runtime_state.clone(),
|
||
|
|
lease: Some(lease),
|
||
|
|
};
|
||
|
|
let restart_after_release = {
|
||
|
|
let worker_future = worker(worker_context);
|
||
|
|
tokio::pin!(worker_future);
|
||
|
|
let mut renew_timer = tokio::time::interval(config.renew_interval);
|
||
|
|
renew_timer.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||
|
|
renew_timer.tick().await;
|
||
|
|
|
||
|
|
loop {
|
||
|
|
tokio::select! {
|
||
|
|
_ = &mut worker_future => break false,
|
||
|
|
_ = renew_timer.tick() => {
|
||
|
|
let Some(lease) = lease_guard.lease.as_ref() else {
|
||
|
|
break false;
|
||
|
|
};
|
||
|
|
match runtime_state.lock_renew(lease, config.lease_ttl).await {
|
||
|
|
Ok(true) => {}
|
||
|
|
Ok(false) => {
|
||
|
|
metrics.record_singleton_lease_lost(task_key);
|
||
|
|
warn!(
|
||
|
|
event_name = "singleton_task_lease_lost",
|
||
|
|
log_type = "ops",
|
||
|
|
task = task_key,
|
||
|
|
owner = %owner,
|
||
|
|
"singleton worker lease is no longer owned"
|
||
|
|
);
|
||
|
|
break true;
|
||
|
|
}
|
||
|
|
Err(error) => {
|
||
|
|
metrics.record_singleton_lease_lost(task_key);
|
||
|
|
metrics.record_singleton_lease_error(task_key);
|
||
|
|
warn!(
|
||
|
|
event_name = "singleton_task_lease_renew_failed",
|
||
|
|
log_type = "ops",
|
||
|
|
task = task_key,
|
||
|
|
owner = %owner,
|
||
|
|
error = ?error,
|
||
|
|
"singleton worker lease renewal failed"
|
||
|
|
);
|
||
|
|
break true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
let release_result = if let Some(lease) = lease_guard.lease.as_ref() {
|
||
|
|
runtime_state.lock_release(lease).await
|
||
|
|
} else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
if let Err(error) = release_result {
|
||
|
|
metrics.record_singleton_lease_error(task_key);
|
||
|
|
warn!(
|
||
|
|
event_name = "singleton_task_lease_release_failed",
|
||
|
|
log_type = "ops",
|
||
|
|
task = task_key,
|
||
|
|
owner = %owner,
|
||
|
|
error = ?error,
|
||
|
|
"singleton worker lease release failed"
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
lease_guard.lease.take();
|
||
|
|
}
|
||
|
|
|
||
|
|
if !restart_after_release {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
tokio::time::sleep(config.retry_interval).await;
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::{
|
||
|
|
spawn_singleton_worker, spawn_singleton_worker_with_context, SingletonWorkerConfig,
|
||
|
|
};
|
||
|
|
use aether_runtime_state::{MemoryRuntimeStateConfig, RuntimeState};
|
||
|
|
use aether_task_runtime::TaskSupervisorMetrics;
|
||
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||
|
|
use std::sync::Arc;
|
||
|
|
use std::time::Duration;
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn allows_only_one_owner_for_shared_runtime() {
|
||
|
|
let runtime = Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
|
||
|
|
let metrics = TaskSupervisorMetrics::default();
|
||
|
|
let executions = Arc::new(AtomicUsize::new(0));
|
||
|
|
let config = SingletonWorkerConfig {
|
||
|
|
lease_ttl: Duration::from_millis(60),
|
||
|
|
renew_interval: Duration::from_millis(15),
|
||
|
|
retry_interval: Duration::from_millis(5),
|
||
|
|
};
|
||
|
|
|
||
|
|
let first = spawn_singleton_worker(
|
||
|
|
runtime.clone(),
|
||
|
|
metrics.clone(),
|
||
|
|
"node-a".to_string(),
|
||
|
|
"test.singleton",
|
||
|
|
config,
|
||
|
|
{
|
||
|
|
let executions = executions.clone();
|
||
|
|
move || {
|
||
|
|
let executions = executions.clone();
|
||
|
|
async move {
|
||
|
|
executions.fetch_add(1, Ordering::SeqCst);
|
||
|
|
std::future::pending::<()>().await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
let second = spawn_singleton_worker(
|
||
|
|
runtime,
|
||
|
|
metrics.clone(),
|
||
|
|
"node-b".to_string(),
|
||
|
|
"test.singleton",
|
||
|
|
config,
|
||
|
|
{
|
||
|
|
let executions = executions.clone();
|
||
|
|
move || {
|
||
|
|
let executions = executions.clone();
|
||
|
|
async move {
|
||
|
|
executions.fetch_add(1, Ordering::SeqCst);
|
||
|
|
std::future::pending::<()>().await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
tokio::time::sleep(Duration::from_millis(160)).await;
|
||
|
|
assert_eq!(executions.load(Ordering::SeqCst), 1);
|
||
|
|
assert!(metrics.snapshot().singleton_lease_contention_total > 0);
|
||
|
|
first.abort();
|
||
|
|
second.abort();
|
||
|
|
let _ = first.await;
|
||
|
|
let _ = second.await;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn shutdown_releases_lease_before_ttl_expires() {
|
||
|
|
let runtime = Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
|
||
|
|
let metrics = TaskSupervisorMetrics::default();
|
||
|
|
let executions = Arc::new(AtomicUsize::new(0));
|
||
|
|
let config = SingletonWorkerConfig {
|
||
|
|
lease_ttl: Duration::from_secs(5),
|
||
|
|
renew_interval: Duration::from_secs(1),
|
||
|
|
retry_interval: Duration::from_millis(5),
|
||
|
|
};
|
||
|
|
|
||
|
|
let first = spawn_singleton_worker(
|
||
|
|
runtime.clone(),
|
||
|
|
metrics.clone(),
|
||
|
|
"node-a".to_string(),
|
||
|
|
"test.shutdown",
|
||
|
|
config,
|
||
|
|
{
|
||
|
|
let executions = executions.clone();
|
||
|
|
move || {
|
||
|
|
let executions = executions.clone();
|
||
|
|
async move {
|
||
|
|
executions.fetch_add(1, Ordering::SeqCst);
|
||
|
|
std::future::pending::<()>().await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
||
|
|
while executions.load(Ordering::SeqCst) < 1 {
|
||
|
|
tokio::task::yield_now().await;
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.await
|
||
|
|
.expect("first owner should start");
|
||
|
|
first.abort();
|
||
|
|
let _ = first.await;
|
||
|
|
|
||
|
|
let second = spawn_singleton_worker(
|
||
|
|
runtime,
|
||
|
|
metrics,
|
||
|
|
"node-b".to_string(),
|
||
|
|
"test.shutdown",
|
||
|
|
config,
|
||
|
|
{
|
||
|
|
let executions = executions.clone();
|
||
|
|
move || {
|
||
|
|
let executions = executions.clone();
|
||
|
|
async move {
|
||
|
|
executions.fetch_add(1, Ordering::SeqCst);
|
||
|
|
std::future::pending::<()>().await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
||
|
|
while executions.load(Ordering::SeqCst) < 2 {
|
||
|
|
tokio::task::yield_now().await;
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.await
|
||
|
|
.expect("second owner should take over without waiting for the lease TTL");
|
||
|
|
|
||
|
|
second.abort();
|
||
|
|
let _ = second.await;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn passes_monotonic_fencing_token_to_worker_context() {
|
||
|
|
let runtime = Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
|
||
|
|
let metrics = TaskSupervisorMetrics::default();
|
||
|
|
let config = SingletonWorkerConfig {
|
||
|
|
lease_ttl: Duration::from_secs(1),
|
||
|
|
renew_interval: Duration::from_millis(100),
|
||
|
|
retry_interval: Duration::from_millis(5),
|
||
|
|
};
|
||
|
|
|
||
|
|
let (first_tx, mut first_rx) = tokio::sync::mpsc::unbounded_channel();
|
||
|
|
spawn_singleton_worker_with_context(
|
||
|
|
runtime.clone(),
|
||
|
|
metrics.clone(),
|
||
|
|
"node-a".to_string(),
|
||
|
|
"test.context",
|
||
|
|
config,
|
||
|
|
move |context| {
|
||
|
|
let first_tx = first_tx.clone();
|
||
|
|
async move {
|
||
|
|
first_tx.send(context).expect("send first context");
|
||
|
|
}
|
||
|
|
},
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.expect("first worker should complete");
|
||
|
|
let first = first_rx.recv().await.expect("first context");
|
||
|
|
assert_eq!(first.task_key(), "test.context");
|
||
|
|
assert_eq!(first.owner(), "node-a");
|
||
|
|
|
||
|
|
let (second_tx, mut second_rx) = tokio::sync::mpsc::unbounded_channel();
|
||
|
|
spawn_singleton_worker_with_context(
|
||
|
|
runtime,
|
||
|
|
metrics,
|
||
|
|
"node-b".to_string(),
|
||
|
|
"test.context",
|
||
|
|
config,
|
||
|
|
move |context| {
|
||
|
|
let second_tx = second_tx.clone();
|
||
|
|
async move {
|
||
|
|
second_tx.send(context).expect("send second context");
|
||
|
|
}
|
||
|
|
},
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.expect("second worker should complete");
|
||
|
|
let second = second_rx.recv().await.expect("second context");
|
||
|
|
assert!(second.fencing_token() > first.fencing_token());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn restarts_worker_after_lease_expires_before_renewal() {
|
||
|
|
let runtime = Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
|
||
|
|
let metrics = TaskSupervisorMetrics::default();
|
||
|
|
let config = SingletonWorkerConfig {
|
||
|
|
lease_ttl: Duration::from_millis(20),
|
||
|
|
renew_interval: Duration::from_millis(50),
|
||
|
|
retry_interval: Duration::from_millis(5),
|
||
|
|
};
|
||
|
|
let (context_tx, mut context_rx) = tokio::sync::mpsc::unbounded_channel();
|
||
|
|
|
||
|
|
let handle = spawn_singleton_worker_with_context(
|
||
|
|
runtime,
|
||
|
|
metrics.clone(),
|
||
|
|
"node-a".to_string(),
|
||
|
|
"test.restart.expired",
|
||
|
|
config,
|
||
|
|
move |context| {
|
||
|
|
let context_tx = context_tx.clone();
|
||
|
|
async move {
|
||
|
|
context_tx.send(context).expect("send worker context");
|
||
|
|
std::future::pending::<()>().await;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
let first = tokio::time::timeout(Duration::from_secs(1), context_rx.recv())
|
||
|
|
.await
|
||
|
|
.expect("first worker should start")
|
||
|
|
.expect("first worker context");
|
||
|
|
let second = tokio::time::timeout(Duration::from_secs(1), context_rx.recv())
|
||
|
|
.await
|
||
|
|
.expect("worker should restart after losing its lease")
|
||
|
|
.expect("restarted worker context");
|
||
|
|
|
||
|
|
assert!(second.fencing_token() > first.fencing_token());
|
||
|
|
assert!(metrics.snapshot().singleton_lease_lost_total >= 1);
|
||
|
|
handle.abort();
|
||
|
|
let _ = handle.await;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn restarts_worker_after_redis_renew_error_when_backend_recovers() {
|
||
|
|
let Ok(mut server) = aether_test_support::ManagedRedisServer::start().await else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let redis_config = aether_runtime_state::RedisClientConfig {
|
||
|
|
url: server.redis_url().to_string(),
|
||
|
|
key_prefix: Some(format!(
|
||
|
|
"aether-gateway-workers-restart-test-{}",
|
||
|
|
std::process::id()
|
||
|
|
)),
|
||
|
|
};
|
||
|
|
let Ok(runtime) = RuntimeState::redis(redis_config, Some(100)).await else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let metrics = TaskSupervisorMetrics::default();
|
||
|
|
let config = SingletonWorkerConfig {
|
||
|
|
lease_ttl: Duration::from_millis(500),
|
||
|
|
renew_interval: Duration::from_millis(25),
|
||
|
|
retry_interval: Duration::from_millis(10),
|
||
|
|
};
|
||
|
|
let (started_tx, mut started_rx) = tokio::sync::mpsc::unbounded_channel();
|
||
|
|
let handle = spawn_singleton_worker(
|
||
|
|
Arc::new(runtime),
|
||
|
|
metrics.clone(),
|
||
|
|
"redis-node-a".to_string(),
|
||
|
|
"test.restart.redis-error",
|
||
|
|
config,
|
||
|
|
move || {
|
||
|
|
let started_tx = started_tx.clone();
|
||
|
|
async move {
|
||
|
|
started_tx.send(()).expect("record worker start");
|
||
|
|
std::future::pending::<()>().await;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
tokio::time::timeout(Duration::from_secs(1), started_rx.recv())
|
||
|
|
.await
|
||
|
|
.expect("first worker should start")
|
||
|
|
.expect("first worker start notification");
|
||
|
|
server.stop().expect("stop redis server");
|
||
|
|
tokio::time::timeout(Duration::from_secs(2), async {
|
||
|
|
while metrics.snapshot().singleton_lease_error_total == 0 {
|
||
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.await
|
||
|
|
.expect("renewal should observe the redis outage");
|
||
|
|
|
||
|
|
server.restart().await.expect("restart redis server");
|
||
|
|
tokio::time::timeout(Duration::from_secs(3), started_rx.recv())
|
||
|
|
.await
|
||
|
|
.expect("worker should restart after redis recovers")
|
||
|
|
.expect("restarted worker notification");
|
||
|
|
assert!(metrics.snapshot().singleton_lease_lost_total >= 1);
|
||
|
|
|
||
|
|
handle.abort();
|
||
|
|
let _ = handle.await;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn redis_backend_allows_only_one_owner_when_available() {
|
||
|
|
let Ok(server) = aether_test_support::ManagedRedisServer::start().await else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let config = aether_runtime_state::RedisClientConfig {
|
||
|
|
url: server.redis_url().to_string(),
|
||
|
|
key_prefix: Some(format!(
|
||
|
|
"aether-gateway-workers-test-{}",
|
||
|
|
std::process::id()
|
||
|
|
)),
|
||
|
|
};
|
||
|
|
let Ok(runtime_a) = RuntimeState::redis(config.clone(), Some(500)).await else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let Ok(runtime_b) = RuntimeState::redis(config, Some(500)).await else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let runtime_a = Arc::new(runtime_a);
|
||
|
|
let runtime_b = Arc::new(runtime_b);
|
||
|
|
let metrics = TaskSupervisorMetrics::default();
|
||
|
|
let executions = Arc::new(AtomicUsize::new(0));
|
||
|
|
let config = SingletonWorkerConfig {
|
||
|
|
lease_ttl: Duration::from_millis(120),
|
||
|
|
renew_interval: Duration::from_millis(30),
|
||
|
|
retry_interval: Duration::from_millis(5),
|
||
|
|
};
|
||
|
|
|
||
|
|
let first = spawn_singleton_worker(
|
||
|
|
runtime_a,
|
||
|
|
metrics.clone(),
|
||
|
|
"redis-node-a".to_string(),
|
||
|
|
"test.redis.singleton",
|
||
|
|
config,
|
||
|
|
{
|
||
|
|
let executions = executions.clone();
|
||
|
|
move || {
|
||
|
|
let executions = executions.clone();
|
||
|
|
async move {
|
||
|
|
executions.fetch_add(1, Ordering::SeqCst);
|
||
|
|
std::future::pending::<()>().await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
let second = spawn_singleton_worker(
|
||
|
|
runtime_b,
|
||
|
|
metrics.clone(),
|
||
|
|
"redis-node-b".to_string(),
|
||
|
|
"test.redis.singleton",
|
||
|
|
config,
|
||
|
|
{
|
||
|
|
let executions = executions.clone();
|
||
|
|
move || {
|
||
|
|
let executions = executions.clone();
|
||
|
|
async move {
|
||
|
|
executions.fetch_add(1, Ordering::SeqCst);
|
||
|
|
std::future::pending::<()>().await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||
|
|
assert_eq!(executions.load(Ordering::SeqCst), 1);
|
||
|
|
first.abort();
|
||
|
|
second.abort();
|
||
|
|
let _ = first.await;
|
||
|
|
let _ = second.await;
|
||
|
|
}
|
||
|
|
}
|