-
Notifications
You must be signed in to change notification settings - Fork 30
fix(aisix-etcd): deterministic wait for supervisor cache-write tests #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5c35254
2e3b053
d577baa
198d8d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,6 +22,7 @@ use std::collections::HashMap; | |||||||||||||||||||||||||||||||||||||||||||||||
| use std::sync::Arc; | ||||||||||||||||||||||||||||||||||||||||||||||||
| use std::sync::Mutex; | ||||||||||||||||||||||||||||||||||||||||||||||||
| use std::time::Duration; | ||||||||||||||||||||||||||||||||||||||||||||||||
| use tokio::task::JoinHandle; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::backoff::ExpBackoff; | ||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::key; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -42,6 +43,16 @@ pub struct Supervisor<P: ConfigProvider> { | |||||||||||||||||||||||||||||||||||||||||||||||
| state: Mutex<HashMap<String, RawEntry>>, | ||||||||||||||||||||||||||||||||||||||||||||||||
| revision: Mutex<i64>, | ||||||||||||||||||||||||||||||||||||||||||||||||
| cache: SnapshotCache, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // JoinHandles for in-flight `flush_cache` writes. Tests use | ||||||||||||||||||||||||||||||||||||||||||||||||
| // [`Self::await_pending_cache_writes`] to deterministically wait | ||||||||||||||||||||||||||||||||||||||||||||||||
| // for these without relying on a wall-clock sleep, which proved | ||||||||||||||||||||||||||||||||||||||||||||||||
| // flaky on slow CI runners. Production code does not read this | ||||||||||||||||||||||||||||||||||||||||||||||||
| // field; if a handle is dropped (e.g. during shutdown), the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // underlying write either completed or was cancelled — either | ||||||||||||||||||||||||||||||||||||||||||||||||
| // way the on-disk cache is best-effort and the next live cycle | ||||||||||||||||||||||||||||||||||||||||||||||||
| // re-publishes from etcd. | ||||||||||||||||||||||||||||||||||||||||||||||||
| pending_writes: Mutex<Vec<JoinHandle<()>>>, | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| impl<P: ConfigProvider> Supervisor<P> { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -63,6 +74,25 @@ impl<P: ConfigProvider> Supervisor<P> { | |||||||||||||||||||||||||||||||||||||||||||||||
| state: Mutex::new(HashMap::new()), | ||||||||||||||||||||||||||||||||||||||||||||||||
| revision: Mutex::new(0), | ||||||||||||||||||||||||||||||||||||||||||||||||
| cache, | ||||||||||||||||||||||||||||||||||||||||||||||||
| pending_writes: Mutex::new(Vec::new()), | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /// Drain the JoinHandles for any in-flight cache writes spawned | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// by [`Self::flush_cache`] and await them. Test-only synchroniser: | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// production code never needs to block on disk persistence. | ||||||||||||||||||||||||||||||||||||||||||||||||
| #[cfg(test)] | ||||||||||||||||||||||||||||||||||||||||||||||||
| pub async fn await_pending_cache_writes(&self) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| let handles: Vec<JoinHandle<()>> = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| let mut pending = self.pending_writes.lock().unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| std::mem::take(&mut *pending) | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| for handle in handles { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Failures here are not test failures — a write that | ||||||||||||||||||||||||||||||||||||||||||||||||
| // panicked is its own bug surfaced separately. We only | ||||||||||||||||||||||||||||||||||||||||||||||||
| // need the await to deterministically order against the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // disk read that follows. | ||||||||||||||||||||||||||||||||||||||||||||||||
| let _ = handle.await; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+91
to
+95
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Failures here are not test failures — a write that | |
| // panicked is its own bug surfaced separately. We only | |
| // need the await to deterministically order against the | |
| // disk read that follows. | |
| let _ = handle.await; | |
| // In tests, a cache write task panic/cancellation should fail | |
| // loudly so the underlying bug is surfaced directly instead of | |
| // being masked by whatever assertion runs after the disk read. | |
| handle | |
| .await | |
| .expect("pending cache write task panicked or was cancelled"); |
Copilot
AI
Apr 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
flush_cache now always pushes the spawned JoinHandle into pending_writes. Even if the intention is “test-only”, this path also runs in production and will accumulate handles unless drained. If you keep pending_writes in the struct, gate the push with #[cfg(test)] (and drop the handle otherwise) or drain/prune completed handles here.
| // Track the JoinHandle so tests can deterministically await | |
| // the write via [`Self::await_pending_cache_writes`] instead | |
| // of leaning on `tokio::time::sleep`, which under CI load | |
| // raced the spawn (~50ms wasn't enough on heavily loaded | |
| // GitHub Actions runners). | |
| if let Ok(rt_handle) = tokio::runtime::Handle::try_current() { | |
| let join = | |
| rt_handle.spawn(async move { cache.store(&entries, revision).await }); | |
| self.pending_writes.lock().unwrap().push(join); | |
| // Track the JoinHandle only in tests so they can | |
| // deterministically await the write via | |
| // [`Self::await_pending_cache_writes`] instead of leaning on | |
| // `tokio::time::sleep`, which under CI load raced the spawn | |
| // (~50ms wasn't enough on heavily loaded GitHub Actions | |
| // runners). In non-test builds, drop the handle immediately to | |
| // detach the task and avoid accumulating completed handles. | |
| if let Ok(rt_handle) = tokio::runtime::Handle::try_current() { | |
| let join = | |
| rt_handle.spawn(async move { cache.store(&entries, revision).await }); | |
| #[cfg(test)] | |
| self.pending_writes.lock().unwrap().push(join); | |
| #[cfg(not(test))] | |
| drop(join); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pending_writesretains every spawned cache-writeJoinHandlefor the lifetime ofSupervisor, but production code never drains it. In a long-running process with frequent Put/Delete/Resync events, this will grow unbounded and keep completed tasks alive, leading to a memory leak. Consider making handle tracking#[cfg(test)]only (anddrop(join)in non-test), or pruning finished handles (e.g.,retain(|h| !h.is_finished())) before/after pushing new ones.