Initial commit
This commit is contained in:
commit
d883c9b10b
10 changed files with 1379 additions and 0 deletions
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = true
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1055
Cargo.lock
generated
Normal file
1055
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "cerulea_relay"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.93"
|
||||
fastwebsockets = { version = "0.8.0", features = ["hyper", "unstable-split", "upgrade"] }
|
||||
http-body-util = "0.1.2"
|
||||
hyper = { version = "1.5.1", features = ["client", "full", "http1", "http2", "server"] }
|
||||
hyper-util = { version = "0.1.10", features = ["tokio", "server", "client", "http1", "http2"] }
|
||||
pin-project-lite = "0.2.15"
|
||||
qstring = "0.7.2"
|
||||
tap = "1.0.1"
|
||||
tokio = { version = "1.41.1", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
uuid = { version = "1.11.0", features = ["v4"] }
|
8
README.md
Normal file
8
README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# cerulea-relay
|
||||
|
||||
Realtime relay (1hr backfill window) for PDSes with fewer than 1000 repos.
|
||||
|
||||
The idea is that we can have much larger limits if we scale down the volume of the network.
|
||||
- Large block sizes
|
||||
- Large record size limit
|
||||
- etcetcetc
|
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod prelude;
|
||||
|
||||
pub mod relay_subscription;
|
||||
pub mod server;
|
21
src/main.rs
Normal file
21
src/main.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use anyhow::Result;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
use cerulea_relay::server::{self, RelayServer};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer())
|
||||
.with(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let server = Arc::new(RelayServer::default());
|
||||
|
||||
// TODO: scrape some dudes
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
server::listen(Arc::clone(&server), addr).await?;
|
||||
Ok(())
|
||||
}
|
1
src/prelude.rs
Normal file
1
src/prelude.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub use tap::prelude::*;
|
183
src/relay_subscription.rs
Normal file
183
src/relay_subscription.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use fastwebsockets::{
|
||||
upgrade::{is_upgrade_request, upgrade},
|
||||
FragmentCollectorRead, Frame, OpCode, Payload, WebSocket, WebSocketError, WebSocketWrite,
|
||||
};
|
||||
use hyper::{
|
||||
body::{Bytes, Incoming},
|
||||
upgrade::Upgraded,
|
||||
Request, Response, StatusCode,
|
||||
};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use qstring::QString;
|
||||
use tokio::{
|
||||
net::{
|
||||
tcp::{OwnedReadHalf, OwnedWriteHalf},
|
||||
TcpStream,
|
||||
},
|
||||
sync::broadcast::{error::RecvError, Receiver},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::server::{empty, full, RelayServer, ServerResponse};
|
||||
|
||||
enum Operation<'f> {
|
||||
NoOp,
|
||||
WriteBlock(Bytes),
|
||||
WriteFrame(Frame<'f>),
|
||||
ExitWithFrame(Frame<'f>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
struct RelaySubscription {
|
||||
id: Uuid,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
type WSRead = FragmentCollectorRead<OwnedReadHalf>;
|
||||
type WSWrite = WebSocketWrite<OwnedWriteHalf>;
|
||||
|
||||
impl RelaySubscription {
|
||||
fn create(mut ws: WebSocket<TokioIo<Upgraded>>) -> (Self, WSRead, WSWrite) {
|
||||
ws.set_auto_close(false);
|
||||
ws.set_auto_pong(false);
|
||||
|
||||
let (ws_rx, ws_tx) = ws.split(|stream| {
|
||||
let upgraded = stream.into_inner();
|
||||
let parts = upgraded.downcast::<TokioIo<TcpStream>>().expect("uhhhh");
|
||||
let (read, write) = parts.io.into_inner().into_split();
|
||||
(read, write)
|
||||
});
|
||||
let ws_rx = FragmentCollectorRead::new(ws_rx);
|
||||
|
||||
let sub = RelaySubscription {
|
||||
id: Uuid::new_v4(),
|
||||
running: true,
|
||||
};
|
||||
|
||||
(sub, ws_rx, ws_tx)
|
||||
}
|
||||
|
||||
async fn dispatch_operation(&mut self, ws_tx: &mut WSWrite, op: Operation<'_>) {
|
||||
if let Err(e) = match op {
|
||||
Operation::NoOp => return,
|
||||
Operation::WriteBlock(bytes) => {
|
||||
ws_tx
|
||||
.write_frame(Frame::binary(Payload::Borrowed(&bytes)))
|
||||
.await
|
||||
}
|
||||
Operation::WriteFrame(frame) => ws_tx.write_frame(frame).await,
|
||||
Operation::ExitWithFrame(frame) => {
|
||||
let _ = ws_tx.write_frame(frame).await;
|
||||
self.running = false;
|
||||
return;
|
||||
}
|
||||
Operation::Exit => {
|
||||
self.running = false;
|
||||
return;
|
||||
}
|
||||
} {
|
||||
tracing::warn!("Encountered error: {:?}", e);
|
||||
self.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_frame<'f>(ws_rx: &mut WSRead) -> Operation<'f> {
|
||||
match ws_rx
|
||||
.read_frame::<_, WebSocketError>(&mut move |_| async {
|
||||
unreachable!() // it'll be fiiiine :3
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(frame) if frame.opcode == OpCode::Ping => {
|
||||
Operation::WriteFrame(Frame::pong(frame.payload))
|
||||
}
|
||||
Ok(frame) if frame.opcode == OpCode::Close => {
|
||||
Operation::ExitWithFrame(Frame::close_raw(frame.payload))
|
||||
}
|
||||
Ok(_frame) => {
|
||||
Operation::NoOp // discard
|
||||
}
|
||||
Err(_e) => Operation::Exit,
|
||||
}
|
||||
}
|
||||
|
||||
async fn rebroadcast_block<'f>(block_rx: &mut Receiver<Bytes>) -> Operation<'f> {
|
||||
match block_rx.recv().await {
|
||||
Ok(block) => Operation::WriteBlock(block),
|
||||
Err(RecvError::Closed) => Operation::ExitWithFrame(Frame::close(1001, b"Going away")),
|
||||
Err(RecvError::Lagged(_)) => {
|
||||
Operation::ExitWithFrame(Frame::close(1008, b"Client too slow"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_subscription(
|
||||
server: Arc<RelayServer>,
|
||||
req: Request<Incoming>,
|
||||
ws: WebSocket<TokioIo<Upgraded>>,
|
||||
) {
|
||||
let query = req.uri().query().map(QString::from);
|
||||
let cursor: Option<usize> = query
|
||||
.as_ref()
|
||||
.and_then(|q| q.get("cursor"))
|
||||
.and_then(|s| s.parse().ok());
|
||||
|
||||
let (mut sub, mut ws_rx, mut ws_tx) = RelaySubscription::create(ws);
|
||||
|
||||
tracing::debug!(id = %sub.id, "subscription started");
|
||||
|
||||
if let Some(_cursor) = cursor {
|
||||
tracing::debug!(id = %sub.id, "filling from event cache");
|
||||
|
||||
// TODO: cursor catchup
|
||||
|
||||
tracing::debug!(id = %sub.id, "subscription live-tailing");
|
||||
}
|
||||
|
||||
// live tailing:
|
||||
let mut block_rx = server.block_tx.subscribe();
|
||||
while sub.running {
|
||||
let op = tokio::select! {
|
||||
biased;
|
||||
op = rebroadcast_block(&mut block_rx) => op,
|
||||
op = read_frame(&mut ws_rx) => op,
|
||||
};
|
||||
sub.dispatch_operation(&mut ws_tx, op).await;
|
||||
}
|
||||
|
||||
tracing::debug!(id = %sub.id, "subscription ended");
|
||||
}
|
||||
|
||||
pub async fn handle_subscription(
|
||||
server: Arc<RelayServer>,
|
||||
mut req: Request<Incoming>,
|
||||
) -> Result<ServerResponse> {
|
||||
if !is_upgrade_request(&req) {
|
||||
return Response::builder()
|
||||
.status(StatusCode::UPGRADE_REQUIRED)
|
||||
.body(full("Upgrade Required"))?
|
||||
.pipe(Ok);
|
||||
}
|
||||
|
||||
let (res, ws_fut) = upgrade(&mut req)?;
|
||||
tokio::task::spawn(async move {
|
||||
let ws = match ws_fut.await {
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
tracing::warn!("error upgrading WebSocket: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
run_subscription(server, req, ws).await;
|
||||
});
|
||||
|
||||
let (head, _) = res.into_parts();
|
||||
Ok(Response::from_parts(head, empty()))
|
||||
}
|
79
src/server.rs
Normal file
79
src/server.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use crate::{prelude::*, relay_subscription::handle_subscription};
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
|
||||
use hyper::{
|
||||
body::{Bytes, Incoming},
|
||||
service::service_fn,
|
||||
Method, Request, Response, StatusCode,
|
||||
};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
pub struct RelayServer {
|
||||
pub block_tx: tokio::sync::broadcast::Sender<Bytes>,
|
||||
}
|
||||
|
||||
impl Default for RelayServer {
|
||||
fn default() -> Self {
|
||||
let (block_tx, _) = tokio::sync::broadcast::channel::<Bytes>(128);
|
||||
Self { block_tx }
|
||||
}
|
||||
}
|
||||
|
||||
pub type ServerResponseBody = BoxBody<Bytes, hyper::Error>;
|
||||
pub fn empty() -> ServerResponseBody {
|
||||
Empty::<Bytes>::new().map_err(|e| match e {}).boxed()
|
||||
}
|
||||
pub fn full<T: Into<Bytes>>(chunk: T) -> ServerResponseBody {
|
||||
Full::new(chunk.into()).map_err(|e| match e {}).boxed()
|
||||
}
|
||||
|
||||
pub type ServerResponse = Response<BoxBody<Bytes, hyper::Error>>;
|
||||
|
||||
async fn serve(server: Arc<RelayServer>, req: Request<Incoming>) -> Result<ServerResponse> {
|
||||
let path = req.uri().path();
|
||||
|
||||
tracing::debug!("{}", path);
|
||||
|
||||
match (req.method(), path) {
|
||||
(&Method::GET, "/") => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(full("cerulea relay running..."))?
|
||||
.pipe(Ok),
|
||||
|
||||
(&Method::GET, "/xrpc/com.atproto.sync.subscribeRepos") => {
|
||||
handle_subscription(server, req).await
|
||||
}
|
||||
|
||||
_ => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(full("Not Found"))?
|
||||
.pipe(Ok),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn listen(server: Arc<RelayServer>, addr: SocketAddr) -> Result<()> {
|
||||
tracing::info!("Listening on: http://{addr}/ ...");
|
||||
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
|
||||
loop {
|
||||
let (stream, _client_addr) = listener.accept().await?;
|
||||
let io = TokioIo::new(stream);
|
||||
let server = Arc::clone(&server);
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(err) = hyper::server::conn::http1::Builder::new()
|
||||
.serve_connection(io, service_fn(move |req| serve(Arc::clone(&server), req)))
|
||||
.with_upgrades()
|
||||
.await
|
||||
{
|
||||
eprintln!("Error handling connection: {err:?}")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue