1use super::server::*;
3
4use crate::api;
5use crate::id;
6use crate::misc;
7use crate::misc::jwt;
8use crate::misc::stream_ext::StreamExt as _;
9use crate::servers::yivi;
10
11use super::server::YiviCtx;
12
13use std::collections::{HashMap, VecDeque};
14use std::rc::Rc;
15
16use actix_web::web;
17use futures::future::FutureExt as _;
18use futures::stream::StreamExt as _;
19
20impl App {
21 pub fn chained_sessions_ctl_or_bad_request(&self) -> api::Result<&ChainedSessionsCtl> {
22 self.chained_sessions_ctl.as_ref().ok_or_else(|| {
23 log::debug!("chained sessions control requested, but not available");
24 api::ErrorCode::BadRequest
25 })
26 }
27
28 pub async fn handle_yivi_wait_for_result(
30 app: Rc<Self>,
31 req: web::Json<api::auths::YiviWaitForResultReq>,
32 ) -> api::Result<api::auths::YiviWaitForResultResp> {
33 let csc = app.chained_sessions_ctl_or_bad_request()?;
34
35 let api::auths::YiviWaitForResultReq { state } = req.into_inner();
36
37 let Some(state) = AuthState::unseal(&state, &app.auth_state_secret) else {
38 return Ok(api::auths::YiviWaitForResultResp::PleaseRestartAuth);
39 };
40
41 let Some(ChainedSessionSetup { id: session_id, .. }) = state.yivi_chained_session else {
42 log::debug!(
43 "yivi-wait-for-result endpoint called on a authentication session without a yivi chained session"
44 );
45 return Err(api::ErrorCode::BadRequest);
46 };
47
48 csc.wait_for_result(session_id).await
49 }
50
51 pub async fn handle_yivi_next_session(
53 app: web::Data<std::rc::Rc<App>>,
54 query: web::Query<api::auths::YiviNextSessionQuery>,
55 result_jwt: String,
56 ) -> impl actix_web::Responder {
57 use actix_web::Either::{Left, Right};
58
59 let app = app.into_inner();
60 let api::auths::YiviNextSessionQuery { state } = query.into_inner();
61
62 log::trace!(
63 "yivi server (or imposter) submits next sessions request; jwt: {result_jwt:?}; auth state: {}",
64 state
65 );
66
67 let result_jwt = jwt::JWT::from(result_jwt);
68
69 let Some(state) = AuthState::unseal(&state, &app.auth_state_secret) else {
70 log::debug!(
71 "yivi server (or an imposter) submitted invalid (or expired) auth state to next-session endpoint"
72 );
73 return Left(actix_web::HttpResponse::BadRequest().finish());
74 };
75
76 let Some(ChainedSessionSetup {
77 id: chained_session_id,
78 drip,
79 }) = state.yivi_chained_session
80 else {
81 log::warn!(
82 "yivi server submitted auth state to next-session endpoint without chained session id"
83 );
84 return Left(actix_web::HttpResponse::BadRequest().finish());
85 };
86
87 let Some(csc) = app.chained_sessions_ctl.clone() else {
89 log::warn!("next-session endpoint invoked, but chained sessions are not supported");
90 return Left(actix_web::HttpResponse::BadRequest().finish());
91 };
92
93 let Some(yivi) = app.yivi.as_ref() else {
94 log::warn!("next-session endpoint invoked, but yivi is not supported");
95 return Left(actix_web::HttpResponse::BadRequest().finish());
96 };
97
98 let Ok(..) = yivi::SessionResult::open_signed(&result_jwt, &yivi.server_creds) else {
99 log::debug!(
100 "invalid yivi signed session result submitted by yivi server (or imposter)",
101 );
102 log::trace!("invalid signed session result jwt: {result_jwt}");
103 return Left(actix_web::HttpResponse::BadRequest().finish());
104 };
105
106 log::trace!(
107 "yivi server submitted disclosure and is waiting for chained session {chained_session_id}"
108 );
109
110 let request_id = id::Id::random();
111
112 let wfns_fut =
113 csc.clone()
114 .wait_for_next_session(chained_session_id, request_id, result_jwt);
115
116 if drip {
117 let on_drop = move || {
120 tokio::task::spawn_local(async move {
121 let _ = csc
122 .send_command(CscCommand::AbortWaitForNextSession {
123 chained_session_id,
124 request_id,
125 })
126 .await;
127 });
128 };
129
130 let wfns_fut = async move {
131 let _deferred = misc::defer(on_drop);
132 wfns_fut.await
133 };
134
135 Right(Left(Self::dripping_wfns_responder(wfns_fut)))
136 } else {
137 Right(Right(Self::regular_wfns_responder(wfns_fut).await))
138 }
139 }
140
141 async fn regular_wfns_responder(
142 wfns_fut: impl Future<Output = api::Result<NextSession>>,
143 ) -> impl actix_web::Responder {
144 match wfns_fut.await {
145 Ok(None) => actix_web::HttpResponse::NoContent().finish(),
146 Ok(Some(session_request)) => {
147 log::debug!("sent chained session to yivi server");
148 actix_web::HttpResponse::Ok().json(session_request)
149 }
150 Err(api::ErrorCode::InternalError) => {
151 actix_web::HttpResponse::InternalServerError().finish()
152 }
153 Err(api::ErrorCode::BadRequest) => actix_web::HttpResponse::BadRequest().finish(),
154 Err(api::ErrorCode::PleaseRetry) => panic!("not expecting 'please retry' here"),
155 }
156 }
157
158 fn dripping_wfns_responder(
159 wfns_fut: impl Future<Output = api::Result<NextSession>> + 'static,
160 ) -> impl actix_web::Responder {
161 let drips = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
162 core::time::Duration::from_millis(100),
163 ))
164 .map(|_| Ok(bytes::Bytes::from_static(b" ")));
165
166 let result_bytes_fut = wfns_fut.map(|result| match result {
167 Ok(Some(session_request)) => {
168 log::debug!("sent chained session to yivi server: {session_request:?}");
169 Ok(serde_json::to_vec_pretty(&session_request)
170 .map_err(|err| {
171 log::error!("failed to serialize session_request to json: {err:#}");
172 api::ErrorCode::InternalError
173 })?
174 .into())
175 }
176 Ok(None) => {
177 log::error!("bug: `None` session_request reached dripping chained session");
178 Err(api::ErrorCode::InternalError)
179 }
180 Err(err) => {
181 log::warn!("failed to release session_request to yivi server: {err:#}");
182 Err(err)
183 }
184 });
185
186 let stream = drips.until_overridden_by(result_bytes_fut.into_stream());
187
188 actix_web::HttpResponse::Ok()
189 .content_type(mime::APPLICATION_JSON)
191 .streaming(stream)
192 }
193
194 pub async fn handle_yivi_release_next_session(
196 app: Rc<Self>,
197 req: web::Json<api::auths::YiviReleaseNextSessionReq>,
198 ) -> api::Result<api::auths::YiviReleaseNextSessionResp> {
199 let csc = app.chained_sessions_ctl_or_bad_request()?;
200 let yivi = app.get_yivi()?;
201
202 let api::auths::YiviReleaseNextSessionReq {
203 state,
204 next_session,
205 stale_after,
206 } = req.into_inner();
207
208 let Some(state) = AuthState::unseal(&state, &app.auth_state_secret) else {
209 return Ok(api::auths::YiviReleaseNextSessionResp::PleaseRestartAuth);
210 };
211
212 let Some(ChainedSessionSetup {
213 id: session_id,
214 drip,
215 }) = state.yivi_chained_session
216 else {
217 log::debug!(
218 "yivi-release-next-session endpoint called on a authentication session without a yivi chained session"
219 );
220 return Err(api::ErrorCode::BadRequest);
221 };
222
223 if next_session.is_none() && drip {
224 log::debug!(
225 "yivi-release-next-session endpoint on a dripping chained session, but with empty next session."
226 );
227 return Err(api::ErrorCode::BadRequest);
228 }
229
230 let esr = if let Some(jwt) = next_session {
231 Some(
232 yivi::ExtendedSessionRequest::open_signed(
233 &jwt,
234 &yivi.requestor_creds.to_verifying_credentials(),
235 )
236 .map_err(|err| {
237 log::debug!("failed to open signed extended session request: {}", err);
238 api::ErrorCode::BadRequest
239 })?,
240 )
241 } else {
242 None
243 };
244
245 csc.release_next_session(session_id, esr, stale_after).await
246 }
247}
248
249#[derive(Clone)]
253pub struct ChainedSessionsCtl {
254 sender: tokio::sync::mpsc::Sender<CscCommand>,
255}
256
257#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
258pub struct ChainedSessionsConfig {
259 #[serde(with = "crate::misc::time_ext::human_duration")]
260 #[serde(default = "default_chained_session_validity")]
261 pub session_validity: core::time::Duration,
262
263 #[serde(default = "default_max_chained_sessions")]
274 pub max_sessions: usize,
275}
276
277fn default_chained_session_validity() -> core::time::Duration {
278 core::time::Duration::from_secs(10 * 60) }
280
281fn default_max_chained_sessions() -> usize {
282 1000
283}
284
285impl Default for ChainedSessionsConfig {
286 fn default() -> Self {
287 crate::misc::serde_ext::default_object()
288 }
289}
290
291type NextSession = Option<yivi::ExtendedSessionRequest>;
292
293type CreateSessionCrs = tokio::sync::oneshot::Sender<api::Result<Option<id::Id>>>;
294type WaitForResultCrs =
295 tokio::sync::oneshot::Sender<api::Result<api::auths::YiviWaitForResultResp>>;
296type WaitForNextSessionCrs = tokio::sync::oneshot::Sender<api::Result<NextSession>>;
297type ReleaseNextSessionCrs =
298 tokio::sync::oneshot::Sender<api::Result<api::auths::YiviReleaseNextSessionResp>>;
299
300enum CscCommand {
301 CreateSession {
302 resp_sender: CreateSessionCrs,
303 },
304 WaitForResult {
305 chained_session_id: id::Id,
306 resp_sender: WaitForResultCrs,
307 },
308 WaitForNextSession {
309 chained_session_id: id::Id,
310
311 request_id: id::Id,
313 disclosure: jwt::JWT,
314 resp_sender: WaitForNextSessionCrs,
315 },
316 AbortWaitForNextSession {
317 chained_session_id: id::Id,
318 request_id: id::Id,
319 },
320 ReleaseNextSession {
321 chained_session_id: id::Id,
322 next_session_request: NextSession,
323 stale_after: Option<u16>,
324 resp_sender: ReleaseNextSessionCrs,
325 },
326}
327
328impl ChainedSessionsCtl {
329 pub async fn create_session(&self) -> api::Result<Option<id::Id>> {
334 let (resp_sender, resp_receiver) = tokio::sync::oneshot::channel();
335
336 self.send_command(CscCommand::CreateSession { resp_sender })
337 .await?;
338
339 let Ok(resp) = resp_receiver.await else {
340 log::warn!("chained session control create-session response channel closed early");
341 return Err(api::ErrorCode::InternalError);
342 };
343
344 resp
345 }
346
347 pub async fn wait_for_result(
349 &self,
350 chained_session_id: id::Id,
351 ) -> api::Result<api::auths::YiviWaitForResultResp> {
352 let (resp_sender, resp_receiver) = tokio::sync::oneshot::channel();
353
354 self.send_command(CscCommand::WaitForResult {
355 chained_session_id,
356 resp_sender,
357 })
358 .await?;
359
360 let Ok(resp) = resp_receiver.await else {
361 log::warn!("chained session control wait-for-result response channel closed early");
362 return Err(api::ErrorCode::InternalError);
363 };
364
365 resp
366 }
367
368 pub async fn wait_for_next_session(
373 self,
374 chained_session_id: id::Id,
375 request_id: id::Id,
376 disclosure: jwt::JWT,
377 ) -> api::Result<NextSession> {
378 let (resp_sender, resp_receiver) = tokio::sync::oneshot::channel();
379
380 self.send_command(CscCommand::WaitForNextSession {
381 chained_session_id,
382 request_id,
383 disclosure,
384 resp_sender,
385 })
386 .await?;
387
388 let Ok(resp) = resp_receiver.await else {
389 log::warn!(
390 "chained session control wait-for-next-session response channel closed early"
391 );
392 return Err(api::ErrorCode::InternalError);
393 };
394
395 resp
396 }
397
398 pub async fn release_next_session(
400 &self,
401 chained_session_id: id::Id,
402 next_session_request: NextSession,
403 stale_after: Option<u16>,
404 ) -> api::Result<api::auths::YiviReleaseNextSessionResp> {
405 let (resp_sender, resp_receiver) = tokio::sync::oneshot::channel();
406
407 self.send_command(CscCommand::ReleaseNextSession {
408 chained_session_id,
409 next_session_request,
410 stale_after,
411 resp_sender,
412 })
413 .await?;
414
415 let Ok(resp) = resp_receiver.await else {
416 log::warn!(
417 "chained session control release-next-session response channel closed early"
418 );
419 return Err(api::ErrorCode::InternalError);
420 };
421
422 resp
423 }
424
425 async fn send_command(&self, cmd: CscCommand) -> api::Result<()> {
426 self.sender.send(cmd).await.map_err(|_| {
427 log::warn!("chained session control command channel closed early");
428 api::ErrorCode::InternalError
429 })
430 }
431
432 pub fn new(ctx: YiviCtx) -> Self {
434 let (sender, receiver) = tokio::sync::mpsc::channel(10);
435
436 tokio::spawn(async {
437 log::trace!("spawned chained sessions control task");
438
439 ChainedSessionsCtl::drive(ctx, receiver).await;
440
441 log::trace!("chained sessions control task is about to complete");
442 });
443
444 Self { sender }
445 }
446
447 async fn drive(ctx: YiviCtx, mut receiver: tokio::sync::mpsc::Receiver<CscCommand>) {
448 let mut backend = ChainedSessionsBackend::new(ctx);
449
450 loop {
451 tokio::select! {
452 cmd_maybe = receiver.recv() => {
453 let Some(cmd) = cmd_maybe else {
454 return
456 };
457
458 backend.handle_cmd(cmd).await;
459 }
460 _ = backend.sleep_until_next_expiry() => {
461 backend.expire_next();
462 }
463 };
464 }
465 }
466}
467
468enum ChainedSessionState {
469 WaitingForYiviServer {
470 waiters: Vec<WaitForResultCrs>,
471 },
472 YiviServerWaiting {
473 disclosure: jwt::JWT,
474
475 waiters: HashMap<id::Id, WaitForNextSessionCrs>,
477 first_arrived_at: std::time::Instant,
478 },
479}
480
481struct ChainedSessionsBackend {
483 ctx: YiviCtx,
484 sessions: HashMap<id::Id, ChainedSessionState>,
485
486 expiry_queue: VecDeque<(tokio::time::Instant, id::Id)>,
490}
491
492impl ChainedSessionsBackend {
493 fn new(ctx: YiviCtx) -> Self {
494 Self {
495 ctx,
496 sessions: Default::default(),
497 expiry_queue: Default::default(),
498 }
499 }
500
501 async fn handle_cmd(&mut self, cmd: CscCommand) {
502 match cmd {
503 CscCommand::WaitForResult {
504 chained_session_id,
505 resp_sender,
506 } => {
507 self.handle_wait_for_result(chained_session_id, resp_sender)
508 .await
509 }
510 CscCommand::WaitForNextSession {
511 chained_session_id,
512 request_id,
513 disclosure,
514 resp_sender,
515 } => {
516 self.handle_wait_for_next_session(
517 chained_session_id,
518 request_id,
519 disclosure,
520 resp_sender,
521 )
522 .await
523 }
524 CscCommand::AbortWaitForNextSession {
525 chained_session_id,
526 request_id,
527 } => {
528 self.handle_abort_wait_for_next_session(chained_session_id, request_id)
529 .await
530 }
531 CscCommand::CreateSession { resp_sender } => {
532 self.handle_create_session(resp_sender).await
533 }
534 CscCommand::ReleaseNextSession {
535 chained_session_id,
536 next_session_request,
537 stale_after,
538 resp_sender,
539 } => {
540 self.handle_release_next_session(
541 chained_session_id,
542 next_session_request,
543 stale_after,
544 resp_sender,
545 )
546 .await
547 }
548 }
549 }
550
551 fn respond_to<T>(
552 resp_sender: tokio::sync::oneshot::Sender<T>,
553 resp: T,
554 chained_session_id: id::Id,
555 ) {
556 if resp_sender.send(resp).is_err() {
557 log::warn!(
558 "response channel for chained session {chained_session_id} was closed before response could be sent"
559 );
560 }
561 }
562
563 async fn handle_create_session(&mut self, resp_sender: CreateSessionCrs) {
564 let max_sessions = self.ctx.chained_sessions_config.max_sessions;
567 if self.sessions.len() >= max_sessions {
568 log::warn!(
569 "refusing new chained session: at the configured maximum of {max_sessions} concurrent chained sessions"
570 );
571 if resp_sender.send(Ok(None)).is_err() {
572 log::warn!(
573 "create-session response channel closed before capacity refusal could be sent"
574 );
575 }
576 return;
577 }
578
579 let chained_session_id = id::Id::random();
580
581 assert!(
582 self.sessions
583 .insert(
584 chained_session_id,
585 ChainedSessionState::WaitingForYiviServer { waiters: vec![] }
586 )
587 .is_none(),
588 "against all odds, 256-bit random ids collided!"
589 );
590
591 let expiry =
592 tokio::time::Instant::now() + self.ctx.chained_sessions_config.session_validity;
593 self.expiry_queue.push_back((expiry, chained_session_id));
594
595 log::trace!("chained session {chained_session_id} created");
596
597 Self::respond_to(
598 resp_sender,
599 Ok(Some(chained_session_id)),
600 chained_session_id,
601 );
602 }
603
604 async fn handle_wait_for_result(
605 &mut self,
606 chained_session_id: id::Id,
607 resp_sender: WaitForResultCrs,
608 ) {
609 let Some(session) = self.sessions.get_mut(&chained_session_id) else {
610 Self::respond_to(
611 resp_sender,
612 Ok(api::auths::YiviWaitForResultResp::SessionGone),
613 chained_session_id,
614 );
615 return;
616 };
617
618 match session {
619 ChainedSessionState::WaitingForYiviServer { waiters } => {
620 log::trace!(
621 "registered waiter for the result of chained session {chained_session_id}",
622 );
623 waiters.push(resp_sender)
624 }
625 ChainedSessionState::YiviServerWaiting { disclosure, .. } => {
626 log::trace!(
627 "result for chained session {chained_session_id} requested and immediately available"
628 );
629 Self::respond_to(
630 resp_sender,
631 Ok(api::auths::YiviWaitForResultResp::Success {
632 disclosure: disclosure.clone(),
633 }),
634 chained_session_id,
635 )
636 }
637 }
638 }
639
640 async fn handle_wait_for_next_session(
641 &mut self,
642 chained_session_id: id::Id,
643 request_id: id::Id,
644 disclosure: jwt::JWT,
645 resp_sender: WaitForNextSessionCrs,
646 ) {
647 let Some(session) = self.sessions.get_mut(&chained_session_id) else {
648 log::warn!(
649 "yivi server submitted disclosure for a chained session {chained_session_id} that cannot be found - was the yivi server too slow?"
650 );
651 Self::respond_to(resp_sender, Ok(None), chained_session_id);
652 return;
653 };
654
655 match session {
657 ChainedSessionState::WaitingForYiviServer { .. } => {
658 }
660 ChainedSessionState::YiviServerWaiting {
661 disclosure: stored_disclosure,
662 waiters,
663 ..
664 } => {
665 if &disclosure != stored_disclosure {
666 log::warn!(
667 "second yivi server submitted a different disclosure for chained session {chained_session_id}"
668 );
669 Self::respond_to(
670 resp_sender,
671 Err(api::ErrorCode::BadRequest),
672 chained_session_id,
673 );
674 return;
675 }
676 log::trace!(
677 "an additional yivi server ({request_id}) is waiting for chained session {chained_session_id}"
678 );
679 let should_be_none = waiters.insert(request_id, resp_sender);
680 if should_be_none.is_some() {
681 log::error!("bug: 256-bit random `request_id` collided - was it random?");
682 panic!("bug: random `request_id`s collided");
683 }
684 return;
685 }
686 }
687 let old_session = std::mem::replace(
688 session,
689 ChainedSessionState::YiviServerWaiting {
690 disclosure: disclosure.clone(),
691 waiters: [(request_id, resp_sender)].into(),
692 first_arrived_at: std::time::Instant::now(),
693 },
694 );
695
696 match old_session {
697 ChainedSessionState::WaitingForYiviServer { waiters } => {
698 log::trace!(
700 "releasing {} waiter(s) on the result of chained session {chained_session_id}",
701 waiters.len()
702 );
703 for waiter in waiters.into_iter() {
704 Self::respond_to(
705 waiter,
706 Ok(api::auths::YiviWaitForResultResp::Success {
707 disclosure: disclosure.clone(),
708 }),
709 chained_session_id,
710 )
711 }
712 }
713 ChainedSessionState::YiviServerWaiting { .. } => {
714 panic!("session changed unexpectedly");
715 }
716 }
717 }
718
719 async fn handle_abort_wait_for_next_session(
720 &mut self,
721 chained_session_id: id::Id,
722 request_id: id::Id,
723 ) {
724 let Some(session) = self.sessions.get_mut(&chained_session_id) else {
725 log::trace!(
726 "wanting to abort yivi server request {request_id} of chained session \
727 {chained_session_id}, but this session is not there (anymore)"
728 );
729 return;
730 };
731
732 match session {
733 ChainedSessionState::WaitingForYiviServer { .. } => {
734 log::warn!(
735 "to be aborted yivi server request {request_id} of chained session \
736 {chained_session_id} is not (yet) recorded in chained session state \
737 (nor any other yivi server request)"
738 );
739 }
740 ChainedSessionState::YiviServerWaiting { waiters, .. } => {
741 let removed = waiters.remove(&request_id);
742
743 if removed.is_none() {
744 log::warn!(
745 "to be aborted yivi server request {request_id} of chained session \
746 {chained_session_id} is not (yet) recorded in chained session state"
747 );
748 }
749
750 }
753 }
754 }
755
756 async fn handle_release_next_session(
757 &mut self,
758 chained_session_id: id::Id,
759 next_session_request: NextSession,
760 stale_after: Option<u16>,
761 resp_sender: ReleaseNextSessionCrs,
762 ) {
763 let Some(session) = self.sessions.get_mut(&chained_session_id) else {
764 log::debug!(
765 "request to release chained session {chained_session_id} that cannot be found "
766 );
767 Self::respond_to(
768 resp_sender,
769 Ok(api::auths::YiviReleaseNextSessionResp::SessionGone),
770 chained_session_id,
771 );
772 return;
773 };
774
775 match session {
777 ChainedSessionState::WaitingForYiviServer { .. } => {
778 log::debug!(
779 "request to release a yivi server that's not there yet, \
780 in chained session {chained_session_id}"
781 );
782 Self::respond_to(
783 resp_sender,
784 Ok(api::auths::YiviReleaseNextSessionResp::TooEarly),
785 chained_session_id,
786 );
787 return;
788 }
789 ChainedSessionState::YiviServerWaiting { .. } => {
790 }
792 }
793
794 log::trace!(
795 "yivi server is about to be released from chained session {chained_session_id}"
796 );
797 let Some(ChainedSessionState::YiviServerWaiting {
798 waiters,
799 first_arrived_at,
800 ..
801 }) = self.sessions.remove(&chained_session_id)
802 else {
803 panic!("chained session state changed unexpectedly");
804 };
805
806 if waiters.is_empty() {
807 Self::respond_to(
808 resp_sender,
809 Ok(api::auths::YiviReleaseNextSessionResp::YiviServerGone),
810 chained_session_id,
811 );
812 return;
813 }
814
815 if let Some(stale_after) = stale_after
816 && std::time::Instant::now()
817 .duration_since(first_arrived_at)
818 .as_millis()
819 > stale_after.into()
820 {
821 Self::respond_to(
822 resp_sender,
823 Ok(api::auths::YiviReleaseNextSessionResp::YiviServerGone),
824 chained_session_id,
825 );
826 return;
827 }
828
829 for waiter in waiters.into_values() {
830 Self::respond_to(waiter, Ok(next_session_request.clone()), chained_session_id);
831 }
832
833 Self::respond_to(
834 resp_sender,
835 Ok(api::auths::YiviReleaseNextSessionResp::Success {}),
836 chained_session_id,
837 );
838 }
839
840 async fn sleep_until_next_expiry(&self) {
841 match self.expiry_queue.front() {
842 Some((expiry, _)) => tokio::time::sleep_until(*expiry).await,
843 None => std::future::pending().await,
844 }
845 }
846
847 fn expire_next(&mut self) {
848 let now = tokio::time::Instant::now();
849
850 while let Some((_, id)) = self.expiry_queue.pop_front_if(|(expiry, _)| *expiry <= now) {
851 let Some(session) = self.sessions.remove(&id) else {
852 continue; };
854
855 log::debug!("chained session {id} expired");
856 match session {
857 ChainedSessionState::WaitingForYiviServer { waiters } => {
858 for waiter in waiters {
859 Self::respond_to(
860 waiter,
861 Ok(api::auths::YiviWaitForResultResp::SessionGone),
862 id,
863 );
864 }
865 }
866 ChainedSessionState::YiviServerWaiting { waiters, .. } => {
867 for waiter in waiters.into_values() {
868 Self::respond_to(waiter, Ok(None), id);
869 }
870 }
871 }
872 }
873 }
874}