Skip to main content

kernel_manager/
kernels.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::{StarnixKernel, generate_kernel_name};
6use anyhow::Error;
7use fidl::endpoints::{Proxy, ServerEnd};
8use fidl_fuchsia_component as fcomponent;
9use fidl_fuchsia_component_runner as frunner;
10use fidl_fuchsia_power_system as fpower;
11use frunner::{ComponentControllerMarker, ComponentStartInfo};
12use fuchsia_async as fasync;
13use fuchsia_component::client::connect_to_protocol;
14use futures::FutureExt;
15use futures::channel::oneshot;
16use starnix_sync::{
17    ActivityGovernorProxyLock, KernelsLock, LockDepMutex, StarnixKernelWakeLeaseLock,
18};
19use std::collections::HashMap;
20use std::sync::Arc;
21use vfs::execution_scope::ExecutionScope;
22use zx;
23
24/// The component URL of the Starnix kernel.
25const KERNEL_URL: &str = "starnix_kernel#meta/starnix_kernel.cm";
26
27/// Create the power lease name for better readability based on the Starnix kernel name.
28fn create_lease_name(kernel_name: &str) -> String {
29    format!("starnix-kernel-{}", kernel_name)
30}
31
32type InFlightWakeLease = futures::future::Shared<oneshot::Receiver<()>>;
33
34#[derive(Default)]
35struct WakeLeaseState {
36    /// Monotonically increasing generation bumped on every acquire or drop transition so stale
37    /// in-flight completions never resurrect a dropped or superseded wake lease.
38    generation: u64,
39    /// Generation of the task currently stored in `in_flight`, so the latest background task clears
40    /// `in_flight` upon completion even if `drop_wake_lease` bumped `generation` in the meantime.
41    in_flight_generation: u64,
42    /// The currently active wake lease for the container.
43    lease: Option<zx::EventPair>,
44    /// Completion handle for the latest in-flight `TakeApplicationActivityLease` task, which
45    /// transitively awaits any earlier in-flight task for this kernel.
46    in_flight: Option<InFlightWakeLease>,
47}
48
49/// Per-kernel wake lease tracker shared between [`Kernels`] and background acquisition tasks.
50pub(crate) struct KernelWakeLease {
51    lease_name: String,
52    state: LockDepMutex<WakeLeaseState, StarnixKernelWakeLeaseLock>,
53}
54
55impl KernelWakeLease {
56    pub(crate) fn new(kernel_name: &str) -> Self {
57        Self {
58            lease_name: create_lease_name(kernel_name),
59            state: LockDepMutex::new(WakeLeaseState::default()),
60        }
61    }
62
63    fn set_initial_lease(&self, lease: Option<zx::EventPair>) {
64        let replaced = std::mem::replace(&mut self.state.lock().lease, lease);
65        drop(replaced);
66    }
67
68    fn clear_on_stop(&self) {
69        let stale_lease = {
70            let mut state = self.state.lock();
71            state.generation += 1;
72            state.in_flight_generation = state.generation;
73            state.in_flight = None;
74            state.lease.take()
75        };
76        drop(stale_lease);
77    }
78
79    fn current_generation(&self) -> u64 {
80        self.state.lock().generation
81    }
82
83    /// Stores `lease` if `target_generation` still matches the latest generation; otherwise returns
84    /// `Err(lease)` so the caller drops the superseded lease outside the lock.
85    fn finish_acquire(
86        &self,
87        target_generation: u64,
88        lease: zx::EventPair,
89    ) -> Result<(), zx::EventPair> {
90        let replaced = {
91            let mut state = self.state.lock();
92            if state.generation != target_generation {
93                return Err(lease);
94            }
95            state.lease.replace(lease)
96        };
97        drop(replaced);
98        Ok(())
99    }
100
101    fn clear_in_flight_if_current(&self, target_generation: u64) {
102        let mut state = self.state.lock();
103        if state.in_flight_generation == target_generation {
104            state.in_flight = None;
105        }
106    }
107
108    #[cfg(test)]
109    pub(crate) fn has_lease(&self) -> bool {
110        self.state.lock().lease.is_some()
111    }
112}
113
114#[derive(Default)]
115struct ActivityGovernorCache {
116    proxy: LockDepMutex<Option<fpower::ActivityGovernorProxy>, ActivityGovernorProxyLock>,
117}
118
119impl ActivityGovernorCache {
120    /// Returns a cached `ActivityGovernorProxy`, connecting to the protocol if no open connection
121    /// is cached. Also returns whether the returned proxy came from the cache.
122    fn get(&self) -> Result<(fpower::ActivityGovernorProxy, bool), Error> {
123        let mut guard = self.proxy.lock();
124        if let Some(proxy) = guard.as_ref() {
125            if !proxy.is_closed() {
126                return Ok((proxy.clone(), true));
127            }
128        }
129        let proxy = connect_to_protocol::<fpower::ActivityGovernorMarker>()?;
130        *guard = Some(proxy.clone());
131        Ok((proxy, false))
132    }
133
134    /// Evicts `failed_proxy` from the cache if it is still the cached proxy or has closed.
135    fn evict(&self, failed_proxy: &fpower::ActivityGovernorProxy) {
136        let mut guard = self.proxy.lock();
137        if guard.as_ref().is_some_and(|cached| {
138            cached.is_closed()
139                || cached.as_channel().as_ref().raw_handle()
140                    == failed_proxy.as_channel().as_ref().raw_handle()
141        }) {
142            *guard = None;
143        }
144    }
145
146    /// Requests an application activity lease for `lease_name` via `activity_governor`,
147    /// reconnecting once if a previously cached proxy disconnected.
148    async fn take_application_activity_lease(
149        &self,
150        activity_governor: fpower::ActivityGovernorProxy,
151        was_cached: bool,
152        lease_name: &str,
153    ) -> Result<Option<zx::EventPair>, Error> {
154        match activity_governor.take_application_activity_lease(lease_name).await {
155            Ok(lease) => Ok(Some(lease)),
156            Err(e) if was_cached && e.is_closed() => {
157                self.evict(&activity_governor);
158                let (activity_governor, _) = self.get()?;
159                match activity_governor.take_application_activity_lease(lease_name).await {
160                    Ok(lease) => Ok(Some(lease)),
161                    Err(e) => {
162                        if e.is_closed() {
163                            self.evict(&activity_governor);
164                        }
165                        log::warn!(
166                            "Failed to acquire application activity lease for kernel: {:?}",
167                            e
168                        );
169                        Ok(None)
170                    }
171                }
172            }
173            Err(e) => {
174                if e.is_closed() {
175                    self.evict(&activity_governor);
176                }
177                log::warn!("Failed to acquire application activity lease for kernel: {:?}", e);
178                Ok(None)
179            }
180        }
181    }
182}
183
184/// [`Kernels`] manages a collection of starnix kernels.
185pub struct Kernels {
186    kernels: Arc<LockDepMutex<HashMap<zx::Koid, StarnixKernel>, KernelsLock>>,
187    background_tasks: ExecutionScope,
188    wake_lease_scope: fasync::Scope,
189    activity_governor: Arc<ActivityGovernorCache>,
190}
191
192impl Kernels {
193    /// Creates a new [`Kernels`] instance.
194    pub fn new() -> Self {
195        Self {
196            kernels: Default::default(),
197            background_tasks: ExecutionScope::new(),
198            wake_lease_scope: fasync::Scope::new_with_name("starnix_wake_leases"),
199            activity_governor: Arc::new(ActivityGovernorCache::default()),
200        }
201    }
202
203    #[cfg(test)]
204    pub(crate) fn new_for_test(
205        kernel_job: Arc<zx::Job>,
206        kernel_name: impl Into<String>,
207        activity_governor: fpower::ActivityGovernorProxy,
208    ) -> Result<Self, Error> {
209        let kernels = Self::new();
210        *kernels.activity_governor.proxy.lock() = Some(activity_governor);
211        let (exposed_dir, _server) =
212            fidl::endpoints::create_proxy::<fidl_fuchsia_io::DirectoryMarker>();
213        let job_koid = kernel_job.koid()?;
214        let name = kernel_name.into();
215        let wake_lease = Arc::new(KernelWakeLease::new(&name));
216        kernels.kernels.lock().insert(
217            job_koid,
218            StarnixKernel {
219                name,
220                exposed_dir,
221                component_instance: zx::Event::create(),
222                job: kernel_job,
223                wake_lease,
224            },
225        );
226        Ok(kernels)
227    }
228
229    #[cfg(test)]
230    pub(crate) fn has_wake_lease(&self, container_job: &zx::Job) -> bool {
231        let Ok(job_koid) = container_job.koid() else {
232            return false;
233        };
234        self.kernels.lock().get(&job_koid).is_some_and(|kernel| kernel.wake_lease.has_lease())
235    }
236
237    #[cfg(test)]
238    pub(crate) async fn wait_for_in_flight_wake_lease(&self, container_job: &zx::Job) {
239        let Ok(job_koid) = container_job.koid() else {
240            return;
241        };
242        let in_flight = self
243            .kernels
244            .lock()
245            .get(&job_koid)
246            .and_then(|kernel| kernel.wake_lease.state.lock().in_flight.clone());
247        if let Some(in_flight) = in_flight {
248            let _ = in_flight.await;
249        }
250    }
251
252    /// Runs a new starnix kernel and adds it to the collection.
253    pub async fn start(
254        &self,
255        start_info: ComponentStartInfo,
256        controller: ServerEnd<ComponentControllerMarker>,
257    ) -> Result<(), Error> {
258        let realm =
259            connect_to_protocol::<fcomponent::RealmMarker>().expect("Failed to connect to realm.");
260
261        let kernel_name = generate_kernel_name(&start_info)?;
262        let lease_name = create_lease_name(&kernel_name);
263        let wake_lease = match self.activity_governor.get() {
264            Ok((activity_governor, was_cached)) => self
265                .activity_governor
266                .take_application_activity_lease(activity_governor, was_cached, &lease_name)
267                .await
268                .unwrap_or(None),
269            Err(_) => None,
270        };
271
272        if let Ok(boot_control) = connect_to_protocol::<fpower::BootControlMarker>() {
273            log::info!(
274                "Notifying Fuchsia SystemActivityGovernor that Starnix has booted to allow suspend"
275            );
276            let _ = boot_control.set_boot_complete().await;
277            if let Err(e) = boot_control.set_boot_complete().await {
278                log::warn!(
279                    "Failed to notify Fuchsia SystemActivityGovernor that Starnix has booted: {:?}",
280                    e
281                );
282            }
283        }
284
285        let (kernel, on_stop) =
286            StarnixKernel::create(realm, KERNEL_URL, start_info, controller).await?;
287        let kernel_job = kernel.job.clone();
288        let kernel_koid = kernel.job.koid()?;
289
290        kernel.wake_lease.set_initial_lease(wake_lease);
291        log::info!("Acquired wake lease for {:?}", kernel_job);
292
293        self.kernels.lock().insert(kernel_koid, kernel);
294
295        let kernels = self.kernels.clone();
296        self.background_tasks.spawn(async move {
297            on_stop.await;
298            let removed = kernels.lock().remove(&kernel_koid);
299            if let Some(removed) = removed {
300                removed.wake_lease.clear_on_stop();
301            }
302        });
303
304        Ok(())
305    }
306
307    /// Gets a momentary snapshot of all kernel jobs.
308    pub fn all_jobs(&self) -> Vec<Arc<zx::Job>> {
309        self.kernels.lock().iter().map(|(_, k)| Arc::clone(k.job())).collect()
310    }
311
312    /// Drops any active wake lease for the container running in the given `container_job`,
313    /// waiting for any in-flight `TakeApplicationActivityLease` request for that container to
314    /// resolve and drop its lease first.
315    pub async fn drop_wake_lease(&self, container_job: &zx::Job) -> Result<(), Error> {
316        // LINT.IfChange
317        fuchsia_trace::instant!(
318            "power",
319            "starnix-runner:drop-application-activity-lease",
320            fuchsia_trace::Scope::Process
321        );
322        // LINT.ThenChange(//src/performance/lib/trace_processing/metrics/suspend.py)
323        let job_koid = container_job.koid()?;
324        let wake_lease_tracker =
325            self.kernels.lock().get(&job_koid).map(|kernel| Arc::clone(&kernel.wake_lease));
326        if let Some(wake_lease_tracker) = wake_lease_tracker {
327            let (active_lease, in_flight) = {
328                let mut state = wake_lease_tracker.state.lock();
329                state.generation += 1;
330                (state.lease.take(), state.in_flight.clone())
331            };
332            drop(active_lease);
333            if let Some(in_flight) = in_flight {
334                let _ = in_flight.await;
335            }
336            log::info!("Dropped wake lease for {:?}", container_job);
337        }
338        Ok(())
339    }
340
341    /// Synchronously writes a `TakeApplicationActivityLease` request to System Activity Governor
342    /// for `container_job` and spawns a background task on [`Kernels`]'s scope to store the lease
343    /// once SAG replies.
344    pub fn start_acquire_wake_lease(&self, container_job: &zx::Job) -> Result<(), Error> {
345        let _ = self.start_acquire_wake_lease_inner(container_job)?;
346        Ok(())
347    }
348
349    /// Acquires a wake lease for the container running in the given `container_job` and waits for
350    /// SAG to return and store the lease.
351    pub async fn acquire_wake_lease(&self, container_job: &zx::Job) -> Result<(), Error> {
352        if let Some(in_flight) = self.start_acquire_wake_lease_inner(container_job)? {
353            let _ = in_flight.await;
354        }
355        Ok(())
356    }
357
358    fn start_acquire_wake_lease_inner(
359        &self,
360        container_job: &zx::Job,
361    ) -> Result<Option<InFlightWakeLease>, Error> {
362        // LINT.IfChange
363        fuchsia_trace::duration!("power", "starnix-runner:acquire-application-activity-lease");
364        // LINT.ThenChange(//src/performance/lib/trace_processing/metrics/suspend.py)
365        let job_koid = container_job.koid()?;
366
367        let Some(wake_lease_tracker) =
368            self.kernels.lock().get(&job_koid).map(|kernel| Arc::clone(&kernel.wake_lease))
369        else {
370            return Ok(None);
371        };
372
373        let (mut activity_governor, mut was_cached) = self.activity_governor.get()?;
374        // Synchronously encode and write `TakeApplicationActivityLease` into SAG's channel before
375        // returning so SAG starts resuming drivers immediately.
376        let lease_fut = match activity_governor
377            .take_application_activity_lease(&wake_lease_tracker.lease_name)
378            .check()
379        {
380            Ok(fut) => fut,
381            Err(e) if was_cached && e.is_closed() => {
382                self.activity_governor.evict(&activity_governor);
383                let (reconnected, _) = self.activity_governor.get()?;
384                activity_governor = reconnected;
385                was_cached = false;
386                match activity_governor
387                    .take_application_activity_lease(&wake_lease_tracker.lease_name)
388                    .check()
389                {
390                    Ok(fut) => fut,
391                    Err(e) => {
392                        if e.is_closed() {
393                            self.activity_governor.evict(&activity_governor);
394                        }
395                        log::warn!(
396                            "Failed to acquire application activity lease for kernel: {:?}",
397                            e
398                        );
399                        return Ok(None);
400                    }
401                }
402            }
403            Err(e) => {
404                if e.is_closed() {
405                    self.activity_governor.evict(&activity_governor);
406                }
407                log::warn!("Failed to acquire application activity lease for kernel: {:?}", e);
408                return Ok(None);
409            }
410        };
411
412        let (done_tx, done_rx) = oneshot::channel::<()>();
413        let in_flight = done_rx.shared();
414        let (target_generation, prev_in_flight) = {
415            let mut state = wake_lease_tracker.state.lock();
416            state.generation += 1;
417            state.in_flight_generation = state.generation;
418            let prev = state.in_flight.replace(in_flight.clone());
419            (state.generation, prev)
420        };
421
422        let activity_governor_cache = Arc::clone(&self.activity_governor);
423        let _join_handle = self.wake_lease_scope.spawn(async move {
424            let _done_guard = scopeguard::guard(done_tx, |tx| {
425                let _ = tx.send(());
426            });
427
428            let maybe_lease = match lease_fut.await {
429                Ok(lease) => Some(lease),
430                Err(e) if was_cached && e.is_closed() => {
431                    activity_governor_cache.evict(&activity_governor);
432                    if wake_lease_tracker.current_generation() != target_generation {
433                        None
434                    } else {
435                        match activity_governor_cache.get() {
436                            Ok((reconnected, _)) => {
437                                match reconnected
438                                    .take_application_activity_lease(&wake_lease_tracker.lease_name)
439                                    .await
440                                {
441                                    Ok(lease) => Some(lease),
442                                    Err(e) => {
443                                        if e.is_closed() {
444                                            activity_governor_cache.evict(&reconnected);
445                                        }
446                                        log::warn!(
447                                            "Failed to acquire application activity lease for kernel: {:?}",
448                                            e
449                                        );
450                                        None
451                                    }
452                                }
453                            }
454                            Err(e) => {
455                                log::warn!(
456                                    "Failed to reconnect to ActivityGovernor for kernel wake lease: {:?}",
457                                    e
458                                );
459                                None
460                            }
461                        }
462                    }
463                }
464                Err(e) => {
465                    if e.is_closed() {
466                        activity_governor_cache.evict(&activity_governor);
467                    }
468                    log::warn!("Failed to acquire application activity lease for kernel: {:?}", e);
469                    None
470                }
471            };
472
473            if let Some(wake_lease) = maybe_lease {
474                match wake_lease_tracker.finish_acquire(target_generation, wake_lease) {
475                    Ok(()) => {
476                        log::info!("Acquired wake lease for job koid {:?}", job_koid);
477                    }
478                    Err(stale_lease) => {
479                        drop(stale_lease);
480                        log::info!(
481                            "Dropped superseded in-flight wake lease for job koid {:?}",
482                            job_koid
483                        );
484                    }
485                }
486            }
487
488            if let Some(prev) = prev_in_flight {
489                let _ = prev.await;
490            }
491            wake_lease_tracker.clear_in_flight_if_current(target_generation);
492        });
493
494        Ok(Some(in_flight))
495    }
496}
497
498impl Drop for Kernels {
499    fn drop(&mut self) {
500        self.background_tasks.shutdown();
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use futures::StreamExt;
508    use std::pin::pin;
509    use std::task::Poll;
510
511    #[fuchsia::test]
512    async fn acquire_wake_lease_reuses_cached_proxy_and_handles_disconnect() {
513        let child_job = Arc::new(fuchsia_runtime::job_default().create_child_job().unwrap());
514        let (proxy, mut stream) =
515            fidl::endpoints::create_proxy_and_stream::<fpower::ActivityGovernorMarker>();
516        let kernels = Kernels::new_for_test(child_job.clone(), "test-kernel", proxy).unwrap();
517
518        let served_requests = Arc::new(std::sync::atomic::AtomicUsize::new(0));
519        let served_requests_clone = served_requests.clone();
520        let server_task = fuchsia_async::Task::local(async move {
521            for _ in 0..2 {
522                let Some(Ok(fpower::ActivityGovernorRequest::TakeApplicationActivityLease {
523                    name,
524                    responder,
525                })) = stream.next().await
526                else {
527                    panic!("expected TakeApplicationActivityLease request");
528                };
529                assert_eq!(name, "starnix-kernel-test-kernel");
530                served_requests_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
531                let (lease_client, _lease_server) = zx::EventPair::create();
532                responder.send(lease_client).unwrap();
533            }
534            // Drop `stream` after two leases to simulate channel disconnection.
535        });
536
537        kernels.acquire_wake_lease(&child_job).await.unwrap();
538        assert!(kernels.has_wake_lease(&child_job));
539        kernels.drop_wake_lease(&child_job).await.unwrap();
540        assert!(!kernels.has_wake_lease(&child_job));
541
542        // Second wake reuses the same cached `ActivityGovernorProxy` without reconnecting.
543        kernels.acquire_wake_lease(&child_job).await.unwrap();
544        assert!(kernels.has_wake_lease(&child_job));
545        assert_eq!(served_requests.load(std::sync::atomic::Ordering::Relaxed), 2);
546        kernels.drop_wake_lease(&child_job).await.unwrap();
547
548        server_task.await;
549
550        // After the cached channel closes, `acquire_wake_lease` evicts the closed proxy and falls
551        // back cleanly without panicking.
552        kernels.acquire_wake_lease(&child_job).await.unwrap();
553        assert!(!kernels.has_wake_lease(&child_job));
554    }
555
556    #[fuchsia::test]
557    async fn start_acquire_wake_lease_writes_request_immediately_and_stores_on_reply() {
558        let child_job = Arc::new(fuchsia_runtime::job_default().create_child_job().unwrap());
559        let (proxy, mut stream) =
560            fidl::endpoints::create_proxy_and_stream::<fpower::ActivityGovernorMarker>();
561        let kernels = Kernels::new_for_test(child_job.clone(), "test-kernel", proxy).unwrap();
562
563        // `start_acquire_wake_lease` returns synchronously after writing the FIDL request to the
564        // channel, before SAG replies.
565        kernels.start_acquire_wake_lease(&child_job).unwrap();
566        assert!(!kernels.has_wake_lease(&child_job));
567
568        let responder = {
569            let mut next_request = pin!(stream.next());
570            let Poll::Ready(Some(Ok(
571                fpower::ActivityGovernorRequest::TakeApplicationActivityLease { name, responder },
572            ))) = futures::poll!(&mut next_request)
573            else {
574                panic!("expected TakeApplicationActivityLease already queued in channel");
575            };
576            assert_eq!(name, "starnix-kernel-test-kernel");
577            responder
578        };
579        assert!(!kernels.has_wake_lease(&child_job));
580
581        // Once SAG replies, the background scope task stores the lease and keeps it alive until
582        // `drop_wake_lease`.
583        let (lease_client, lease_server) = zx::EventPair::create();
584        responder.send(lease_client).unwrap();
585        kernels.wait_for_in_flight_wake_lease(&child_job).await;
586        assert!(kernels.has_wake_lease(&child_job));
587        assert_eq!(
588            lease_server
589                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::ZERO)
590                .to_result(),
591            Err(zx::Status::TIMED_OUT)
592        );
593
594        kernels.drop_wake_lease(&child_job).await.unwrap();
595        assert!(!kernels.has_wake_lease(&child_job));
596        assert!(
597            lease_server
598                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::ZERO)
599                .to_result()
600                .is_ok()
601        );
602    }
603
604    #[fuchsia::test]
605    async fn drop_wake_lease_waits_for_in_flight_acquire_and_drops_lease() {
606        let child_job = Arc::new(fuchsia_runtime::job_default().create_child_job().unwrap());
607        let (proxy, mut stream) =
608            fidl::endpoints::create_proxy_and_stream::<fpower::ActivityGovernorMarker>();
609        let kernels = Kernels::new_for_test(child_job.clone(), "test-kernel", proxy).unwrap();
610
611        kernels.start_acquire_wake_lease(&child_job).unwrap();
612        let Some(Ok(fpower::ActivityGovernorRequest::TakeApplicationActivityLease {
613            name,
614            responder,
615        })) = stream.next().await
616        else {
617            panic!("expected TakeApplicationActivityLease request");
618        };
619        assert_eq!(name, "starnix-kernel-test-kernel");
620
621        // Rapid re-suspend calls `drop_wake_lease` while `TakeApplicationActivityLease` is still in
622        // flight on SAG. `drop_wake_lease` must remain pending until SAG replies and the stale
623        // lease is dropped.
624        let mut drop_fut = pin!(kernels.drop_wake_lease(&child_job));
625        assert!(matches!(futures::poll!(&mut drop_fut), Poll::Pending));
626
627        let (lease_client, lease_server) = zx::EventPair::create();
628        responder.send(lease_client).unwrap();
629        drop_fut.await.unwrap();
630
631        assert!(!kernels.has_wake_lease(&child_job));
632        assert!(
633            lease_server
634                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::ZERO)
635                .to_result()
636                .is_ok()
637        );
638    }
639
640    #[fuchsia::test]
641    async fn newer_acquire_supersedes_out_of_order_earlier_in_flight_lease() {
642        let child_job = Arc::new(fuchsia_runtime::job_default().create_child_job().unwrap());
643        let (proxy, mut stream) =
644            fidl::endpoints::create_proxy_and_stream::<fpower::ActivityGovernorMarker>();
645        let kernels = Kernels::new_for_test(child_job.clone(), "test-kernel", proxy).unwrap();
646
647        kernels.start_acquire_wake_lease(&child_job).unwrap();
648        kernels.start_acquire_wake_lease(&child_job).unwrap();
649
650        let Some(Ok(fpower::ActivityGovernorRequest::TakeApplicationActivityLease {
651            responder: responder1,
652            ..
653        })) = stream.next().await
654        else {
655            panic!("expected first TakeApplicationActivityLease request");
656        };
657        let Some(Ok(fpower::ActivityGovernorRequest::TakeApplicationActivityLease {
658            responder: responder2,
659            ..
660        })) = stream.next().await
661        else {
662            panic!("expected second TakeApplicationActivityLease request");
663        };
664
665        let (lease2_client, lease2_server) = zx::EventPair::create();
666        responder2.send(lease2_client).unwrap();
667        let (lease1_client, lease1_server) = zx::EventPair::create();
668        responder1.send(lease1_client).unwrap();
669
670        kernels.wait_for_in_flight_wake_lease(&child_job).await;
671        assert!(kernels.has_wake_lease(&child_job));
672        // Earlier lease must be closed immediately; latest lease must remain active.
673        assert!(
674            lease1_server
675                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::ZERO)
676                .to_result()
677                .is_ok()
678        );
679        assert_eq!(
680            lease2_server
681                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::ZERO)
682                .to_result(),
683            Err(zx::Status::TIMED_OUT)
684        );
685    }
686
687    #[fuchsia::test]
688    async fn cancelled_drop_wake_lease_preserves_in_flight_tracking_for_retry() {
689        let child_job = Arc::new(fuchsia_runtime::job_default().create_child_job().unwrap());
690        let (proxy, mut stream) =
691            fidl::endpoints::create_proxy_and_stream::<fpower::ActivityGovernorMarker>();
692        let kernels = Kernels::new_for_test(child_job.clone(), "test-kernel", proxy).unwrap();
693
694        kernels.start_acquire_wake_lease(&child_job).unwrap();
695        let Some(Ok(fpower::ActivityGovernorRequest::TakeApplicationActivityLease {
696            responder,
697            ..
698        })) = stream.next().await
699        else {
700            panic!("expected TakeApplicationActivityLease request");
701        };
702
703        // Poll `drop_wake_lease` once (pending on SAG's reply) and drop/cancel the future.
704        {
705            let mut cancelled_drop = pin!(kernels.drop_wake_lease(&child_job));
706            assert!(matches!(futures::poll!(&mut cancelled_drop), Poll::Pending));
707        }
708
709        // A subsequent `drop_wake_lease` must still observe the in-flight acquisition and wait
710        // until SAG replies and the superseded lease is dropped.
711        let mut retry_drop = pin!(kernels.drop_wake_lease(&child_job));
712        assert!(matches!(futures::poll!(&mut retry_drop), Poll::Pending));
713
714        let (lease_client, lease_server) = zx::EventPair::create();
715        responder.send(lease_client).unwrap();
716        retry_drop.await.unwrap();
717
718        assert!(!kernels.has_wake_lease(&child_job));
719        assert!(
720            lease_server
721                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::ZERO)
722                .to_result()
723                .is_ok()
724        );
725        // Once the background task completes, `in_flight` must be cleared despite `drop_wake_lease`
726        // having bumped `state.generation`.
727        let job_koid = child_job.koid().unwrap();
728        assert!(
729            kernels
730                .kernels
731                .lock()
732                .get(&job_koid)
733                .unwrap()
734                .wake_lease
735                .state
736                .lock()
737                .in_flight
738                .is_none()
739        );
740    }
741}