Skip to content

fix(server): graceful shutdown on SIGTERM/SIGINT with reset-connected and timeout (#26) - #131

Open
angelTomo9 wants to merge 6 commits into
Bitcoindefi:mainfrom
angelTomo9:fix-server-graceful-shutdown-1787657855842
Open

fix(server): graceful shutdown on SIGTERM/SIGINT with reset-connected and timeout (#26)#131
angelTomo9 wants to merge 6 commits into
Bitcoindefi:mainfrom
angelTomo9:fix-server-graceful-shutdown-1787657855842

Conversation

@angelTomo9

@angelTomo9 angelTomo9 commented Aug 25, 2026

Copy link
Copy Markdown

Closes #26

Summary of Changes

Implements graceful server shutdown on SIGTERM and SIGINT to cleanly unmark connected characters in the database and notify connected players before termination.

Features & Robustness

  • SIGTERM & SIGINT Signal Handlers: Registers listeners calling gracefulShutdown(signal) with re-entrancy protection.
  • Player Notification: Cleanly closes all active client WebSockets with code 1000 and reason "Servidor reiniciando. Por favor vuelve a conectar en unos momentos.".
  • Database Reset: Dispatches POST /internal/characters/reset-connected to reset character connection state in PostgreSQL.
  • Hard Timeout Fallback: Implements a 5-second unrefed timer to guarantee the process never hangs if API networking is degraded during container destruction.
  • Safety Net Retained: Startup reset (resetConnectedCharactersOnStartup) remains active for abrupt crashes.

Summary by Gitar

  • World Builder & Map Editing:
    • Added map edit permissions and protected map restrictions for major cities in api/src/repositories/worldBuilder.ts
    • Implemented dynamic palette overrides and graphic metadata endpoints in api/src/server.ts

This will update automatically on new commits.

Comment thread server/src/server.ts Outdated
Comment on lines +619 to +633
let paletteId = entry.paletteId;

if (!paletteId) {
// Asignar siguiente ID de paleta para el mapa (por encima de las paletas estándar base)
const nextIdResult = await pool.query<{ next_id: number }>(
`SELECT COALESCE(MAX(palette_id), 1000) + 1 AS next_id
FROM game_map_palette_overrides
WHERE map_num = $1`,
[mapNum],
);
paletteId = Number(nextIdResult.rows[0]?.next_id ?? 1001);
}

const graphicsArray = entry.graphics.map((g) => (g == null ? 0 : g));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Palette next_id allocation is racy under concurrency

When paletteId is omitted, the next id is computed via SELECT COALESCE(MAX(palette_id),1000)+1 and then inserted in a separate statement. Two concurrent upsertPaletteEntry calls for the same map can compute the same next_id; the ON CONFLICT (map_num, palette_id) DO UPDATE then makes the second silently overwrite the first entry instead of creating a new one. Since map editing is low-frequency admin-only, impact is limited, but consider using a sequence or a single INSERT ... SELECT with a computed id to make allocation atomic.

Was this helpful? React with 👍 / 👎

Comment thread server/src/server.ts
Comment on lines +782 to +787
const accountKey =
(user as any).idAccount ||
(user as any).account_id ||
(client as any).accountId ||
socket.getIp(client) ||
idUser;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: Account-key fallback references non-existent fields

The accountKey chain checks (user as any).account_id and (client as any).accountId, but only user.idAccount actually exists on the runtime types (RuntimeCharacter.idAccount). The two extra as any conditions are dead code that will never contribute and mask type checking. Since idAccount is optional and undefined for some sessions, those clients silently fall back to socket.getIp(client), re-introducing the CGNAT grouping the PR aims to avoid — worth confirming idAccount is reliably populated. Simplify to user.idAccount ?? socket.getIp(client) ?? idUser.

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 1 resolved / 3 findings

Implements graceful server shutdown on SIGTERM and SIGINT with websocket notifications and database connection resets. Consider addressing the racy palette next_id allocation and the non-existent accountKey field references.

💡 Edge Case: Palette next_id allocation is racy under concurrency

📄 api/src/repositories/worldBuilder.ts:619-633

When paletteId is omitted, the next id is computed via SELECT COALESCE(MAX(palette_id),1000)+1 and then inserted in a separate statement. Two concurrent upsertPaletteEntry calls for the same map can compute the same next_id; the ON CONFLICT (map_num, palette_id) DO UPDATE then makes the second silently overwrite the first entry instead of creating a new one. Since map editing is low-frequency admin-only, impact is limited, but consider using a sequence or a single INSERT ... SELECT with a computed id to make allocation atomic.

💡 Quality: Account-key fallback references non-existent fields

📄 server/src/server.ts:782-787

The accountKey chain checks (user as any).account_id and (client as any).accountId, but only user.idAccount actually exists on the runtime types (RuntimeCharacter.idAccount). The two extra as any conditions are dead code that will never contribute and mask type checking. Since idAccount is optional and undefined for some sessions, those clients silently fall back to socket.getIp(client), re-introducing the CGNAT grouping the PR aims to avoid — worth confirming idAccount is reliably populated. Simplify to user.idAccount ?? socket.getIp(client) ?? idUser.

✅ 1 resolved
Edge Case: Race timeout timer not cleared after fetch wins

📄 server/src/server.ts:998-1000 📄 server/src/server.ts:1010
The timeoutPromise schedules a 3.5s setTimeout that is never cleared when fetchPromise wins the Promise.race. The timer is not unref()'d, so it keeps the event loop alive for up to 3.5s after the API call succeeds. It is benign here only because process.exit(0) follows immediately, but it is a latent leak if the code is ever reused. Capture the timer id and clearTimeout it in a finally, or call .unref() on it like the outer forceExitTimeout.

🤖 Prompt for agents
Code Review: Implements graceful server shutdown on SIGTERM and SIGINT with websocket notifications and database connection resets. Consider addressing the racy palette next_id allocation and the non-existent accountKey field references.

1. 💡 Edge Case: Palette next_id allocation is racy under concurrency
   Files: api/src/repositories/worldBuilder.ts:619-633

   When `paletteId` is omitted, the next id is computed via `SELECT COALESCE(MAX(palette_id),1000)+1` and then inserted in a separate statement. Two concurrent `upsertPaletteEntry` calls for the same map can compute the same `next_id`; the `ON CONFLICT (map_num, palette_id) DO UPDATE` then makes the second silently overwrite the first entry instead of creating a new one. Since map editing is low-frequency admin-only, impact is limited, but consider using a sequence or a single INSERT ... SELECT with a computed id to make allocation atomic.

2. 💡 Quality: Account-key fallback references non-existent fields
   Files: server/src/server.ts:782-787

   The `accountKey` chain checks `(user as any).account_id` and `(client as any).accountId`, but only `user.idAccount` actually exists on the runtime types (RuntimeCharacter.idAccount). The two extra `as any` conditions are dead code that will never contribute and mask type checking. Since `idAccount` is optional and undefined for some sessions, those clients silently fall back to `socket.getIp(client)`, re-introducing the CGNAT grouping the PR aims to avoid — worth confirming `idAccount` is reliably populated. Simplify to `user.idAccount ?? socket.getIp(client) ?? idUser`.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Important

Your trial ends in 7 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

El server no desmarca personajes al apagarse: quedan bloqueados tras un reinicio

1 participant