mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 04:26:43 -06:00
refactor: replace shell entrypoint with TypeScript, add DB retry logic
- Migrate Docker entrypoint from inline shell script to TypeScript for better maintainability - Add exponential backoff DB connection retry (prod only, dev fails fast) - Increase healthcheck start-period to account for DB warmup
This commit is contained in:
+4
-44
@@ -89,48 +89,8 @@ COPY web/package.json web/bun.lock ./web/
|
|||||||
RUN cd web && bun install --frozen-lockfile --production && \
|
RUN cd web && bun install --frozen-lockfile --production && \
|
||||||
ln -s /app/web/node_modules /app/web/build/node_modules
|
ln -s /app/web/node_modules /app/web/build/node_modules
|
||||||
|
|
||||||
# Create inline entrypoint script
|
# Copy entrypoint script
|
||||||
RUN cat > /entrypoint.sh << 'EOF'
|
COPY web/entrypoint.ts ./web/
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
kill "$BUN_PID" "$RUST_PID" 2>/dev/null || true
|
|
||||||
rm -f /tmp/api.sock /tmp/bun.sock
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
trap cleanup SIGTERM SIGINT
|
|
||||||
|
|
||||||
# Start Bun SSR (propagate LOG_JSON and set UPSTREAM_URL)
|
|
||||||
cd /app/web/build
|
|
||||||
SOCKET_PATH=/tmp/bun.sock LOG_JSON="${LOG_JSON}" UPSTREAM_URL=/tmp/api.sock bun --preload /app/web/console-logger.js index.js &
|
|
||||||
BUN_PID=$!
|
|
||||||
|
|
||||||
# Wait for Bun socket
|
|
||||||
timeout=50
|
|
||||||
while [ ! -S /tmp/bun.sock ] && [ $timeout -gt 0 ]; do
|
|
||||||
sleep 0.1
|
|
||||||
timeout=$((timeout - 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ! -S /tmp/bun.sock ]; then
|
|
||||||
echo "ERROR: Bun failed to create socket within 5s"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start Rust server
|
|
||||||
# Note: [::] binds to both IPv4 and IPv6 on Linux
|
|
||||||
/app/api \
|
|
||||||
--listen "[::]:${PORT:-8080}" \
|
|
||||||
--listen /tmp/api.sock \
|
|
||||||
--downstream /tmp/bun.sock &
|
|
||||||
RUST_PID=$!
|
|
||||||
|
|
||||||
# Wait for either process to exit
|
|
||||||
wait -n "$BUN_PID" "$RUST_PID" 2>/dev/null || wait "$BUN_PID" "$RUST_PID"
|
|
||||||
cleanup
|
|
||||||
EOF
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
|
|
||||||
# Environment configuration
|
# Environment configuration
|
||||||
# RUST_LOG - optional, overrides LOG_LEVEL with full tracing filter syntax
|
# RUST_LOG - optional, overrides LOG_LEVEL with full tracing filter syntax
|
||||||
@@ -142,7 +102,7 @@ ENV PORT=8080 \
|
|||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
CMD wget -q --spider http://localhost:${PORT}/api/health || exit 1
|
CMD wget -q --spider http://localhost:${PORT}/api/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["bun", "run", "/app/web/entrypoint.ts"]
|
||||||
|
|||||||
+41
-5
@@ -8,14 +8,50 @@ pub use settings::*;
|
|||||||
pub use tags::*;
|
pub use tags::*;
|
||||||
|
|
||||||
use sqlx::{PgPool, postgres::PgPoolOptions, query};
|
use sqlx::{PgPool, postgres::PgPoolOptions, query};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
/// Database connection pool creation
|
/// Database connection pool creation with retry logic
|
||||||
|
///
|
||||||
|
/// Production: Exponential backoff (1s -> 2s -> 4s... -> 30s cap), max 10 attempts
|
||||||
|
/// Development: Fail fast (1 attempt)
|
||||||
pub async fn create_pool(database_url: &str) -> Result<PgPool, sqlx::Error> {
|
pub async fn create_pool(database_url: &str) -> Result<PgPool, sqlx::Error> {
|
||||||
PgPoolOptions::new()
|
let max_attempts: u32 = if cfg!(debug_assertions) { 1 } else { 10 };
|
||||||
|
let initial_delay = Duration::from_secs(1);
|
||||||
|
let max_delay = Duration::from_secs(30);
|
||||||
|
|
||||||
|
let pool_options = PgPoolOptions::new()
|
||||||
.max_connections(20)
|
.max_connections(20)
|
||||||
.acquire_timeout(std::time::Duration::from_secs(3))
|
.acquire_timeout(Duration::from_secs(3));
|
||||||
.connect(database_url)
|
|
||||||
.await
|
let mut last_error = None;
|
||||||
|
let mut delay = initial_delay;
|
||||||
|
|
||||||
|
for attempt in 1..=max_attempts {
|
||||||
|
match pool_options.clone().connect(database_url).await {
|
||||||
|
Ok(pool) => {
|
||||||
|
if attempt > 1 {
|
||||||
|
tracing::info!(attempt, "Database connection established after retry");
|
||||||
|
}
|
||||||
|
return Ok(pool);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
last_error = Some(e);
|
||||||
|
if attempt < max_attempts {
|
||||||
|
tracing::warn!(
|
||||||
|
attempt,
|
||||||
|
max_attempts,
|
||||||
|
delay_secs = delay.as_secs(),
|
||||||
|
"Database connection failed, retrying..."
|
||||||
|
);
|
||||||
|
sleep(delay).await;
|
||||||
|
delay = (delay * 2).min(max_delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(last_error.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Health check query
|
/// Health check query
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { spawn, type Subprocess } from "bun";
|
||||||
|
import { unlinkSync, existsSync } from "fs";
|
||||||
|
|
||||||
|
const BUN_SOCKET = "/tmp/bun.sock";
|
||||||
|
const API_SOCKET = "/tmp/api.sock";
|
||||||
|
const PORT = process.env.PORT || "8080";
|
||||||
|
const LOG_JSON = process.env.LOG_JSON || "true";
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
try {
|
||||||
|
unlinkSync(BUN_SOCKET);
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
unlinkSync(API_SOCKET);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on signals
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
cleanup();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
cleanup();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start Bun SSR
|
||||||
|
console.log("Starting Bun SSR...");
|
||||||
|
const bunProc = spawn({
|
||||||
|
cmd: ["bun", "--preload", "/app/web/console-logger.js", "index.js"],
|
||||||
|
cwd: "/app/web/build",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
SOCKET_PATH: BUN_SOCKET,
|
||||||
|
LOG_JSON,
|
||||||
|
UPSTREAM_URL: API_SOCKET,
|
||||||
|
},
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for Bun socket (5s timeout)
|
||||||
|
const startTime = Date.now();
|
||||||
|
while (!existsSync(BUN_SOCKET)) {
|
||||||
|
if (Date.now() - startTime > 5000) {
|
||||||
|
console.error("ERROR: Bun failed to create socket within 5s");
|
||||||
|
bunProc.kill();
|
||||||
|
cleanup();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await Bun.sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Rust server
|
||||||
|
console.log("Starting Rust API...");
|
||||||
|
const rustProc = spawn({
|
||||||
|
cmd: [
|
||||||
|
"/app/api",
|
||||||
|
"--listen",
|
||||||
|
`[::]:${PORT}`,
|
||||||
|
"--listen",
|
||||||
|
API_SOCKET,
|
||||||
|
"--downstream",
|
||||||
|
BUN_SOCKET,
|
||||||
|
],
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor both processes - exit if either dies
|
||||||
|
async function monitor(name: string, proc: Subprocess) {
|
||||||
|
const exitCode = await proc.exited;
|
||||||
|
console.error(`${name} exited with code ${exitCode}`);
|
||||||
|
return { name, exitCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Promise.race([
|
||||||
|
monitor("Bun", bunProc),
|
||||||
|
monitor("Rust", rustProc),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Kill the other process
|
||||||
|
console.error(`${result.name} died, shutting down...`);
|
||||||
|
bunProc.kill();
|
||||||
|
rustProc.kill();
|
||||||
|
cleanup();
|
||||||
|
process.exit(result.exitCode || 1);
|
||||||
Reference in New Issue
Block a user