swap backend to rust
This commit is contained in:
@ -1,2 +1,3 @@
|
|||||||
.build/
|
target/
|
||||||
tests/
|
tests/
|
||||||
|
Justfile
|
||||||
|
|||||||
2
backend/.gitignore
vendored
2
backend/.gitignore
vendored
@ -1 +1 @@
|
|||||||
.build/
|
target/
|
||||||
|
|||||||
1979
backend/Cargo.lock
generated
Normal file
1979
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
backend/Cargo.toml
Normal file
27
backend/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "nuchat"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.8.4"
|
||||||
|
clap = { version = "4.5.41", features = ["derive"] }
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
tokio = { version = "1.46.1", features = ["full"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
reqwest = { version = "0.12.22", features = ["json"] }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
path = "src/main.rs"
|
||||||
|
name = "nuchat"
|
||||||
|
|
||||||
|
# [lints.clippy]
|
||||||
|
# missing_errors_doc
|
||||||
|
|
||||||
@ -1,13 +1,27 @@
|
|||||||
FROM golang:1.24-alpine AS build
|
FROM rust:1.87 AS base
|
||||||
|
RUN cargo install --locked cargo-chef sccache
|
||||||
|
ENV RUSTC_WRAPPER=sccache SCCACHE_DIR=/sccache
|
||||||
|
|
||||||
|
FROM base AS planner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go build -o nuchat fergus.molloy.xyz/nuchat
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
|
|
||||||
FROM scratch
|
FROM base AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY --from=planner /app/recipe.json recipe.json
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/git \
|
||||||
|
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
|
||||||
|
cargo chef cook --release --recipe-path recipe.json
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/git \
|
||||||
|
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
|
||||||
|
cargo build --release --bin nuchat --target-dir=/app/target
|
||||||
|
|
||||||
COPY --from=build /app/nuchat .
|
FROM gcr.io/distroless/cc AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/target/release/nuchat ./nuchat
|
||||||
|
|
||||||
ENTRYPOINT ["/app/nuchat"]
|
ENTRYPOINT [ "./nuchat", "--host 0.0.0.0" ]
|
||||||
|
|||||||
@ -1,21 +1,18 @@
|
|||||||
GO_TEST := "gotestsum --format=testname --"
|
run *ARGS: build
|
||||||
|
./target/debug/nuchat {{ARGS}}
|
||||||
|
|
||||||
dirs:
|
start: build
|
||||||
@mkdir -p .build/tests
|
./target/debug/nuchat 2>&1 > target/debug/logs/nuchat.log &
|
||||||
|
|
||||||
build: dirs
|
build:
|
||||||
go build -o .build/nuchat fergus.molloy.xyz/nuchat
|
@mkdir -p target/debug/logs
|
||||||
|
cargo build
|
||||||
|
|
||||||
run: build
|
test: integration unit
|
||||||
./.build/nuchat
|
|
||||||
|
|
||||||
test: unit integration coverage
|
|
||||||
|
|
||||||
integration:
|
integration:
|
||||||
{{GO_TEST}} ./tests/... | tee .build/tests/integration.log
|
if [ ! $(curl -sf "localhost:7000/healthcheck" ) ]; then just run; fi
|
||||||
|
cargo test --test '*'
|
||||||
|
|
||||||
unit:
|
unit:
|
||||||
{{GO_TEST}} -race $(go list ./... | grep -v "/tests") | tee .build/tests/unit.log
|
cargo test --lib --bins
|
||||||
|
|
||||||
coverage:
|
|
||||||
{{GO_TEST}} $(go list ./... | grep -v "/tests") -coverprofile .build/tests/coverprofile.txt | tee .build/tests/coverage.log
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
module fergus.molloy.xyz/nuchat
|
|
||||||
|
|
||||||
go 1.24.5
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("hello world 3")
|
|
||||||
}
|
|
||||||
63
backend/src/lib.rs
Normal file
63
backend/src/lib.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
mod routes;
|
||||||
|
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use axum::{Router, serve::WithGracefulShutdown};
|
||||||
|
use tokio::signal;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
pub fn run(
|
||||||
|
listener: tokio::net::TcpListener,
|
||||||
|
) -> Result<
|
||||||
|
WithGracefulShutdown<
|
||||||
|
tokio::net::TcpListener,
|
||||||
|
Router,
|
||||||
|
Router,
|
||||||
|
impl std::future::Future<Output = ()>,
|
||||||
|
>,
|
||||||
|
std::io::Error,
|
||||||
|
> {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()),
|
||||||
|
)
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
// build our application with some routes
|
||||||
|
let (app, rx) = routes::app();
|
||||||
|
|
||||||
|
// run it
|
||||||
|
tracing::debug!("listening on {}", listener.local_addr()?);
|
||||||
|
let server = axum::serve(listener, app);
|
||||||
|
Ok(server.with_graceful_shutdown(shutdown_signal(rx)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown_signal(rx: mpsc::Receiver<bool>) {
|
||||||
|
let ctrl_c = async {
|
||||||
|
signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("failed to install Ctrl+C handler");
|
||||||
|
};
|
||||||
|
let endpoint = tokio::spawn(async move {
|
||||||
|
let _ = rx.recv();
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
let terminate = async {
|
||||||
|
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||||
|
.expect("failed to install signal handler")
|
||||||
|
.recv()
|
||||||
|
.await;
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let terminate = std::future::pending::<()>();
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = ctrl_c => {},
|
||||||
|
_ = terminate => {},
|
||||||
|
_ = endpoint => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
30
backend/src/main.rs
Normal file
30
backend/src/main.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use clap::{Parser, command};
|
||||||
|
use nuchat::run;
|
||||||
|
use tracing::{Level, event};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Host to bind to
|
||||||
|
#[arg(long, default_value = "127.0.0.1")]
|
||||||
|
host: String,
|
||||||
|
|
||||||
|
/// Port to use
|
||||||
|
#[arg(long, default_value_t = 7000)]
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), std::io::Error> {
|
||||||
|
let args = Args::parse();
|
||||||
|
let str_address = format!("{}:{}", args.host, args.port);
|
||||||
|
let address: SocketAddr = str_address
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|_| panic!("could not parse address: {str_address}"));
|
||||||
|
let listener = tokio::net::TcpListener::bind(address).await?;
|
||||||
|
let result = run(listener)?.await;
|
||||||
|
event!(Level::INFO, "Server stopped");
|
||||||
|
result
|
||||||
|
}
|
||||||
6
backend/src/routes/healthcheck.rs
Normal file
6
backend/src/routes/healthcheck.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use axum::Json;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
pub async fn healthcheck() -> Json<Value> {
|
||||||
|
Json(json!({"healthy": true}))
|
||||||
|
}
|
||||||
19
backend/src/routes/mod.rs
Normal file
19
backend/src/routes/mod.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use axum::Router;
|
||||||
|
use axum::routing::{get, post};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
mod healthcheck;
|
||||||
|
mod shutdown;
|
||||||
|
|
||||||
|
use healthcheck::healthcheck;
|
||||||
|
use shutdown::shutdown;
|
||||||
|
|
||||||
|
pub fn app() -> (Router, mpsc::Receiver<bool>) {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
(
|
||||||
|
Router::new()
|
||||||
|
.route("/healthcheck", get(healthcheck))
|
||||||
|
.route("/shutdown", post(move || shutdown(tx.clone()))),
|
||||||
|
rx,
|
||||||
|
)
|
||||||
|
}
|
||||||
8
backend/src/routes/shutdown.rs
Normal file
8
backend/src/routes/shutdown.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use std::sync::mpsc;
|
||||||
|
use tracing::{Level, event};
|
||||||
|
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
pub async fn shutdown(tx: mpsc::Sender<bool>) {
|
||||||
|
event!(Level::INFO, "Shutdown request received, stopping server...");
|
||||||
|
tx.send(true).expect("failed to send shutdown signal");
|
||||||
|
}
|
||||||
21
backend/tests/healthcheck_test.rs
Normal file
21
backend/tests/healthcheck_test.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_healthcheck() -> reqwest::Result<()> {
|
||||||
|
let response = reqwest::get("http://localhost:7000/healthcheck").await?;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
assert_eq!(response.text().await?, r#"{"healthy":true}"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_healthcheck2() -> reqwest::Result<()> {
|
||||||
|
let response = reqwest::get("http://localhost:7000/healthcheck").await?;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
assert_eq!(response.text().await?, r#"{"healthy":true}"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package tests
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestHello(t *testing.T) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -2,13 +2,14 @@ services:
|
|||||||
ui:
|
ui:
|
||||||
build: ./ui
|
build: ./ui
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "3000:3000"
|
||||||
backend:
|
backend:
|
||||||
build: ./backend
|
build: ./backend
|
||||||
#ports:
|
ports:
|
||||||
#- "7000:7000"
|
- "7000:7000"
|
||||||
db:
|
db:
|
||||||
image: postgres:17-alpine
|
image: postgres:17-alpine
|
||||||
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
Reference in New Issue
Block a user