feat(F-155): completed feature
This commit is contained in:
@@ -2,6 +2,7 @@ services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: mdv-dev-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Dev-only credentials. Public by design, never reuse them outside local dev.
|
||||
POSTGRES_USER: mdv
|
||||
@@ -20,11 +21,24 @@ services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: mdv-dev-redis
|
||||
restart: unless-stopped
|
||||
command:
|
||||
['redis-server', '--appendonly', 'yes', '--appendfsync', 'everysec', '--save', '60', '1']
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
- mdv_redis_data:/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 2s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
|
||||
volumes:
|
||||
mdv_pg_data:
|
||||
# Keep the existing Docker volume explicit so Compose project-name changes
|
||||
# cannot silently create an empty PostgreSQL volume.
|
||||
name: project_mdv_pg_data
|
||||
mdv_redis_data:
|
||||
# Keep the existing Docker volume explicit and persist Redis AOF/RDB files.
|
||||
name: project_mdv_redis_data
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
@@ -20,7 +20,8 @@
|
||||
"docker:down": "docker compose down",
|
||||
"db:seed": "node --env-file-if-exists=.env scripts/seed.cjs",
|
||||
"db:down": "node --env-file-if-exists=.env node_modules/node-pg-migrate/bin/node-pg-migrate.js down --migrations-dir migrations",
|
||||
"db:status": "node --env-file-if-exists=.env scripts/db-status.mjs"
|
||||
"db:status": "node --env-file-if-exists=.env scripts/db-status.mjs",
|
||||
"infra:persistence-check": "node --env-file-if-exists=.env scripts/check-persistence.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^11.1.2",
|
||||
|
||||
158
project/scripts/check-persistence.mjs
Normal file
158
project/scripts/check-persistence.mjs
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import pg from 'pg';
|
||||
|
||||
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const envPath = resolve(projectDir, '.env');
|
||||
|
||||
if (existsSync(envPath)) {
|
||||
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq === -1) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
const value = trimmed.slice(eq + 1).trim();
|
||||
if (key && process.env[key] === undefined) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) throw new Error('DATABASE_URL not set');
|
||||
|
||||
const recreate = process.argv.includes('--recreate');
|
||||
const token = process.env.PERSISTENCE_CHECK_TOKEN ?? `persistence-${Date.now()}`;
|
||||
const pgKey = 'infra.persistence_check';
|
||||
const redisKey = 'infra:persistence_check';
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return execFileSync(command, args, {
|
||||
cwd: projectDir,
|
||||
stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
|
||||
function dockerCompose(args, options) {
|
||||
return run('docker', ['compose', ...args], options);
|
||||
}
|
||||
|
||||
function dockerExec(container, args, options) {
|
||||
return run('docker', ['exec', container, ...args], options);
|
||||
}
|
||||
|
||||
function waitForInfrastructure() {
|
||||
const deadline = Date.now() + 60_000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
dockerExec('mdv-dev-postgres', ['pg_isready', '-U', 'mdv', '-d', 'mercadodevida'], {
|
||||
capture: true,
|
||||
});
|
||||
dockerExec('mdv-dev-redis', ['redis-cli', 'ping'], { capture: true });
|
||||
return;
|
||||
} catch {
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
|
||||
}
|
||||
}
|
||||
throw new Error('PostgreSQL/Redis did not become ready in 60s');
|
||||
}
|
||||
|
||||
async function writePostgresMarker() {
|
||||
const pool = new pg.Pool({ connectionString: databaseUrl });
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO app_meta (key, value, updated_at)
|
||||
VALUES ($1, $2, now())
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
|
||||
[pgKey, token],
|
||||
);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function readPostgresMarker() {
|
||||
const pool = new pg.Pool({ connectionString: databaseUrl });
|
||||
try {
|
||||
const result = await pool.query('SELECT value FROM app_meta WHERE key = $1', [pgKey]);
|
||||
return result.rows[0]?.value;
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
function writeRedisMarker() {
|
||||
dockerExec('mdv-dev-redis', ['redis-cli', 'SET', redisKey, token]);
|
||||
}
|
||||
|
||||
function readRedisMarker() {
|
||||
return dockerExec('mdv-dev-redis', ['redis-cli', 'GET', redisKey], { capture: true }).trim();
|
||||
}
|
||||
|
||||
function printMounts() {
|
||||
const pgMount = run(
|
||||
'docker',
|
||||
[
|
||||
'inspect',
|
||||
'mdv-dev-postgres',
|
||||
'--format',
|
||||
'{{range .Mounts}}{{.Name}} -> {{.Destination}}{{println}}{{end}}',
|
||||
],
|
||||
{ capture: true },
|
||||
).trim();
|
||||
const redisMount = run(
|
||||
'docker',
|
||||
[
|
||||
'inspect',
|
||||
'mdv-dev-redis',
|
||||
'--format',
|
||||
'{{range .Mounts}}{{.Name}} -> {{.Destination}}{{println}}{{end}}',
|
||||
],
|
||||
{ capture: true },
|
||||
).trim();
|
||||
const redisAppendOnly = dockerExec(
|
||||
'mdv-dev-redis',
|
||||
['redis-cli', 'CONFIG', 'GET', 'appendonly'],
|
||||
{
|
||||
capture: true,
|
||||
},
|
||||
).trim();
|
||||
console.log(`PostgreSQL mount: ${pgMount}`);
|
||||
console.log(`Redis mount: ${redisMount}`);
|
||||
console.log(`Redis appendonly config:\n${redisAppendOnly}`);
|
||||
}
|
||||
|
||||
console.log('[INFO] Starting infrastructure through docker compose...');
|
||||
dockerCompose(['up', '-d', 'postgres', 'redis']);
|
||||
waitForInfrastructure();
|
||||
printMounts();
|
||||
|
||||
console.log(`[INFO] Writing persistence marker: ${token}`);
|
||||
await writePostgresMarker();
|
||||
writeRedisMarker();
|
||||
|
||||
if (recreate) {
|
||||
console.log('[INFO] Recreating PostgreSQL and Redis containers while keeping named volumes...');
|
||||
dockerCompose(['stop', 'postgres', 'redis']);
|
||||
dockerCompose(['rm', '-f', 'postgres', 'redis']);
|
||||
dockerCompose(['up', '-d', 'postgres', 'redis']);
|
||||
} else {
|
||||
console.log('[INFO] Restarting PostgreSQL and Redis containers...');
|
||||
dockerCompose(['restart', 'postgres', 'redis']);
|
||||
}
|
||||
waitForInfrastructure();
|
||||
printMounts();
|
||||
|
||||
const pgValue = await readPostgresMarker();
|
||||
const redisValue = readRedisMarker();
|
||||
if (pgValue !== token) {
|
||||
throw new Error(`PostgreSQL marker mismatch: expected ${token}, got ${pgValue ?? '<missing>'}`);
|
||||
}
|
||||
if (redisValue !== token) {
|
||||
throw new Error(`Redis marker mismatch: expected ${token}, got ${redisValue || '<missing>'}`);
|
||||
}
|
||||
|
||||
console.log('[OK] PostgreSQL and Redis persistence verified');
|
||||
@@ -165,21 +165,21 @@ start_infrastructure() {
|
||||
command -v docker >/dev/null 2>&1 || { echo '[FAIL] Docker is required' >&2; exit 1; }
|
||||
echo '[INFO] Starting PostgreSQL and Redis...'
|
||||
|
||||
local service container
|
||||
for service in postgres redis; do
|
||||
container="mdv-dev-$service"
|
||||
if docker inspect "$container" >/dev/null 2>&1; then
|
||||
docker start "$container" >/dev/null
|
||||
else
|
||||
(cd "$PROJECT_DIR" && docker compose up -d "$service")
|
||||
fi
|
||||
done
|
||||
# Always go through Compose instead of plain `docker start` so changes such
|
||||
# as Redis AOF or explicit volume names are reconciled without losing named
|
||||
# volumes. Compose recreates containers when needed but keeps
|
||||
# project_mdv_pg_data/project_mdv_redis_data attached.
|
||||
(cd "$PROJECT_DIR" && docker compose up -d postgres redis)
|
||||
|
||||
local deadline=$((SECONDS + 60))
|
||||
until docker exec mdv-dev-postgres pg_isready -U mdv -d mercadodevida >/dev/null 2>&1; do
|
||||
(( SECONDS < deadline )) || { echo '[FAIL] PostgreSQL did not become ready' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
until docker exec mdv-dev-redis redis-cli ping >/dev/null 2>&1; do
|
||||
(( SECONDS < deadline )) || { echo '[FAIL] Redis did not become ready' >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
Reference in New Issue
Block a user