now using postgres

This commit is contained in:
2026-03-26 23:25:57 +01:00
parent d973d362f8
commit 5b3f8b89d5
4 changed files with 131 additions and 111 deletions

View File

@@ -29,7 +29,7 @@
cargoLock.lockFile = ./server/Cargo.lock; cargoLock.lockFile = ./server/Cargo.lock;
nativeBuildInputs = [ pkgs.pkg-config ]; nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.sqlite ]; # buildInputs = [ pkgs.sqlite ];
# doCheck = false; # doCheck = false;
}; };
@@ -41,6 +41,7 @@
config, config,
lib, lib,
pkgs, pkgs,
inputs,
... ...
}: }:
let let
@@ -53,29 +54,53 @@
type = lib.types.port; type = lib.types.port;
default = 9003; default = 9003;
}; };
databasePath = lib.mkOption { databaseUrl = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "/var/lib/slimes-server/slimes.db"; default = "postgres://slimes:secret@127.0.0.1:9005/slimes";
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
systemd.services.slimes-server = { systemd.services.slimes-server = {
description = "Slimes Benchmark Server"; description = "Slimes Benchmark Server";
after = [ "network.target" ]; after = [
"network.target"
"slimes-server-db-container.service"
];
requires = [ "slimes-server-db-container.service" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
serviceConfig = { serviceConfig = {
ExecStart = "${ ExecStart = "${
self.packages.${pkgs.system}.default self.packages.${pkgs.system}.default
}/bin/slimes-server --database-url ${cfg.databasePath} --port ${toString cfg.port}"; }/bin/slimes-server --database-url ${cfg.databaseUrl} --port ${toString cfg.port}";
Restart = "on-failure"; Restart = "on-failure";
StateDirectory = "slimes-server"; # StateDirectory = "slimes-server";
DynamicUser = true; # DynamicUser = true;
ProtectSystem = "strict"; ProtectSystem = "strict";
ProtectHome = true; ProtectHome = true;
NoNewPrivileges = true; NoNewPrivileges = true;
}; };
}; };
systemd.services."slimes-server-db-container" = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
StateDirectory = "slimes-server-db";
ExecStart = ''
${pkgs.podman}/bin/podman run \
--name slimes-server-db \
--replace \
-p 127.0.0.1:9005:5432 \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_USER=slimes \
-e POSTGRES_DB=slimes \
-v /var/lib/slimes-server-db:/var/lib/postgresql/data \
docker.io/library/postgres:15
'';
ExecStop = "${pkgs.podman}/bin/podman stop slimes-server-db";
Restart = "always";
};
};
}; };
}; };
}; };

1
server/Cargo.lock generated
View File

@@ -1042,7 +1042,6 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [ dependencies = [
"cc",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]

View File

@@ -11,7 +11,7 @@ clap = { version = "4.6.0", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
# slimes = { version = "1.0.0", path = ".." } # slimes = { version = "1.0.0", path = ".." }
sqlx = { version = "0.8.6", features = ["chrono", "sqlite", "runtime-tokio"] } sqlx = { version = "0.8.6", features = ["chrono", "runtime-tokio", "postgres"] }
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros"] }
tower-http = { version = "0.6.8", features = ["fs", "cors"] } tower-http = { version = "0.6.8", features = ["fs", "cors"] }
tower_governor = "0.8.0" tower_governor = "0.8.0"

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use std::{net::SocketAddr, sync::Arc, time::Duration};
use axum::{ use axum::{
Json, Router, Json, Router,
@@ -8,15 +8,14 @@ use axum::{
}; };
use clap::Parser; use clap::Parser;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; use sqlx::{PgPool, postgres::PgPoolOptions};
use tower_governor::{GovernorLayer, governor::GovernorConfigBuilder}; use tower_governor::{GovernorLayer, governor::GovernorConfigBuilder};
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
struct Args { struct Args {
/// Path to the SQLite database file #[arg(short, long)]
#[arg(short, long, default_value = "slimes.db")]
database_url: String, database_url: String,
/// Port to listen on /// Port to listen on
@@ -43,10 +42,10 @@ pub struct BenchmarkReport {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct FullReport { pub struct FullReport {
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
pub id: Option<i64>, pub id: Option<i32>,
pub mac_address: String, pub mac_address: String,
pub timestamp: String, pub timestamp: String,
pub slimes: Option<HashMap<String, Vec<String>>>, pub slimes: Option<serde_json::Value>,
pub benchmark: Option<BenchmarkReport>, pub benchmark: Option<BenchmarkReport>,
pub client_version: String, pub client_version: String,
pub signature: String, pub signature: String,
@@ -54,30 +53,31 @@ pub struct FullReport {
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Pagination { pub struct Pagination {
pub limit: Option<u32>, pub limit: Option<i64>,
pub offset: Option<u32>, pub offset: Option<i64>,
} }
pub struct AppState { pub struct AppState {
db: SqlitePool, db: PgPool,
} }
async fn submit( async fn submit(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(payload): Json<FullReport>, Json(payload): Json<FullReport>,
) -> Result<(StatusCode, Json<i64>), (StatusCode, String)> { ) -> Result<(StatusCode, Json<i32>), (StatusCode, String)> {
let score = payload let score = payload
.benchmark .benchmark
.as_ref() .as_ref()
.map(|b| b.multi_thread.score) .map(|b| b.multi_thread.score)
.unwrap_or(0); .unwrap_or(0);
let raw_json = serde_json::to_string(&payload) let raw_json = serde_json::to_value(&payload)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let result = sqlx::query( let row: (i32,) = sqlx::query_as(
"INSERT INTO reports (mac_address, score, timestamp, client_version, signature, data) "INSERT INTO reports (mac_address, score, timestamp, client_version, signature, data)
VALUES (?, ?, ?, ?, ?, ?)", VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id",
) )
.bind(&payload.mac_address) .bind(&payload.mac_address)
.bind(score as i64) .bind(score as i64)
@@ -85,16 +85,15 @@ async fn submit(
.bind(&payload.client_version) .bind(&payload.client_version)
.bind(&payload.signature) .bind(&payload.signature)
.bind(raw_json) .bind(raw_json)
.execute(&state.db) .fetch_one(&state.db)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let id = result.last_insert_rowid(); Ok((StatusCode::CREATED, Json(row.0)))
Ok((StatusCode::CREATED, Json(id)))
} }
fn parse_report_row(id: i64, data: String) -> Option<FullReport> { fn parse_report_row(id: i32, data: serde_json::Value) -> Option<FullReport> {
let mut report: FullReport = serde_json::from_str(&data).ok()?; let mut report: FullReport = serde_json::from_value(data).ok()?;
report.id = Some(id); report.id = Some(id);
Some(report) Some(report)
} }
@@ -106,13 +105,15 @@ async fn get_leaderboard(
let limit = pagination.limit.unwrap_or(10); let limit = pagination.limit.unwrap_or(10);
let offset = pagination.offset.unwrap_or(0); let offset = pagination.offset.unwrap_or(0);
let rows: Vec<(i64, String)> = sqlx::query_as( let rows: Vec<(i32, serde_json::Value)> = sqlx::query_as(
r#" r#"
SELECT id, data FROM reports r SELECT id, data FROM (
WHERE score = (SELECT MAX(score) FROM reports WHERE mac_address = r.mac_address) SELECT DISTINCT ON (mac_address) id, data, score
GROUP BY mac_address FROM reports
ORDER BY mac_address, score DESC
) sub
ORDER BY score DESC ORDER BY score DESC
LIMIT ? OFFSET ? LIMIT $1 OFFSET $2
"#, "#,
) )
.bind(limit) .bind(limit)
@@ -131,9 +132,10 @@ async fn get_leaderboard(
async fn get_report_by_id( async fn get_report_by_id(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Path(id): Path<i64>, Path(id): Path<i32>,
) -> Result<Json<FullReport>, (StatusCode, String)> { ) -> Result<Json<FullReport>, (StatusCode, String)> {
let row: (i64, String) = sqlx::query_as("SELECT id, data FROM reports WHERE id = ?") let row: (i32, serde_json::Value) =
sqlx::query_as("SELECT id, data FROM reports WHERE id = $1")
.bind(id) .bind(id)
.fetch_optional(&state.db) .fetch_optional(&state.db)
.await .await
@@ -162,26 +164,20 @@ async fn main() -> anyhow::Result<()> {
let subscriber = tracing_subscriber::FmtSubscriber::new(); let subscriber = tracing_subscriber::FmtSubscriber::new();
tracing::subscriber::set_global_default(subscriber).unwrap(); tracing::subscriber::set_global_default(subscriber).unwrap();
let path = &args.database_url; let pool = PgPoolOptions::new()
if !path.is_empty() && !std::path::Path::new(path).exists() {
tracing::info!("Creating database file at {}", path);
std::fs::File::create(path)?;
}
let pool = SqlitePoolOptions::new()
.max_connections(5) .max_connections(5)
.connect(format!("sqlite:{}", &args.database_url).as_str()) .connect(&args.database_url)
.await?; .await?;
sqlx::query( sqlx::query(
"CREATE TABLE IF NOT EXISTS reports ( "CREATE TABLE IF NOT EXISTS reports (
id INTEGER PRIMARY KEY AUTOINCREMENT, id SERIAL PRIMARY KEY,
mac_address TEXT NOT NULL, mac_address TEXT NOT NULL,
score INTEGER NOT NULL, score BIGINT NOT NULL,
timestamp TEXT NOT NULL, timestamp TEXT NOT NULL,
client_version TEXT NOT NULL, client_version TEXT NOT NULL,
signature TEXT, signature TEXT,
data TEXT NOT NULL data JSONB NOT NULL
);", );",
) )
.execute(&pool) .execute(&pool)