Skip to main content

mitosis/router/
mod.rs

1//! The router: one TCP port for every clone, the clone picked by the database name of the startup
2//! message. Auth stays end to end: the router forwards the client's startup message (with the
3//! database renamed) to the clone's TCP port and then copies bytes. It never connects over the
4//! clone's Unix socket, which trusts every local user. It never logs message bodies.
5
6pub mod wire;
7
8use crate::clones;
9use crate::ctx::Ctx;
10use crate::events;
11use crate::lifecycle::log;
12use crate::names::format_duration;
13use crate::protocol::RouterStatus;
14use crate::state::{CloneRow, CloneStatus};
15use crate::templates::now;
16use serde_json::json;
17use std::collections::HashMap;
18use std::io;
19use std::net::SocketAddr;
20use std::sync::Arc;
21use std::time::Duration;
22use tokio::io::{AsyncReadExt, AsyncWriteExt};
23use tokio::net::{TcpListener, TcpStream};
24use tokio::sync::Notify;
25use tokio::task::JoinHandle;
26use wire::FirstMessage;
27
28/// How long a client may take to send its startup message (Postgres' `authentication_timeout`).
29const STARTUP_TIMEOUT: Duration = Duration::from_secs(60);
30const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
31/// Between bind attempts while the port is taken; `MITOSIS_ROUTER_RETRY_MS` shortens it in tests.
32const RETRY_MS: u64 = 60_000;
33
34/// What the router knows about open connections; read through `Ctx::router`.
35pub struct RouterState {
36    /// Clone id → open router connections.
37    pub conns: HashMap<String, usize>,
38    /// Clone id → unix time of the last router connect or disconnect.
39    pub last_active: HashMap<String, i64>,
40    /// `(pid, secret key)` from `BackendKeyData` → clone id, for cancel requests.
41    pub cancel: HashMap<(i32, Vec<u8>), String>,
42    pub status: RouterStatus,
43    /// The port is bound (`bind` succeeded); `status.listening` turns true once `spawn` accepts.
44    bound: bool,
45    /// Connection number → (clone id, notified to close that connection).
46    closers: HashMap<u64, (String, Arc<Notify>)>,
47    next_conn: u64,
48}
49
50impl RouterState {
51    pub fn new(port: u16) -> RouterState {
52        RouterState {
53            conns: HashMap::new(),
54            last_active: HashMap::new(),
55            cancel: HashMap::new(),
56            status: RouterStatus {
57                listening: false,
58                listen: String::new(),
59                port,
60                error: None,
61            },
62            bound: false,
63            closers: HashMap::new(),
64            next_conn: 0,
65        }
66    }
67
68    pub fn open_conns(&self, id: &str) -> usize {
69        self.conns.get(id).copied().unwrap_or(0)
70    }
71
72    pub fn last_active(&self, id: &str) -> Option<i64> {
73        self.last_active.get(id).copied()
74    }
75
76    /// Closes every client connection to clone `id` (on destroy or sleep).
77    pub fn close_clone(&mut self, id: &str) {
78        for (clone, close) in self.closers.values() {
79            if clone == id {
80                // A stored permit: the connection sees it even if it is not waiting yet.
81                close.notify_one();
82            }
83        }
84    }
85}
86
87/// True while the router is on and its port is bound (connections queue until `spawn` accepts
88/// them): only then can a sleeping clone be woken by a connection, so only then may clones be put
89/// to sleep.
90pub fn listening(ctx: &Ctx) -> bool {
91    ctx.config().router && {
92        let r = ctx.router();
93        r.bound || r.status.listening
94    }
95}
96
97/// The warning `clone` responses carry while the router is on but not listening.
98pub fn warning(ctx: &Ctx) -> Option<String> {
99    if !ctx.config().router {
100        return None;
101    }
102    let error = ctx.router().status.error.clone()?;
103    Some(format!(
104        "the router is not listening ({error}); it retries every minute. Until then use the \
105         direct URL (mitosis env <clone> --direct, or direct_url); clones do not sleep \
106         meanwhile, and mitosis wake <clone> starts one that already sleeps"
107    ))
108}
109
110fn retry_interval() -> Duration {
111    let ms = std::env::var("MITOSIS_ROUTER_RETRY_MS")
112        .ok()
113        .and_then(|v| v.parse::<u64>().ok())
114        .filter(|ms| *ms > 0)
115        .unwrap_or(RETRY_MS);
116    Duration::from_millis(ms)
117}
118
119/// Binds `router_listen:router_port` once and records the outcome: on success the port counts as
120/// bound (see `listening`); `spawn` reports it listening once it accepts. On failure the status
121/// holds the error and `router.failed` is recorded (once per distinct error). The daemon binds
122/// before its startup gc, so recovery knows whether the router can wake clones.
123pub async fn bind(ctx: &Ctx) -> Option<TcpListener> {
124    let cfg = ctx.config();
125    let addr = SocketAddr::new(cfg.router_listen, cfg.router_port);
126    match TcpListener::bind(addr).await {
127        Ok(l) => {
128            let mut r = ctx.router();
129            r.bound = true;
130            r.status = RouterStatus {
131                listening: false,
132                listen: addr.ip().to_string(),
133                port: addr.port(),
134                error: None,
135            };
136            Some(l)
137        }
138        Err(e) => {
139            let error = format!("cannot listen on {addr}: {e}");
140            let first = {
141                let mut r = ctx.router();
142                r.bound = false;
143                let first = r.status.error.as_deref() != Some(error.as_str());
144                r.status = RouterStatus {
145                    listening: false,
146                    listen: addr.ip().to_string(),
147                    port: addr.port(),
148                    error: Some(error.clone()),
149                };
150                first
151            };
152            if first {
153                log(
154                    ctx,
155                    &format!(
156                        "router: {error}; retrying every {}",
157                        format_duration(retry_interval())
158                    ),
159                );
160                events::emit(
161                    ctx,
162                    events::ROUTER_FAILED,
163                    None,
164                    None,
165                    json!({ "listen": addr.to_string(), "error": e.to_string() }),
166                );
167            }
168            None
169        }
170    }
171}
172
173/// Serves connections on `listener` (from `bind`); without one, retries the bind every minute
174/// first. Abort the handle to stop accepting. Open connections end when the process exits.
175pub fn spawn(ctx: Arc<Ctx>, listener: Option<TcpListener>) -> JoinHandle<()> {
176    tokio::spawn(async move {
177        let mut listener = listener;
178        let listener = loop {
179            if let Some(l) = listener.take() {
180                break l;
181            }
182            tokio::time::sleep(retry_interval()).await;
183            listener = bind(&ctx).await;
184        };
185        ctx.router().status.listening = true;
186        let addr = listener
187            .local_addr()
188            .map_or_else(|_| "?".to_string(), |a| a.to_string());
189        log(&ctx, &format!("router listening on {addr}"));
190        events::emit(
191            &ctx,
192            events::ROUTER_STARTED,
193            None,
194            None,
195            json!({ "listen": addr }),
196        );
197        loop {
198            match listener.accept().await {
199                Ok((stream, _)) => {
200                    tokio::spawn(connection(ctx.clone(), stream));
201                }
202                // EMFILE and the like: back off instead of spinning.
203                Err(_) => tokio::time::sleep(Duration::from_millis(100)).await,
204            }
205        }
206    })
207}
208
209/// One client connection; errors only end it (a client hanging up is normal).
210async fn connection(ctx: Arc<Ctx>, mut client: TcpStream) {
211    let _ = client.set_nodelay(true);
212    let first = match tokio::time::timeout(STARTUP_TIMEOUT, first_message(&mut client)).await {
213        Ok(Ok(m)) => m,
214        _ => return,
215    };
216    match first {
217        FirstMessage::Ssl | FirstMessage::GssEnc => {}
218        FirstMessage::DirectTls => log(
219            &ctx,
220            "router: closed a direct TLS connection (no TLS through the router; use sslmode=disable or prefer)",
221        ),
222        FirstMessage::Cancel { pid, key } => {
223            let _ = cancel(&ctx, pid, key).await;
224        }
225        FirstMessage::Startup {
226            major,
227            minor,
228            params,
229        } => {
230            let _ = startup(ctx, client, major, minor, params).await;
231        }
232    }
233}
234
235/// The first message after at most one SSL and one GSS refusal (`N`, no TLS through the router).
236async fn first_message(client: &mut TcpStream) -> io::Result<FirstMessage> {
237    let mut first = wire::read_first(client).await?;
238    for _ in 0..2 {
239        if !matches!(first, FirstMessage::Ssl | FirstMessage::GssEnc) {
240            break;
241        }
242        client.write_all(b"N").await?;
243        first = wire::read_first(client).await?;
244    }
245    Ok(first)
246}
247
248/// Sends a cancel request on to the clone whose backend handed out `(pid, key)`; unknown keys are
249/// dropped silently, as Postgres does.
250async fn cancel(ctx: &Ctx, pid: i32, key: Vec<u8>) -> io::Result<()> {
251    let Some(id) = ctx.router().cancel.get(&(pid, key.clone())).cloned() else {
252        return Ok(());
253    };
254    let Some(port) = ctx.store().get_clone(&id).ok().flatten().map(|r| r.port) else {
255        return Ok(());
256    };
257    let mut server =
258        tokio::time::timeout(CONNECT_TIMEOUT, TcpStream::connect(("127.0.0.1", port))).await??;
259    server.write_all(&wire::cancel_bytes(pid, &key)).await?;
260    // Postgres closes once it has read the request; waiting for that keeps it from being cut off.
261    let _ = tokio::time::timeout(CONNECT_TIMEOUT, server.read(&mut [0u8; 1])).await;
262    Ok(())
263}
264
265/// The clone named `db`, else the clone with id `db`.
266fn lookup(ctx: &Ctx, db: &str) -> crate::error::Result<Option<CloneRow>> {
267    let rows = ctx.store().list_clones()?;
268    Ok(rows
269        .iter()
270        .find(|c| c.name.as_deref() == Some(db))
271        .or_else(|| rows.iter().find(|c| c.id == db))
272        .cloned())
273}
274
275async fn refuse(
276    client: &mut TcpStream,
277    sqlstate: &str,
278    msg: &str,
279    hint: Option<&str>,
280) -> io::Result<()> {
281    client
282        .write_all(&wire::error_response(sqlstate, msg, hint))
283        .await
284}
285
286/// Counts a connection to a clone while it lives; dropping it (also on a panic or abort) undoes
287/// the count, records `last_active` and forgets the connection's cancel key.
288struct Tracked {
289    ctx: Arc<Ctx>,
290    conn: u64,
291    clone: String,
292    key: Option<(i32, Vec<u8>)>,
293}
294
295impl Tracked {
296    fn begin(ctx: &Arc<Ctx>, clone: &str) -> (Tracked, Arc<Notify>) {
297        let close = Arc::new(Notify::new());
298        let mut r = ctx.router();
299        r.next_conn += 1;
300        let conn = r.next_conn;
301        r.closers.insert(conn, (clone.to_string(), close.clone()));
302        *r.conns.entry(clone.to_string()).or_default() += 1;
303        r.last_active.insert(clone.to_string(), now());
304        drop(r);
305        let tracked = Tracked {
306            ctx: ctx.clone(),
307            conn,
308            clone: clone.to_string(),
309            key: None,
310        };
311        (tracked, close)
312    }
313
314    fn record_key(&mut self, key: (i32, Vec<u8>)) {
315        let mut r = self.ctx.router();
316        if let Some(old) = self.key.take() {
317            r.cancel.remove(&old);
318        }
319        r.cancel.insert(key.clone(), self.clone.clone());
320        self.key = Some(key);
321    }
322}
323
324impl Drop for Tracked {
325    fn drop(&mut self) {
326        let mut r = self.ctx.router();
327        r.closers.remove(&self.conn);
328        if let Some(n) = r.conns.get_mut(&self.clone) {
329            *n = n.saturating_sub(1);
330            if *n == 0 {
331                r.conns.remove(&self.clone);
332            }
333        }
334        r.last_active.insert(self.clone.clone(), now());
335        if let Some(k) = self.key.take() {
336            r.cancel.remove(&k);
337        }
338    }
339}
340
341async fn startup(
342    ctx: Arc<Ctx>,
343    mut client: TcpStream,
344    major: u16,
345    minor: u16,
346    mut params: Vec<(String, String)>,
347) -> io::Result<()> {
348    if major != 3 {
349        let msg = format!("unsupported frontend protocol {major}.{minor}: the router speaks 3.x");
350        return refuse(&mut client, "0A000", &msg, None).await;
351    }
352    let param = |name: &str| {
353        params
354            .iter()
355            .find(|(k, _)| k == name)
356            .map(|(_, v)| v.clone())
357    };
358    // Postgres' rule: no database means the user name.
359    let db = param("database")
360        .filter(|d| !d.is_empty())
361        .or_else(|| param("user"))
362        .unwrap_or_default();
363    let row = match lookup(&ctx, &db) {
364        Ok(Some(row)) => row,
365        Ok(None) => {
366            let hint = format!(
367                "connect as postgres://postgres:<password>@127.0.0.1:{}/<clone name or id>",
368                ctx.config().router_port
369            );
370            let msg = format!("no clone named \"{db}\" (the database name picks the clone)");
371            return refuse(&mut client, "3D000", &msg, Some(&hint)).await;
372        }
373        Err(e) => return refuse(&mut client, "58000", &e.message, None).await,
374    };
375    // Counted before the status check: a sleep that has not marked the clone yet sees the count
376    // and backs off; one that has is seen here, and the clone is woken again.
377    let mut tries = 0;
378    let (row, mut tracked, close) = loop {
379        tries += 1;
380        let row = match clones::ensure_running(&ctx, &row.id).await {
381            Ok(row) => row,
382            Err(e) => return refuse(&mut client, "57P03", &e.message, e.hint.as_deref()).await,
383        };
384        let (tracked, close) = Tracked::begin(&ctx, &row.id);
385        let same = ctx
386            .store()
387            .get_clone(&row.id)
388            .ok()
389            .flatten()
390            .is_some_and(|r| r.status == CloneStatus::Ready && r.port == row.port);
391        if same {
392            break (row, tracked, close);
393        }
394        if tries >= 3 {
395            // Slept, woken or moved under this connection three times: say so rather than try
396            // a port that may not answer.
397            drop(tracked);
398            let label = row.name.as_deref().unwrap_or(&row.id);
399            let msg = format!("clone {label} is changing state; retry");
400            return refuse(&mut client, "57P03", &msg, None).await;
401        }
402    };
403    match params.iter_mut().find(|(k, _)| k == "database") {
404        Some((_, v)) => *v = row.database.clone(),
405        None => params.push(("database".into(), row.database.clone())),
406    }
407    let label = row.name.as_deref().unwrap_or(&row.id).to_string();
408    let server =
409        match tokio::time::timeout(CONNECT_TIMEOUT, TcpStream::connect(("127.0.0.1", row.port)))
410            .await
411        {
412            Ok(Ok(s)) => s,
413            _ => {
414                let msg = format!("clone {label} does not answer on its port {}", row.port);
415                return refuse(&mut client, "57P03", &msg, None).await;
416            }
417        };
418    let _ = server.set_nodelay(true);
419
420    let (mut cr, mut cw) = client.into_split();
421    let (mut sr, mut sw) = server.into_split();
422    sw.write_all(&wire::startup_bytes(major, minor, &params))
423        .await?;
424    let up = async {
425        let _ = tokio::io::copy(&mut cr, &mut sw).await;
426        let _ = sw.shutdown().await;
427    };
428    // The server's startup messages go through one by one, so the cancel key can be read; from
429    // ReadyForQuery on it is a plain copy.
430    let down = async {
431        loop {
432            let (tag, body) = wire::read_message(&mut sr).await?;
433            if tag == b'K' {
434                if let Some(key) = wire::parse_backend_key(&body) {
435                    tracked.record_key(key);
436                }
437            }
438            cw.write_all(&wire::message(tag, &body)).await?;
439            if tag == b'Z' {
440                break;
441            }
442        }
443        tokio::io::copy(&mut sr, &mut cw).await?;
444        io::Result::Ok(())
445    };
446    tokio::pin!(up, down);
447    // The connection ends when the server side ends (the client hanging up shuts the server's
448    // write side, so the server ends next), or when the clone is closed.
449    tokio::select! {
450        _ = close.notified() => {}
451        _ = &mut down => {}
452        _ = &mut up => {
453            tokio::select! {
454                _ = close.notified() => {}
455                _ = &mut down => {}
456            }
457        }
458    }
459    Ok(())
460}