now using postgres
This commit is contained in:
39
flake.nix
39
flake.nix
@@ -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
1
server/Cargo.lock
generated
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user