Skip to content

Commit b9b8f28

Browse files
committed
On listeners, addresses, and ports
This change reworks how we define and manage network ports, and achieves a bunch of goals in one go, at the cost of being quite big, of course. ## Unix-socket all the things Restate server now supports listening on unix-sockets on all services (fabric, admin, ingress, and tokio-console). But even better, we listen on *both* the inet socket and unix-socket by default. Unix sockets get automatically created under the `restate-data/*.sock` and get cleaned up on shutdown (even if not, they are cleaned up on the next start). The unix-socket support include restatectl and restate CLIs. `restatectl -s unix:restate-data/admin.sock status` and setting env variables like `RESTATE_ADMIN_URL=unix:restate-data/admin.sock` will work for `restate svc status`. You can also use `curl` to call ingress like `curl --unix-socket restate-data/ingress.sock [http://local/Counter/123/add](http://test/Counter/123/add) --silent --json 1` (note that the hostname in the URL is ignored when connecting with unix-sockets. Listening on unix-sockets can be disabled on all ports or on certain services, with a new option `listen-mode` that can be supplied in env-variable, config file, or `restate-server --listen-mode=tcp`. Listen modes support `tcp`, `unix`, and `all (default)`. When using `unix` we’ll only listen on unix-sockets and all advertised addresses will automatically be derived to show the unix-socket address. As a result of this change, all unit tests now use unix-sockets, no more port conflicts with your locally running services and potentially less flaky tests on CI. I’ve updated (and simplified) the local-cluster-runner utility to make use of it. There is more room for more improvements there still. ## Let’s talk about ports **Random Ports** Restate can now select random ports on startup by `restate-server --use-random-ports=true` or `RESTATE_USE_RANDOM_PORTS=true`. Those are conflict-free (os-selected) and because unix-sockets are also created, users (in the future) can use restate/restatectl to by pointing them to the unix-socket until they figure the ports. We print the advertised addresses for all services on startup and with the new `restate-server --no-logo` advertiseds address will be the first thing printed on stdout. **Socket Activation** Another cool feature is the support for LISTEN_FD/systemd compatible file-descriptor passing for listener sockets from the parent process to restate-server. A parent process can open the tcp listeners and even the unix-socket listeners (except for fabric port) and pass the file descriptors to restate-server (i.e. via systemd socket activation, or a utility like `systemfd` ). for instance `systemfd --no-pid -s http::9000 -- restate-server` where `9000` becomes the ingress port. You can pass multiple ports and restate has a certain order to assign those ports. *What does this bring to the table?* 1. Restarting restate-server without losing the socket listeners (ingress is the biggest winner), so clients will not observe connection errors during restart or upgrades. 2. Test harnesses, wrappers, or even our own tools can pre-allocate the tcp ports, and listen on them before starting restate. Those external wrappers don’t need to wait any more for restate-server to start before they try to connect to it (no connection retries needed). **This unlocks embedding restate in tests or shipping a restate-lite version that only listens on anonymous unix sockets**. In fact, we are a couple of steps away from making it possible to fully embed restate for use-cases that don’t need a server. The only small thing that’s missing is the invoker using a pre-supplied file-descriptor and/or support unix-sockets to connect to deployments. 3. Restate server will now attempt to bind on all required ports and unix-sockets very early in its startup, before starting any roles or opening the database. This reduces the downtime window, and allows us to centralize port assignment (for random) and gives us a nice place to print all addresses. ## Advertised Addresses The PR unifies how we manage and configure advertised addresses for all services, it deprecates some of the old inconsistently named configuration keys (admin and ingress advertised addresses). But most importantly, restate-server will now attempt to detect a reasonable value for the advertised address. If the restate is listen mode is `unix` (only), it’ll now print `unix:/` advertised addresses, and if it’s tcp, it’ll try and detect the public routable IP address of the node instead of using `127.0.0.1` This makes docker deployments much nicer while maintaining to override all of them as needed. There is also a new option to override the hostname part of this address only without interfering with random ports `RESTATE_ADVERTISED_HOST=my_host.com` (or via cli, config) for global override, and it can be applied per service (`RESTATE_ADMIN__ADVERTISED_HOST=`). In fact, all new options can be overridden per service. Additionally, all addresses and ports are now managed by a new component `AddressBook` that’s available via task-center. The address book is what powers handing off listeners down to services and it provides an interface to query all bound addresses such that we can return them in future `GetIdent` responses (not implemented yet). A related improvement is how we configure `metadata-client` ’s `addresses`. Nodes now do not need to supply their own node advertised address in `addresses`. They only need to know about one or more of their peers but we'll now automatically include our own node if it's running a metadata server, thanks to early port binding and the `AddressBook`, this makes `addresses` field in config completely optional and for single-node setups, it's now empty by default. This opens the door (not implemented) to adding support for `restate-server --peer=<address>` which would allow restate nodes to join a cluster and bootstrap completely by connecting to known peer address. This will let is figure its own correct advertised address, its metadata configuration without passing a configuration file. ## Misc - New config options (global and with per-service override) `bind-port`, `bind-ip`, `advertised-host`, `use-random-ports` and `listen-mode` - `restate-hyper-uds` crate to support using unix-socket with hyper clients, we should have a **config-gated** option to allow invoking deployments via unix-sockets too (any takers?) - Port numbers, unix socket names, and service names are defined in a set of zero-cost types in `restate-types::net::address` - Type-checked usage of addresses in all the codebase to denote which services they're meant to refer, this avoid confusion where a type like `AdvertisedAddress` didn't make it clear which service. You’ll see types like `AdvertisedAddress<AdminPort>` everywhere now. - Documentation and configuration json schema express the service name and defaults according to the `ListenerPort` type parameter. # What did we lose? - For simplicity and to reduce confusion, unix-socket paths are not configurable anymore through `bind-address`, they will now be always created under restate_data directory (fabric.sock, ingress.sock, admin.sock, and tokio.sock (if enabled). The socket files are deleted on process shutdown. The benefit is that their locations are predictable for tools, users, and system operators. `restatectl -s unix:restate_data/admin.sock status` - Unix socket names have a limit of ~108 bytes in most unix systems, this puts a limit over the path length of restate-data, I've included a small optimization that converts the path into relative if CWD is a prefix of the data-dir but this is not guaranteed solution. I'd say we evaluate how much is this going to be a problem in practice and we can provide a configurable base directory for unix-sockets via config and env variable as needed. It's literally a single variable in AddressBook.
1 parent 158a16d commit b9b8f28

File tree

82 files changed

+2826
-1413
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+2826
-1413
lines changed

Cargo.lock

Lines changed: 38 additions & 38 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ restate-encoding = { path = "crates/encoding" }
5050
restate-errors = { path = "crates/errors" }
5151
restate-fs-util = { path = "crates/fs-util" }
5252
restate-futures-util = { path = "crates/futures-util" }
53+
restate-hyper-uds = { path = "crates/hyper-uds" }
5354
restate-ingress-http = { path = "crates/ingress-http" }
5455
restate-ingress-kafka = { path = "crates/ingress-kafka" }
5556
restate-invoker-api = { path = "crates/invoker-api" }
@@ -190,7 +191,7 @@ rangemap = "1.5.1"
190191
rayon = { version = "1.10" }
191192
regex = { version = "1.11" }
192193
regress = { version = "0.10" }
193-
reqwest = { version = "0.12.5", default-features = false, features = [
194+
reqwest = { version = "0.12", default-features = false, features = [
194195
"json",
195196
"rustls-tls",
196197
"stream",

benchmarks/src/lib.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@
99
// by the Apache License, Version 2.0.
1010

1111
#![allow(clippy::async_yields_async)]
12-
1312
//! Utilities for benchmarking the Restate runtime
13+
14+
use std::net::SocketAddr;
15+
use std::time::Duration;
16+
1417
use anyhow::anyhow;
1518
use futures_util::{TryFutureExt, future};
1619
use http::Uri;
1720
use http::header::CONTENT_TYPE;
1821
use pprof::flamegraph::Options;
22+
use tokio::net::TcpListener;
23+
use tokio::runtime::Runtime;
24+
use tokio::sync::oneshot;
25+
use tracing::warn;
26+
1927
use restate_core::{TaskCenter, TaskCenterBuilder, TaskKind, cancellation_token, task_center};
2028
use restate_node::Node;
2129
use restate_rocksdb::RocksDbManager;
@@ -26,11 +34,8 @@ use restate_types::config::{
2634
};
2735
use restate_types::config_loader::ConfigLoaderBuilder;
2836
use restate_types::logs::metadata::ProviderKind;
37+
use restate_types::net::listener::AddressBook;
2938
use restate_types::retries::RetryPolicy;
30-
use std::time::Duration;
31-
use tokio::runtime::Runtime;
32-
use tokio::sync::oneshot;
33-
use tracing::warn;
3439

3540
pub fn discover_deployment(current_thread_rt: &Runtime, address: Uri) {
3641
let client = reqwest::Client::builder()
@@ -102,16 +107,23 @@ pub fn spawn_restate(config: Configuration) -> task_center::Handle {
102107
.build()
103108
.expect("task_center builds")
104109
.into_handle();
110+
105111
let mut prometheus = Prometheus::install(&config.common);
106112
restate_types::config::set_current_config(config.clone());
113+
114+
let mut address_book = AddressBook::new(restate_types::config::node_filepath(""));
107115
let live_config = Configuration::live();
108116

109117
tc.block_on(async {
110118
RocksDbManager::init();
111119
prometheus.start_upkeep_task();
112120

121+
if let Err(err) = address_book.bind_from_config(&live_config.pinned()).await {
122+
panic!("Failed to bind address book: {err}");
123+
}
124+
113125
TaskCenter::spawn(TaskKind::SystemBoot, "restate", async move {
114-
let node = Node::create(live_config, prometheus)
126+
let node = Node::create(live_config, prometheus, address_book)
115127
.await
116128
.expect("Restate node must build");
117129
node.start().await
@@ -126,9 +138,12 @@ pub fn spawn_mock_service_endpoint(task_center_handle: &task_center::Handle) {
126138
task_center_handle.block_on(async {
127139
let (running_tx, running_rx) = oneshot::channel();
128140
TaskCenter::spawn(TaskKind::TestRunner, "mock-service-endpoint", async move {
141+
let addr: SocketAddr = ([127, 0, 0, 1], 9080).into();
142+
let listener = TcpListener::bind(addr).await?;
143+
129144
cancellation_token()
130145
.run_until_cancelled(mock_service_endpoint::listener::run_listener(
131-
"127.0.0.1:9080".parse().expect("valid socket addr"),
146+
listener,
132147
|| {
133148
let _ = running_tx.send(());
134149
},

cli/src/cli_env.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use serde::{Deserialize, Serialize};
2020
use url::Url;
2121

2222
use restate_cli_util::OsEnv;
23+
use restate_types::net::address::{AdminPort, AdvertisedAddress, HttpIngressPort};
2324

2425
use crate::app::GlobalOpts;
2526

@@ -47,8 +48,8 @@ pub const EDITOR_ENV: &str = "RESTATE_EDITOR";
4748
pub struct CliConfig {
4849
pub environment_type: EnvironmentType,
4950

50-
pub ingress_base_url: Option<Url>,
51-
pub admin_base_url: Option<Url>,
51+
pub ingress_base_url: Option<AdvertisedAddress<HttpIngressPort>>,
52+
pub admin_base_url: Option<AdvertisedAddress<AdminPort>>,
5253
pub bearer_token: Option<String>,
5354

5455
#[cfg(feature = "cloud")]
@@ -62,8 +63,8 @@ impl CliConfig {
6263
Self {
6364
environment_type: EnvironmentType::Default,
6465

65-
ingress_base_url: Some(Url::parse("http://localhost:8080/").unwrap()),
66-
admin_base_url: Some(Url::parse("http://localhost:9070/").unwrap()),
66+
ingress_base_url: Some(AdvertisedAddress::default()),
67+
admin_base_url: Some(AdvertisedAddress::default()),
6768
bearer_token: None,
6869

6970
#[cfg(feature = "cloud")]
@@ -251,7 +252,7 @@ impl CliEnv {
251252
}
252253
}
253254

254-
pub fn ingress_base_url(&self) -> Result<&Url> {
255+
pub fn ingress_base_url(&self) -> Result<&AdvertisedAddress<HttpIngressPort>> {
255256
match self.config.ingress_base_url.as_ref() {
256257
Some(ingress_base_url) => Ok(ingress_base_url),
257258
None => Err(anyhow!(
@@ -262,7 +263,7 @@ impl CliEnv {
262263
}
263264
}
264265

265-
pub fn admin_base_url(&self) -> Result<&Url> {
266+
pub fn admin_base_url(&self) -> Result<&AdvertisedAddress<AdminPort>> {
266267
match self.config.admin_base_url.as_ref() {
267268
Some(admin_base_url) => Ok(admin_base_url),
268269
None => Err(anyhow!(
@@ -380,15 +381,15 @@ mod tests {
380381
.ingress_base_url
381382
.expect("ingress_base_url must be provided")
382383
.to_string(),
383-
"http://localhost:8080/".to_string()
384+
"http://127.0.0.1:8080/".to_string()
384385
);
385386
assert_eq!(
386387
cli_env
387388
.config
388389
.admin_base_url
389390
.expect("admin_base_url must be provided")
390391
.to_string(),
391-
"http://localhost:9070/".to_string()
392+
"http://127.0.0.1:9070/".to_string()
392393
);
393394

394395
// Defaults are templated over RESTATE_HOST

0 commit comments

Comments
 (0)