swap backend to rust
This commit is contained in:
@ -1,2 +1,3 @@
|
||||
.build/
|
||||
target/
|
||||
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
|
||||
|
||||
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
|
||||
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:
|
||||
@mkdir -p .build/tests
|
||||
start: build
|
||||
./target/debug/nuchat 2>&1 > target/debug/logs/nuchat.log &
|
||||
|
||||
build: dirs
|
||||
go build -o .build/nuchat fergus.molloy.xyz/nuchat
|
||||
build:
|
||||
@mkdir -p target/debug/logs
|
||||
cargo build
|
||||
|
||||
run: build
|
||||
./.build/nuchat
|
||||
|
||||
test: unit integration coverage
|
||||
test: integration unit
|
||||
|
||||
integration:
|
||||
{{GO_TEST}} ./tests/... | tee .build/tests/integration.log
|
||||
if [ ! $(curl -sf "localhost:7000/healthcheck" ) ]; then just run; fi
|
||||
cargo test --test '*'
|
||||
|
||||
unit:
|
||||
{{GO_TEST}} -race $(go list ./... | grep -v "/tests") | tee .build/tests/unit.log
|
||||
|
||||
coverage:
|
||||
{{GO_TEST}} $(go list ./... | grep -v "/tests") -coverprofile .build/tests/coverprofile.txt | tee .build/tests/coverage.log
|
||||
cargo test --lib --bins
|
||||
|
||||
@ -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:
|
||||
build: ./ui
|
||||
ports:
|
||||
- "5173:5173"
|
||||
- "3000:3000"
|
||||
backend:
|
||||
build: ./backend
|
||||
#ports:
|
||||
#- "7000:7000"
|
||||
ports:
|
||||
- "7000:7000"
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user