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}