Skip to main content

pubhubs/servers/
run.rs

1//! Running PubHubs [`Server`]s
2use std::num::NonZero;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use actix_web::web;
7use anyhow::{Context as _, Result, bail};
8use core::convert::Infallible;
9use tokio::sync::mpsc;
10
11use crate::api;
12use crate::misc::defer;
13use crate::servers::{
14    App, AppBase, AppCreator, Command, Constellation, DiscoverVerdict, Name, Server,
15    for_all_servers, server::RunningState,
16};
17
18/// A set of running PubHubs servers.
19pub struct Set {
20    /// Handle to the task waiting on [SetInner::wait].
21    wait_jh: tokio::task::JoinHandle<usize>,
22}
23
24/// Additional options for [`Set::new_opts`].
25///
26/// Used by the integration tests to run the servers on pre-bound, ephemeral ports, so that
27/// multiple [`Set`]s can run simultaneously.  Each listener is `try_clone`d (the file descriptor
28/// is dup'd) every time its server (re)starts — e.g. after discovery — so the originals are kept
29/// alive (by the `Runner`, for as long as the server runs) to keep the port reserved.
30#[derive(Default)]
31pub struct SetOpts {
32    /// If set, PubHubs Central listens on this pre-bound socket instead of binding the address in
33    /// its config.  Its advertised `phc_url` must point at the same port (not checked at runtime).
34    pub phc_listener: Option<std::net::TcpListener>,
35
36    /// Like [`SetOpts::phc_listener`], but for the transcryptor.
37    pub transcryptor_listener: Option<std::net::TcpListener>,
38
39    /// Like [`SetOpts::phc_listener`], but for the authentication server.
40    pub auths_listener: Option<std::net::TcpListener>,
41}
42
43impl SetOpts {
44    /// Takes the pre-bound listener for the given server, if any.
45    fn take_listener(&mut self, name: Name) -> Option<std::net::TcpListener> {
46        match name {
47            Name::PubhubsCentral => self.phc_listener.take(),
48            Name::Transcryptor => self.transcryptor_listener.take(),
49            Name::AuthenticationServer => self.auths_listener.take(),
50        }
51    }
52}
53
54impl Set {
55    /// Creates a new set of PubHubs servers from the given config.
56    /// To signal shutdown of these senders, drop the returned `sender`.
57    pub fn new(
58        config: &crate::servers::Config,
59    ) -> Result<(Self, tokio::sync::oneshot::Sender<Infallible>)> {
60        Self::new_opts(config, SetOpts::default())
61    }
62
63    /// Like [`Set::new`], but takes additional [`SetOpts`].
64    pub fn new_opts(
65        config: &crate::servers::Config,
66        opts: SetOpts,
67    ) -> Result<(Self, tokio::sync::oneshot::Sender<Infallible>)> {
68        let (inner, shutdown_sender) = SetInner::new(config, opts)?;
69
70        let wait_jh = tokio::task::spawn(inner.wait());
71
72        Ok((Self { wait_jh }, shutdown_sender))
73    }
74
75    /// Waits for one of the servers to return, panic, or be cancelled.
76    /// If that happens, the other servers are directed to shutdown as well.
77    ///
78    /// Returns the number of servers that did *not* shutdown cleanly.
79    ///
80    /// Panics when the tokio runtime is shut down.
81    pub async fn wait(self) -> usize {
82        match self.wait_jh.await {
83            Ok(nr) => nr,
84            Err(join_error) => {
85                panic!("task waiting on servers to exit was cancelled or panicked: {join_error}");
86            }
87        }
88    }
89}
90
91/// A set of running PubHubs servers.
92struct SetInner {
93    /// The servers' tasks
94    joinset: tokio::task::JoinSet<Result<()>>,
95
96    /// Via `shutdown_sender` [`Set`] broadcasts the instruction to shutdown to all servers
97    /// running in the `joinset`.  It does so not by `send`ing a message, but by dropping
98    /// the `shutdown_sender`.
99    shutdown_sender: Option<tokio::sync::broadcast::Sender<Infallible>>,
100
101    /// Via `shutdown_receiver`, the [Set] received the instruction to close.
102    shutdown_receiver: tokio::sync::oneshot::Receiver<Infallible>,
103}
104
105impl Drop for SetInner {
106    fn drop(&mut self) {
107        if self.shutdown_sender.is_some() {
108            log::error!(
109                "the completion of all pubhubs servers was not awaited - please consume SetInner using wait() or shutdown()"
110            )
111        }
112    }
113}
114
115impl SetInner {
116    /// Creates a new set of PubHubs servers from the given config.
117    ///
118    /// Returns not only the [`SetInner`] instance, but also a [`tokio::sync::oneshot::Sender<Infallible>`]
119    /// that can be dropped to signal the [`SetInner`] should shutdown.
120    pub fn new(
121        config: &crate::servers::Config,
122        mut opts: SetOpts,
123    ) -> Result<(Self, tokio::sync::oneshot::Sender<Infallible>)> {
124        let rt_handle: tokio::runtime::Handle = tokio::runtime::Handle::current();
125        let mut joinset = tokio::task::JoinSet::<Result<()>>::new();
126
127        let (shutdown_sender, _) = tokio::sync::broadcast::channel(1); // NB capacity of 0 is not allowed
128
129        // count the number of servers
130        let server_count: usize = {
131            let mut counter: usize = 0;
132
133            macro_rules! count_server {
134                ($server:ident) => {
135                    if config.$server.is_some() {
136                        counter += 1;
137                    }
138                };
139            }
140
141            for_all_servers!(count_server);
142
143            counter
144        };
145
146        // don't use one thread per code for each server - this speeds up testing
147        let worker_count: Option<NonZero<usize>> =
148            std::thread::available_parallelism()
149                .ok()
150                .map(|parallelism: NonZero<usize>| {
151                    if server_count == 0 {
152                        return NonZero::<usize>::new(1).unwrap();
153                    }
154
155                    NonZero::<usize>::try_from(parallelism.get() / server_count)
156                        .unwrap_or(NonZero::<usize>::new(1).unwrap()) // more servers than cores
157                });
158
159        macro_rules! run_server {
160            ($server:ident) => {
161                if config.$server.is_some() {
162                    let config = config.clone();
163                    let rt_handle = rt_handle.clone();
164                    let shutdown_receiver = shutdown_sender.subscribe();
165                    let listener =
166                        opts.take_listener(<crate::servers::$server::Server as Server>::NAME);
167
168                    // We use spawn_blocking instead of spawn, because we want a separate thread
169                    // for each server to run on
170                    joinset.spawn_blocking(move || -> Result<()> {
171                        Self::run_server::<crate::servers::$server::Server>(
172                            config,
173                            rt_handle,
174                            shutdown_receiver,
175                            worker_count,
176                            listener,
177                        )
178                    });
179                }
180            };
181        }
182
183        for_all_servers!(run_server);
184
185        // A pre-bound listener supplied for a server absent from `config` is never taken above and
186        // would be dropped silently (releasing its port); catch that misuse in debug builds.
187        debug_assert!(
188            opts.phc_listener.is_none()
189                && opts.transcryptor_listener.is_none()
190                && opts.auths_listener.is_none(),
191            "SetOpts has a pre-bound listener for a server not present in the config"
192        );
193
194        let (external_shutdown_sender, external_shutdown_receiver) =
195            tokio::sync::oneshot::channel();
196
197        Ok((
198            Self {
199                joinset,
200                shutdown_sender: Some(shutdown_sender),
201                shutdown_receiver: external_shutdown_receiver,
202            },
203            external_shutdown_sender,
204        ))
205    }
206
207    // Creates a server from the given `config` and run it ont the given tokio runtime.
208    //
209    // Abort when the `shutdown_receiver` channel is closed.
210    fn run_server<S: Server>(
211        config: crate::servers::Config,
212        rt_handle: tokio::runtime::Handle,
213        shutdown_receiver: tokio::sync::broadcast::Receiver<Infallible>,
214        worker_count: Option<NonZero<usize>>,
215        listener: Option<std::net::TcpListener>,
216    ) -> Result<()> {
217        assert!(config.preparation_state == crate::servers::config::PreparationState::Preliminary);
218
219        let localset = tokio::task::LocalSet::new();
220
221        let fut = localset.run_until(async {
222            let config = config.prepare_for(S::NAME).await?;
223
224            crate::servers::run::Runner::<S>::new(
225                &config,
226                shutdown_receiver,
227                worker_count,
228                listener,
229            )?
230            .run()
231            .await
232        });
233
234        let result = rt_handle.block_on(fut);
235
236        rt_handle.block_on(localset);
237
238        log::debug!("{} stopped with {:?}", S::NAME, result);
239
240        result
241    }
242
243    /// Waits for one of the servers to return, panic, or be cancelled.
244    /// If that happens, the other servers are directed to shutdown as well.
245    ///
246    /// If this function is not called, servers can fail silently.
247    ///
248    /// Returns the number of servers that did *not* shutdown cleanly
249    pub async fn wait(mut self) -> usize {
250        log::trace!("waiting for one of the servers to exit...");
251        let err_count: usize = tokio::select! {
252            // either one of the servers exits
253            result_maybe = self
254                .joinset
255                .join_next() => {
256                    let result = result_maybe.expect("no servers to wait on");
257                    let is_err : bool =  matches!(result, Err(_) | Ok(Err(_)));
258
259                    log::log!( if is_err { log::Level::Error } else { log::Level::Debug },
260                        "one of the servers exited with {result:?};  stopping all servers.."
261                    );
262
263                    if is_err {
264                        1
265                    } else {
266                        0
267                    }
268                },
269
270            // or we get the command to shut down from higher up
271            result = &mut self.shutdown_receiver => {
272                result.expect_err("received Infallible");
273                log::debug!("shutdown requested"); 0
274            }
275        };
276
277        self.shutdown().await + err_count
278    }
279
280    /// Requests shutdown of all servers, and wait for it to complete.
281    /// Returns the number of servers that did *not* shutdown cleanly.
282    pub async fn shutdown(mut self) -> usize {
283        assert!(
284            self.shutdown_sender.is_some(),
285            "only signal_shutdown should take shutdown_sender"
286        );
287
288        // This causes the shutdown_receivers at the different servers to be closed,
289        // which in turn should cause those servers' threads to join.
290        drop(self.shutdown_sender.take());
291
292        let mut err_count = 0usize;
293
294        while let Some(result) = self.joinset.join_next().await {
295            err_count += if matches!(result, Err(_) | Ok(Err(_))) {
296                1
297            } else {
298                0
299            };
300        }
301
302        // join_next returned None, which means the JoinSet is empty
303
304        err_count
305    }
306}
307
308/// Runs a [`Server`]
309struct Runner<ServerT: Server> {
310    pubhubs_server: Rc<ServerT>,
311    shutdown_receiver: tokio::sync::broadcast::Receiver<Infallible>,
312    worker_count: Option<NonZero<usize>>,
313
314    /// If set, the server listens on this pre-bound socket — `try_clone`d on each (re)start —
315    /// instead of binding the address in its [`ServerConfig`](crate::servers::config::ServerConfig).
316    /// Held here so the port stays reserved for the server's whole lifetime.  See [`SetOpts`].
317    listener: Option<std::net::TcpListener>,
318
319    /// Number of restarts (i.e. modifications applied)
320    generation: usize,
321}
322
323/// The handles to control an [actix_web::dev::Server] running a pubhubs [Server].
324struct Handles<S: Server> {
325    /// Handle to the actual actix TCP server.  The [actix_web::dev::Server] is owned
326    /// by the task driving it.
327    actix_server_handle: actix_web::dev::ServerHandle,
328
329    /// Handle to the task driving the actix TCP server
330    actix_join_handle: tokio::task::JoinHandle<Result<(), std::io::Error>>,
331
332    /// Handle to the task running (discovery for) the PubHubs server.
333    ph_join_handle: tokio::task::JoinHandle<Result<Option<crate::servers::server::BoxModifier<S>>>>,
334
335    /// Dropped to order `ph_join_handle` to shutdown.  [None] when used.
336    ph_shutdown_sender: Option<tokio::sync::oneshot::Sender<Infallible>>,
337
338    /// Receives commands from the [App]s
339    command_receiver: mpsc::Receiver<CommandRequest<S>>,
340
341    /// To check whether [Handles::shutdown] was completed before being dropped
342    drop_bomb: crate::misc::drop_ext::Bomb<Box<dyn FnOnce()>>,
343}
344
345impl<S: Server> Handles<S> {
346    /// Drives the actix server until a [Command] is received - which is returned
347    async fn run_until_command(&mut self, runner: &mut Runner<S>) -> anyhow::Result<Command<S>> {
348        tokio::select! {
349
350            // received command from running pubhubs/actix server
351            command_request_maybe = self.command_receiver.recv() => {
352                if let Some(command_request) = command_request_maybe {
353                    return Ok(command_request.accept());
354                }
355
356                log::error!("{}'s command receiver is unexpectedly closed", S::NAME);
357                bail!("{}'s command receiver is unexpectedly closed", S::NAME);
358            },
359
360            // pubhubs server exited, returning a modification request
361            res = &mut self.ph_join_handle => {
362                let modifier : crate::servers::server::BoxModifier<S> =
363                res.with_context(|| format!("{}'s pubhubs task joined unexpectedly", S::NAME))?
364                    .with_context(|| format!("{}'s pubhubs task crashed", S::NAME))?
365                    .with_context(|| format!("{}'s pubhubs task stopped without being asked to", S::NAME))?;
366
367
368                #[expect(clippy::needless_return)] // "return" makes the code more readable here
369                return Ok(Command::Modify(modifier));
370            },
371
372            // the thread running this server wants us to quit
373            Err(err) = runner.shutdown_receiver.recv() => {
374                match err {
375                    tokio::sync::broadcast::error::RecvError::Lagged(_) => {
376                        panic!("got impossible `Lagged` error from shutdown sender");
377                    },
378                    tokio::sync::broadcast::error::RecvError::Closed => {
379                        #[expect(clippy::needless_return)] // "return" is more readable here
380                        return Ok(Command::Exit);
381                    },
382                }
383            },
384
385            // the actix serer exited unexpectedly
386            res = &mut self.actix_join_handle => {
387                res.inspect_err(|err| log::error!("{}'s actix task joined unexpectedly: {}", S::NAME, err) )
388                    .with_context(|| format!("{}'s actix task joined unexpectedly", S::NAME))?
389                    .inspect_err(|err| log::error!("{}'s http server crashed: {}", S::NAME, err) )
390                    .with_context(|| format!("{}'s http server crashed", S::NAME))?;
391
392                log::error!("{}'s actix server stopped unexpectedly", S::NAME);
393                bail!("{}'s actix server stopped unexpectedly", S::NAME);
394            },
395        };
396    }
397
398    /// Consumes this [`Handles`] shutting down the actix server and pubhubs tasks.
399    async fn shutdown(mut self) -> anyhow::Result<()> {
400        log::debug!("Shut down of {} started", S::NAME);
401
402        anyhow::ensure!(
403            self.ph_shutdown_sender.is_some(),
404            "shutdown of ph task already ordered"
405        );
406
407        drop(self.ph_shutdown_sender.take());
408
409        // This is a noop if the actix server is already stopped
410        //
411        // We do not use graceful shutdown, becaus in practise the persistent connections
412        // delay shutdown for the maximal shutdown timeout, which causes more disruption
413        // than the graceful shutdown aims to prevent
414        self.actix_server_handle.stop(false).await;
415
416        let maybe_modifier = self
417            .ph_join_handle
418            .await
419            .with_context(|| {
420                format!(
421                    "{}'s pubhubs task did not join gracefully after being asked to stop",
422                    S::NAME
423                )
424            })?
425            .with_context(|| {
426                format!(
427                    "{}'s pubhubs task crashed after being asked to stop",
428                    S::NAME
429                )
430            })?;
431
432        if let Some(ph_modifier) = maybe_modifier {
433            log::error!(
434                "Woops! {}'s pubhubs task's modifier {} was ignored, because another modifier was first",
435                S::NAME,
436                ph_modifier
437            );
438        }
439
440        self.drop_bomb.defuse();
441
442        log::debug!("Shut down of {} completed", S::NAME);
443
444        Ok(())
445    }
446}
447
448/// Encapsulates the handling of running just one discovery process per server
449struct DiscoveryLimiter {
450    /// Lock that makes sure only one discovery task is running at the same time.
451    ///
452    /// The protected value is true when restart due to a changed constellation is imminent.
453    restart_imminent_lock: Arc<tokio::sync::RwLock<bool>>,
454
455    /// Set when contents of `restart_imminent_lock` lock was observed to be true,
456    /// reducing the load on this lock.
457    restart_imminent_cached: std::cell::OnceCell<()>,
458}
459
460impl Clone for DiscoveryLimiter {
461    fn clone(&self) -> Self {
462        Self {
463            restart_imminent_lock: self.restart_imminent_lock.clone(),
464            // The DiscoveryLimiter is never cloned as part of an `AppBase`.
465            restart_imminent_cached: std::cell::OnceCell::<()>::new(),
466        }
467    }
468}
469
470impl DiscoveryLimiter {
471    fn new() -> Self {
472        DiscoveryLimiter {
473            restart_imminent_lock: Arc::new(tokio::sync::RwLock::new(false)),
474            restart_imminent_cached: std::cell::OnceCell::<()>::new(),
475        }
476    }
477
478    /// This functions contains the discovery logic that's shared between servers.
479    ///
480    /// Discovery can be invoked for two reasons:  
481    ///
482    ///  1. This server has just been restarted and is not aware of the current constellation.
483    ///     Perhaps part of our configuration has changed that makes the current constellation
484    ///     obsolete.
485    ///
486    ///  2. The `.ph/discovery/run` endpoint was triggered.  This should happen when another
487    ///     server detects that our constellation is out-of-date, but since the `.ph/discovery/run`
488    ///     endpoint is unprotected, anyone can invoke it at any time.
489    ///
490    ///     The non-PHC servers check during their discovery whether the constellation PHC advertises
491    ///     is up-to-date with respect to their own configuration.  If it isn't, the non-PHC server
492    ///     triggers PHC to run discovery.
493    ///     
494    ///     PHC checks during its discovery (after it obtained recent details from each of the
495    ///     other servers) whether the constellations of the other servers are up-to-date,
496    ///     and will trigger their discovery routine when these aren't.
497    ///
498    ///
499    /// Thus the procedure for discovery is as follows.
500    ///
501    ///
502    /// Non-PHC:
503    ///   
504    ///  1. Obtain constellation from PHC.  Return if it coincides with the constellation that we already
505    ///     got - if we already got one.
506    ///  2. Check the constellation against our own configuration.  If it's up-to-date, restart
507    ///     this server but with the new constellation.
508    ///  3. Invoke discovery on PHC, and return a retryable error - effectively go back to step 1.
509    ///
510    ///
511    /// PHC:
512    ///
513    ///  1. Obtain discovery info from ourselves - i.e. check whether `phc_url` is configured
514    ///     correctly.
515    ///  2. Retrieve discovery info from the other servers and construct a constellation from it.
516    ///  3. If the constellation has changed, restart to update it.
517    ///  4. Invoke discovery on those servers that have no or outdated constellations,
518    ///     and return a retryable error - effectively go back to step 1.
519    ///
520    ///
521    async fn request_discovery<S: Server>(
522        &self,
523        app: Rc<S::AppT>,
524    ) -> api::Result<api::DiscoveryRunResp> {
525        log::debug!(
526            "{server_name}: discovery is requested",
527            server_name = S::NAME
528        );
529
530        let mut restart_imminent_guard = match self.obtain_lock().await {
531            Some(guard) => guard,
532            None => {
533                log::debug!(
534                    "{server_name}: discovery aborted because the server is already restarting",
535                    server_name = S::NAME
536                );
537                return Ok(api::DiscoveryRunResp::Restarting);
538            }
539        };
540
541        // Obtain discovery info from PHC (even when we are PHC ourselves, for perhaps
542        // the phc_url is misconfigured) and perform some basis checks.
543        // Should not return an error when our constellation is out of sync.
544        let phc_discovery_info = AppBase::<S>::discover_phc(app.clone()).await?;
545
546        if phc_discovery_info.constellation_or_id.is_none() && S::NAME != Name::PubhubsCentral {
547            // PubHubs Central is not yet ready - make the caller retry
548            log::info!(
549                "Discovery of {} is run but {} has no constellation yet",
550                S::NAME,
551                Name::PubhubsCentral,
552            );
553            return Err(api::ErrorCode::PleaseRetry);
554        }
555
556        // What discovery determined should happen; threaded into the `modify` closure below.
557        enum PostDiscovery<Seed> {
558            /// Replace the published constellation and rebuild the running state.
559            Republish(Box<Constellation>, Seed),
560            /// Keep the published constellation; only rebuild the running state from this seed.
561            RebuildRunningState(Seed),
562            /// Exit the binary so an updated one is (hopefully) started in its place.
563            ExitBinary,
564        }
565
566        let post_discovery = match app.discover(phc_discovery_info).await? {
567            DiscoverVerdict::ConstellationOutdated {
568                new_constellation,
569                seed,
570            } => PostDiscovery::Republish(new_constellation, seed),
571            DiscoverVerdict::RunningStateOutdated { seed } => {
572                PostDiscovery::RebuildRunningState(seed)
573            }
574            DiscoverVerdict::BinaryOutdated => PostDiscovery::ExitBinary,
575            DiscoverVerdict::Alright => return Ok(api::DiscoveryRunResp::UpToDate),
576        };
577
578        // modify server, and restart (to modify all Apps)
579
580        let display = match &post_discovery {
581            PostDiscovery::Republish(..) => "updated constellation after discovery",
582            PostDiscovery::RebuildRunningState(_) => "updated running state after discovery",
583            PostDiscovery::ExitBinary => "restarting binary hoping to update version",
584        };
585
586        let result = app
587            .handle
588            .modify(display, move |server: &mut S| -> bool {
589                let (constellation, seed) = match post_discovery {
590                    PostDiscovery::Republish(new_constellation, seed) => (*new_constellation, seed),
591                    PostDiscovery::RebuildRunningState(seed) => {
592                        let Some(running_state) = server.running_state.as_ref() else {
593                            log::error!("{}: running state to rebuild is absent", S::NAME);
594                            return false;
595                        };
596                        (
597                            AsRef::<Constellation>::as_ref(&running_state.constellation).clone(),
598                            seed,
599                        )
600                    }
601                    PostDiscovery::ExitBinary => {
602                        return false; // no, don't restart the server, but exit the binary so that
603                        // - hopefully - a new version of the binary will be started by e.g. systemd
604                    }
605                };
606
607                let extra = match server.create_running_state(&constellation, &seed) {
608                    Ok(extra) => extra,
609                    Err(err) => {
610                        log::error!(
611                            "Error while restarting {} after discovery: {}",
612                            S::NAME,
613                            err
614                        );
615                        return false; // do not restart
616                    }
617                };
618
619                let new_url = constellation.url(S::NAME).clone();
620
621                let new_running_state = match RunningState::new(constellation, extra) {
622                    Ok(running_state) => running_state,
623                    Err(err) => {
624                        log::error!(
625                            "Error while restarting {} after discovery: {}",
626                            S::NAME,
627                            err
628                        );
629                        return false; // do not restart
630                    }
631                };
632
633                let old_running_state = server.running_state.replace(new_running_state);
634
635                // See if our url has changed
636                if old_running_state.is_none_or(|rs| rs.constellation.url(S::NAME) != &new_url) {
637                    log::info!("{}: at {}", S::NAME, new_url);
638                }
639
640                true // yes, restart this server
641            })
642            .await;
643
644        if let Err(()) = result {
645            log::warn!(
646                "failed to initiate restart of {} for discovery, probably because the server is already shutting down",
647                S::NAME,
648            );
649            return Err(api::ErrorCode::PleaseRetry);
650        }
651
652        log::trace!(
653            "{server_name}: registering imminent restart",
654            server_name = S::NAME
655        );
656        *restart_imminent_guard = true;
657        let _ = self.restart_imminent_cached.set(());
658
659        Ok(api::DiscoveryRunResp::Restarting)
660    }
661
662    /// Obtains write lock to `self.restart_imminent_lock` when restart is not imminent.
663    async fn obtain_lock(&self) -> Option<tokio::sync::RwLockWriteGuard<'_, bool>> {
664        if self.restart_imminent_cached.get().is_some() {
665            log::trace!("restart imminent: cached");
666            return None;
667        }
668
669        if *self.restart_imminent_lock.read().await {
670            log::trace!("restart imminent: discovered after obtaining read lock");
671            let _ = self.restart_imminent_cached.set(());
672            return None;
673        }
674
675        let restart_imminent_guard = self.restart_imminent_lock.write().await;
676
677        if *restart_imminent_guard {
678            // while we re-obtained the lock, discovery has completed
679            log::trace!("restart imminent: discovered after obtaining write lock");
680            let _ = self.restart_imminent_cached.set(());
681            return None;
682        }
683
684        Some(restart_imminent_guard)
685    }
686}
687
688/// Handle to a [`Server`] passed to [`App`]s.
689///
690/// Used to issue commands to the server.  Since discovery is requested a often a separate struct
691/// is used to deal with discovery requests.
692pub struct Handle<S: Server> {
693    /// To send commands to the server
694    sender: mpsc::Sender<CommandRequest<S>>,
695
696    /// To coordinate the handling of discovery requests
697    discovery_limiter: DiscoveryLimiter,
698}
699
700struct CommandRequest<S: Server> {
701    /// The actual command
702    command: Command<S>,
703
704    /// A way for the [`Server`] to inform the [`App`] that the command is about to be executed.
705    feedback_sender: tokio::sync::oneshot::Sender<()>,
706}
707
708impl<S: Server> CommandRequest<S> {
709    /// Let's the issuer of the command know that the command is to be fulfilled
710    fn accept(self) -> Command<S> {
711        if self.feedback_sender.send(()).is_err() {
712            log::warn!(
713                "The app issuing command '{}' that is about to execute has already dropped.",
714                &self.command
715            );
716        }
717        self.command
718    }
719}
720
721// We cannot use "derive(Clone)", because Server is not Clone.
722impl<S: Server> Clone for Handle<S> {
723    fn clone(&self) -> Self {
724        Handle {
725            sender: self.sender.clone(),
726            discovery_limiter: self.discovery_limiter.clone(),
727        }
728    }
729}
730
731impl<S: Server> Handle<S> {
732    /// Issues command to [Runner].  Waits for the command to be next in line,
733    /// but does not wait for the command to be completed.
734    ///
735    /// May return `Err(())` when another command shutdown the server before this
736    /// command could be executed.
737    ///
738    /// When `Ok(())` is returned, this means the command is guaranteed to be executed momentarily.
739    pub(crate) async fn issue_command(&self, command: Command<S>) -> Result<(), ()> {
740        let (feedback_sender, feedback_receiver) = tokio::sync::oneshot::channel();
741
742        let result = self
743            .sender
744            .send(CommandRequest {
745                command,
746                feedback_sender,
747            })
748            .await;
749
750        if let Err(send_error) = result {
751            log::warn!(
752                "{server_name}: since the command receiver is closed (probably because the server is shutting down/restarting) we could not issue the command {cmd:?}",
753                server_name = S::NAME,
754                cmd = send_error.0.command.to_string(),
755            );
756            return Err(());
757        };
758
759        // Wait for the command to be the next in line.
760        //
761        // If feedback receiver returns an error, the command might not have executed.
762        feedback_receiver.await.map_err(|_| ())
763    }
764
765    pub async fn modify(
766        &self,
767        display: impl std::fmt::Display + Send + 'static,
768        modifier: impl FnOnce(&mut S) -> bool + Send + 'static,
769    ) -> Result<(), ()> {
770        self.issue_command(Command::Modify(Box::new((modifier, display))))
771            .await
772    }
773
774    /// Executes `inspector` on the server instance, returning its result.
775    ///
776    /// Returns Err(()) when the command or its result could not be sent, probably because the
777    /// server was shutting down.
778    pub async fn inspect<T: Send + 'static>(
779        &self,
780        display: impl std::fmt::Display + Send + 'static,
781        inspector: impl FnOnce(&S) -> T + Send + 'static,
782    ) -> Result<T, ()> {
783        let (sender, receiver) = tokio::sync::oneshot::channel::<T>();
784
785        self.issue_command(Command::Inspect(Box::new((
786            |server: &S| {
787                if sender.send(inspector(server)).is_err() {
788                    log::warn!(
789                        "{}: could not return result of inspection because receiver was already closed",
790                        S::NAME
791                    );
792                    // Might happen when the server is restarted ungracefully - so don't panic here.
793                }
794            },
795            display,
796        ))))
797        .await?;
798
799        receiver.await.map_err(|_| {
800            log::warn!(
801                "{server_name}: could receive result of inspector",
802                server_name = S::NAME,
803            );
804        })
805    }
806
807    pub async fn request_discovery(&self, app: Rc<S::AppT>) -> api::Result<api::DiscoveryRunResp> {
808        self.discovery_limiter.request_discovery::<S>(app).await
809    }
810}
811
812impl<S: Server> Runner<S> {
813    pub fn new(
814        global_config: &crate::servers::Config,
815        shutdown_receiver: tokio::sync::broadcast::Receiver<Infallible>,
816        worker_count: Option<NonZero<usize>>,
817        listener: Option<std::net::TcpListener>,
818    ) -> Result<Self> {
819        log::trace!("{}: creating runner...", S::NAME);
820
821        let pubhubs_server = Rc::new(S::new(global_config)?);
822
823        log::trace!("{}: created runner", S::NAME);
824
825        Ok(Runner {
826            pubhubs_server,
827            shutdown_receiver,
828            worker_count,
829            listener,
830            generation: 0,
831        })
832    }
833
834    fn create_actix_server(&self) -> Result<Handles<S>> {
835        let app_creator: S::AppCreatorT = self.pubhubs_server.deref().clone();
836
837        let (command_sender, command_receiver) = mpsc::channel(1);
838
839        let handle = Handle::<S> {
840            sender: command_sender,
841            discovery_limiter: DiscoveryLimiter::new(),
842        };
843
844        let ac_context: <S::AppCreatorT as AppCreator<S>>::ContextT = Default::default();
845
846        let server_config = self.pubhubs_server.server_config();
847
848        let bind_target: String = match self.listener.as_ref() {
849            // Pre-bound socket — the integration tests use this for ephemeral ports.
850            Some(listener) => format!("pre-bound socket {:?}", listener.local_addr()),
851            None => format!(
852                "{}, port {}",
853                server_config
854                    .ips
855                    .iter()
856                    .map(|ip| ip.to_string())
857                    .collect::<Box<[String]>>()
858                    .join(", "),
859                server_config.port,
860            ),
861        };
862
863        log::info!(
864            "{}:  binding actix server to {}, running on {:?}",
865            S::NAME,
866            bind_target,
867            std::thread::current().id()
868        );
869
870        let app_creator2 = app_creator.clone();
871        let handle2 = handle.clone();
872        let ac_context2 = ac_context.clone();
873        let generation: usize = self.generation;
874
875        let actual_actix_server: actix_web::dev::Server = {
876            // Build actix server
877            let mut builder: actix_web::HttpServer<_, _, _, _> =
878                actix_web::HttpServer::new(move || {
879                    let app: Rc<S::AppT> = Rc::new(app_creator2.clone().into_app(
880                        &handle2,
881                        &ac_context2,
882                        generation,
883                    ));
884
885                    actix_web::App::new().wrap(S::cors()).configure(
886                        |sc: &mut web::ServiceConfig| {
887                            // first configure endpoints common to all servers
888                            AppBase::<S>::configure_actix_app(&app, sc);
889
890                            // and then server-specific endpoints
891                            app.configure_actix_app(sc);
892
893                            let weak = Rc::downgrade(&app);
894
895                            tokio::task::spawn_local(async move {
896                                let thread = std::thread::current();
897
898                                log::debug!(
899                                    "{}: spawned local task on thread {:?}",
900                                    S::NAME,
901                                    thread.id()
902                                );
903
904                                let _deferred = defer(|| {
905                                    log::debug!(
906                                        "{}: local task on thread {:?} stopped",
907                                        S::NAME,
908                                        thread.id()
909                                    );
910                                });
911
912                                S::AppT::local_task(weak).await;
913                            });
914                        },
915                    )
916                })
917                .disable_signals(); // we handle signals ourselves
918
919            builder = match self.listener.as_ref() {
920                // Reuse the pre-bound listener (the integration tests use this for ephemeral
921                // ports), dup'd each restart so the held original keeps the port reserved.
922                // See [`SetOpts`].
923                Some(listener) => {
924                    builder.listen(listener.try_clone().context("cloning pre-bound listener")?)?
925                }
926                None => builder.bind(server_config)?,
927            };
928
929            if let Some(worker_count) = self.worker_count {
930                builder = builder.workers(worker_count.get());
931            }
932
933            builder.run()
934        };
935
936        // start actix server
937        let actix_server_handle = actual_actix_server.handle().clone();
938        let actix_join_handle = tokio::task::spawn(actual_actix_server);
939
940        // start PH server task (doing discovery)
941        let (ph_shutdown_sender, ph_shutdown_receiver) = tokio::sync::oneshot::channel();
942        let ph_join_handle =
943            tokio::task::spawn_local(self.pubhubs_server.clone().run_until_modifier(
944                ph_shutdown_receiver,
945                Rc::new(app_creator.into_app(&handle, &ac_context, generation)),
946            ));
947
948        Ok(Handles {
949            actix_server_handle,
950            actix_join_handle,
951            command_receiver,
952            ph_join_handle,
953            ph_shutdown_sender: Some(ph_shutdown_sender),
954            drop_bomb: crate::misc::drop_ext::Bomb::panic(|| {
955                format!("Part of {} was not shut down properly", S::NAME)
956            }),
957        })
958    }
959
960    pub async fn run(mut self) -> Result<()> {
961        loop {
962            let modifier = self.run_until_modifier().await?;
963
964            let pubhubs_server_mutref: &mut S =
965                Rc::get_mut(&mut self.pubhubs_server).expect("pubhubs_server is still borrowed");
966
967            let modifier_fmt = format!("{modifier}"); // so modifier can be consumed
968
969            log::info!("{}: applying modification {:?}", S::NAME, modifier_fmt);
970
971            if !modifier.modify(pubhubs_server_mutref) {
972                log::info!(
973                    "{}: not restarting upon request of {:?}",
974                    S::NAME,
975                    modifier_fmt
976                );
977                return Ok(());
978            }
979
980            self.generation += 1;
981            log::info!("{}: restarting...", S::NAME);
982        }
983    }
984
985    pub async fn run_until_modifier(
986        &mut self,
987    ) -> Result<crate::servers::server::BoxModifier<S>, anyhow::Error> {
988        let mut handles = self.create_actix_server()?;
989
990        let result = Self::run_until_modifier_inner(&mut handles, self).await;
991
992        handles.shutdown().await?;
993
994        result
995    }
996
997    async fn run_until_modifier_inner(
998        handles: &mut Handles<S>,
999        runner: &mut Runner<S>,
1000    ) -> Result<crate::servers::server::BoxModifier<S>, anyhow::Error> {
1001        loop {
1002            match handles.run_until_command(runner).await? {
1003                Command::Modify(modifier) => {
1004                    log::debug!(
1005                        "Stopping {} for modification {:?}...",
1006                        S::NAME,
1007                        modifier.to_string()
1008                    );
1009
1010                    return Ok::<_, anyhow::Error>(modifier);
1011                }
1012                Command::Inspect(inspector) => {
1013                    log::debug!(
1014                        "{}: applying inspection {:?}",
1015                        S::NAME,
1016                        inspector.to_string()
1017                    );
1018                    inspector.inspect(&*runner.pubhubs_server);
1019                }
1020                Command::Exit => {
1021                    log::debug!("Stopping {}, as requested", S::NAME);
1022
1023                    return Ok::<_, anyhow::Error>(Box::new(crate::servers::server::Exiter));
1024                }
1025            }
1026        }
1027    }
1028}