feat(M1): Project scaffolding, DB schema, core infrastructure
- Initialize Rust workspace with 7 crates (pm-web, pm-worker, pm-core, pm-agent-client, pm-auth, pm-ca, pm-reports) - React + TypeScript + Vite + MUI frontend scaffold - Full PostgreSQL schema: all 17 tables with indexes and constraints - pm-core: config (TOML+env), db (SQLx pool + migrations), error (unified AppError + JSON envelope), request_id (ULID middleware), logging (tracing JSON/pretty) - pm-web: Axum skeleton, /status/health endpoint, static file serving - pm-worker: Tokio skeleton, heartbeat writer, schema version check - Embedded sqlx migrations with advisory lock (single-writer) - systemd unit files, setup.sh, build-frontend.sh - config.example.toml with all configuration keys - docs/runbooks/restore.md - cargo check passes with zero warnings Closes M1.
This commit is contained in:
69
crates/pm-core/src/db.rs
Normal file
69
crates/pm-core/src/db.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use sqlx::postgres::{PgPool, PgPoolOptions};
|
||||
use std::time::Duration;
|
||||
use crate::config::DatabaseConfig;
|
||||
|
||||
/// Initialize and return a PostgreSQL connection pool.
|
||||
pub async fn init_pool(cfg: &DatabaseConfig) -> Result<PgPool, sqlx::Error> {
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(cfg.max_connections)
|
||||
.min_connections(cfg.min_connections)
|
||||
.acquire_timeout(Duration::from_secs(cfg.acquire_timeout_secs))
|
||||
.connect(&cfg.url)
|
||||
.await?;
|
||||
|
||||
tracing::info!(
|
||||
max_connections = cfg.max_connections,
|
||||
"PostgreSQL connection pool initialized"
|
||||
);
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
/// Run embedded SQLx migrations.
|
||||
/// Uses a PostgreSQL advisory lock to ensure only one writer runs migrations.
|
||||
pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> {
|
||||
tracing::info!("Acquiring advisory lock for migrations");
|
||||
|
||||
// Advisory lock key — consistent hash of the application name
|
||||
const LOCK_KEY: i64 = 0x7061_7463_686d_6772; // "patchmgr" bytes
|
||||
|
||||
// Acquire advisory lock; blocks until granted
|
||||
sqlx::query("SELECT pg_advisory_lock($1)")
|
||||
.bind(LOCK_KEY)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "Failed to acquire advisory lock");
|
||||
e
|
||||
})
|
||||
.expect("Advisory lock must be acquired before running migrations");
|
||||
|
||||
tracing::info!("Running database migrations");
|
||||
let result = sqlx::migrate!("../../migrations").run(pool).await;
|
||||
|
||||
// Always release the lock
|
||||
sqlx::query("SELECT pg_advisory_unlock($1)")
|
||||
.bind(LOCK_KEY)
|
||||
.execute(pool)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
match &result {
|
||||
Ok(_) => tracing::info!("Database migrations completed successfully"),
|
||||
Err(e) => tracing::error!(error = %e, "Database migrations failed"),
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Check that the database schema is at the expected version.
|
||||
/// Used by the worker to wait until migrations have been applied.
|
||||
pub async fn check_schema_version(pool: &PgPool) -> Result<i64, sqlx::Error> {
|
||||
let row: (i64,) = sqlx::query_as(
|
||||
"SELECT COUNT(*) FROM _sqlx_migrations WHERE success = true",
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.0)
|
||||
}
|
||||
Reference in New Issue
Block a user