Skip to main content

tsoracle_standalone/
lib.rs

1//
2//  ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3//  ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4//  ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6//  tsoracle — Distributed Timestamp Oracle
7//  https://www.tsoracle.rs
8//
9//  Copyright (c) 2026 Prisma Risk
10//
11//  Licensed under the Apache License, Version 2.0 (the "License");
12//  you may not use this file except in compliance with the License.
13//  You may obtain a copy of the License at
14//
15//      https://www.apache.org/licenses/LICENSE-2.0
16//
17//  Unless required by applicable law or agreed to in writing, software
18//  distributed under the License is distributed on an "AS IS" BASIS,
19//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20//  See the License for the specific language governing permissions and
21//  limitations under the License.
22//
23
24//! Driver selection, configuration, and peer transport for a standalone
25//! tsoracle node. See `build`.
26//!
27//! [`Standalone::admin`] is the runtime membership-admin handle ([`MembershipAdmin`]): live for the openraft driver (add a learner, promote it to voter, remove a node, and list members — served over the `--admin-listen` gRPC port), and [`UnsupportedAdmin`] for the file and paxos drivers, which reject every mutating op.
28#![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))]
29
30use std::future::Future;
31use std::pin::Pin;
32use std::sync::Arc;
33
34use tsoracle_consensus::ConsensusDriver;
35
36mod config;
37pub use config::{
38    AdminTlsConfig, DriverConfig, FileConfig, MemberAddr, OpenraftConfig, PaxosConfig,
39    PeerTlsConfig, RaftTuning, parse_peer_map,
40};
41
42mod drivers;
43
44#[cfg(feature = "file")]
45pub use drivers::file::init_file_seeded;
46
47/// Test-only entry point that builds an openraft node from pre-bound listeners
48/// instead of binding internally. See `drivers::openraft::build_openraft_with_listeners`.
49#[cfg(all(any(test, feature = "test-support"), feature = "openraft"))]
50pub use drivers::openraft::build_openraft_with_listeners;
51
52/// Test-only entry point that builds a paxos node from a pre-bound peer
53/// listener. See `drivers::paxos::build_paxos_with_listeners`.
54#[cfg(all(any(test, feature = "test-support"), feature = "paxos"))]
55pub use drivers::paxos::build_paxos_with_listeners;
56
57mod error;
58pub use error::StandaloneError;
59
60/// Driver-agnostic membership-admin surface. Re-exported types at the crate
61/// root preserve the original public surface; the module itself is also
62/// public so the `test_support` sub-module is reachable when its feature is
63/// enabled.
64pub mod admin;
65pub use admin::{
66    AdminError, MemberEntry, MemberRole, MembershipAdmin, MembershipView, NewMember,
67    UnsupportedAdmin,
68};
69
70/// Generated `tsoracle.admin.v1` gRPC types (client + server). Public so the
71/// `tsoracle admin` CLI can use the client stub.
72#[cfg(feature = "openraft")]
73pub mod admin_proto {
74    tonic::include_proto!("tsoracle.admin.v1");
75}
76
77mod transport;
78pub use transport::{FatalSignal, TransportHandle};
79
80#[cfg(any(feature = "openraft", feature = "paxos"))]
81pub(crate) mod peer_tls;
82
83#[cfg(feature = "openraft")]
84pub(crate) mod admin_tls;
85
86/// A constructed, running standalone node: the consensus driver plus the
87/// background peer-transport task (if any). The caller (the bin) owns the
88/// client-facing `tsoracle_server::Server`; this type owns only the driver
89/// and peer transport.
90pub struct Standalone {
91    pub driver: Arc<dyn ConsensusDriver>,
92    transport: TransportHandle,
93    /// Driver-specific action run when the shutdown signal fires, BEFORE the
94    /// client server stops accepting (openraft: graceful leadership handoff;
95    /// file/paxos: none). Lazy — it reads live state when awaited at shutdown.
96    drain: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
97    /// Runtime membership administration for this driver.
98    pub admin: Arc<dyn MembershipAdmin>,
99    /// Peer transport for the membership-admin gRPC server (openraft only;
100    /// `noop` for file/paxos in this sub-project).
101    admin_transport: TransportHandle,
102    /// Address the admin gRPC server actually bound to, after `:0` resolution.
103    /// `None` when no admin listener was requested or for drivers that do not
104    /// serve an admin surface (file, paxos).
105    admin_listen_addr: Option<std::net::SocketAddr>,
106    /// Fail-fast signal shared by the transport supervisors: trips when a
107    /// peer or admin server task exits unexpectedly. Inert for the file
108    /// driver, which spawns no servers.
109    fatal: FatalSignal,
110}
111
112impl Standalone {
113    /// Take the pre-shutdown drain action, if the driver has one. Await the
114    /// returned future when the shutdown signal fires (before stopping the
115    /// client gRPC server), then call [`Standalone::shutdown`] for the peer
116    /// transport. `None` for drivers without a drain step (file, paxos).
117    pub fn take_drain(&mut self) -> Option<Pin<Box<dyn Future<Output = ()> + Send>>> {
118        self.drain.take()
119    }
120
121    /// Cooperatively stop the peer transport. Call off the same shutdown
122    /// signal that stops the client gRPC server.
123    pub async fn shutdown(mut self) {
124        self.admin_transport.shutdown().await;
125        self.transport.shutdown().await;
126    }
127
128    /// Address the admin gRPC server actually bound to (resolves `:0` to the
129    /// OS-picked port). `None` when no admin listener was requested or for
130    /// drivers without an admin surface.
131    pub fn admin_listen_addr(&self) -> Option<std::net::SocketAddr> {
132        self.admin_listen_addr
133    }
134
135    /// Fail-fast signal: resolves if a peer or admin transport server task
136    /// dies while the node is supposed to be healthy (issue #616's zombie
137    /// mode). The bin selects on [`FatalSignal::tripped`] next to the OS
138    /// shutdown signal and exits non-zero so an orchestrator restarts the
139    /// node instead of leaving the consensus driver running with no peer
140    /// transport.
141    pub fn fatal_signal(&self) -> FatalSignal {
142        self.fatal.clone()
143    }
144}
145
146/// Open storage, construct the selected driver, and spawn its peer transport
147/// (binding the peer listener before returning, so a bind failure is a
148/// startup error rather than a background log line).
149pub async fn build(cfg: DriverConfig) -> Result<Standalone, StandaloneError> {
150    match cfg {
151        #[cfg(feature = "file")]
152        DriverConfig::File(c) => drivers::file::build_file(c),
153        #[cfg(feature = "openraft")]
154        DriverConfig::Openraft(c) => drivers::openraft::build_openraft(c).await,
155        #[cfg(feature = "paxos")]
156        DriverConfig::Paxos(c) => drivers::paxos::build_paxos(c).await,
157    }
158}