Skip to content

Instantly share code, notes, and snippets.

@milushov
Created May 7, 2026 15:03
Show Gist options
  • Select an option

  • Save milushov/6a2e422d9b64f17455519cfbb815eec8 to your computer and use it in GitHub Desktop.

Select an option

Save milushov/6a2e422d9b64f17455519cfbb815eec8 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CA-5591/CA-5592 Architecture</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 40px; }
h1 { text-align: center; margin-bottom: 8px; font-size: 24px; color: #f8fafc; }
.subtitle { text-align: center; color: #94a3b8; margin-bottom: 40px; font-size: 14px; }
.container { display: flex; gap: 40px; max-width: 1400px; margin: 0 auto; }
.project { flex: 1; border: 2px solid #334155; border-radius: 12px; padding: 24px; position: relative; }
.project-title { position: absolute; top: -14px; left: 20px; background: #0f172a; padding: 0 12px; font-weight: 700; font-size: 14px; letter-spacing: 1px; }
.rgs .project-title { color: #60a5fa; }
.games .project-title { color: #34d399; }
.box { border-radius: 8px; padding: 16px; margin-bottom: 16px; }
.job { background: #1e293b; border-left: 4px solid #60a5fa; }
.service { background: #1e293b; border-left: 4px solid #f59e0b; }
.controller { background: #1e293b; border-left: 4px solid #34d399; }
.worker { background: #1e293b; border-left: 4px solid #a78bfa; }
.model { background: #1e293b; border-left: 4px solid #f472b6; }
.purge { background: #1e293b; border-left: 4px solid #ef4444; }
.box-title { font-weight: 700; font-size: 13px; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
.box-title .tag { font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 600; }
.tag-job { background: #1e3a5f; color: #60a5fa; }
.tag-service { background: #422006; color: #f59e0b; }
.tag-controller { background: #064e3b; color: #34d399; }
.tag-worker { background: #2e1065; color: #a78bfa; }
.tag-model { background: #500724; color: #f472b6; }
.tag-purge { background: #450a0a; color: #ef4444; }
.box-body { font-size: 12px; color: #94a3b8; line-height: 1.6; }
.box-body code { background: #334155; padding: 1px 6px; border-radius: 4px; font-size: 11px; color: #e2e8f0; }
.arrow-section { display: flex; align-items: center; justify-content: center; margin: 12px 0; }
.arrow { color: #475569; font-size: 20px; }
.http-bridge { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; min-width: 80px; }
.http-arrow { font-size: 32px; color: #f59e0b; }
.http-label { font-size: 10px; color: #f59e0b; font-weight: 700; letter-spacing: 1px; }
.http-detail { font-size: 10px; color: #94a3b8; text-align: center; max-width: 140px; }
.flow-num { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; font-size: 11px; font-weight: 700; margin-right: 6px; flex-shrink: 0; }
.flow-num-blue { background: #1e3a5f; color: #60a5fa; }
.flow-num-green { background: #064e3b; color: #34d399; }
.flow-num-red { background: #450a0a; color: #ef4444; }
.legend { max-width: 1400px; margin: 32px auto 0; display: flex; gap: 24px; justify-content: center; flex-wrap: wrap; }
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #94a3b8; }
.legend-dot { width: 12px; height: 12px; border-radius: 3px; }
.note { max-width: 1400px; margin: 24px auto 0; padding: 16px; background: #1e293b; border: 1px solid #334155; border-radius: 8px; }
.note-title { font-size: 13px; font-weight: 700; color: #f59e0b; margin-bottom: 8px; }
.note-body { font-size: 12px; color: #94a3b8; line-height: 1.8; }
.note-body li { margin-left: 16px; }
.phase { max-width: 1400px; margin: 24px auto 0; padding: 12px 16px; border-radius: 8px; border: 1px dashed #475569; }
.phase-title { font-size: 12px; font-weight: 700; margin-bottom: 4px; }
.phase-1 { color: #60a5fa; }
.phase-2 { color: #ef4444; }
.phase-body { font-size: 12px; color: #94a3b8; }
</style>
</head>
<body>
<h1>CA-5591 / CA-5592 - Game Tables Cleanup</h1>
<p class="subtitle">Delete unused game tables (no traffic) - Archive game tables with traffic outside required currencies</p>
<div class="container">
<!-- RGS -->
<div class="project rgs">
<div class="project-title">RGS BACKEND (client-area)</div>
<div class="box job">
<div class="box-title">
<span class="flow-num flow-num-blue">1</span>
<span>GameTables::CleanupKickoffJob</span>
<span class="tag tag-job">JOB</span>
</div>
<div class="box-body">
Cron or manual trigger.<br>
Iterates <code>Environment.find_each</code><br>
Skips envs without qualifying servers.<br>
Enqueues <code>CleanupJob</code> per environment.
</div>
</div>
<div class="arrow-section"><span class="arrow">&#8595; perform_async(env.id)</span></div>
<div class="box job">
<div class="box-title">
<span class="flow-num flow-num-blue">2</span>
<span>GameTables::CleanupJob</span>
<span class="tag tag-job">JOB</span>
</div>
<div class="box-body">
Finds next active server (chains via <code>perform_async</code>).<br>
Delegates to <code>GameTables::Cleanup</code> service.<br>
Logs failures, chains to next server.
</div>
</div>
<div class="arrow-section"><span class="arrow">&#8595; Cleanup.call(server:, environment:)</span></div>
<div class="box service">
<div class="box-title">
<span class="flow-num flow-num-blue">3</span>
<span>GameTables::Cleanup</span>
<span class="tag tag-service">SERVICE</span>
</div>
<div class="box-body">
Creates magic <code>archived_at</code> = today 11:11:11.<br>
Queries <code>GameTable</code> outside <code>required_currencies</code>.<br>
Loads traffic from <code>AggregatedGGR</code>.<br>
Splits into <b>to_delete</b> (no traffic) / <b>to_archive</b> (with traffic).<br>
<b>Updates local records:</b> destroy / archive with magic <code>archived_at</code>.<br>
Sends <code>bo_id</code>s + <code>archived_at</code> to Games API in slices of 500.<br>
<code>async: true</code> in every request.
</div>
</div>
</div>
<!-- HTTP Bridge -->
<div class="http-bridge" style="padding-top: 200px;">
<div class="http-label">HTTP POST</div>
<div class="http-arrow">&#10230;</div>
<div class="http-detail">
/api/v1/client_area/<br>game_tables/cleanup<br><br>
<code>{ game_table_ids,<br>action_type,<br>archived_at,<br>async: true }</code>
</div>
<div style="margin-top: 8px; font-size: 10px; color: #64748b;">max 500 IDs/req<br>same archived_at on both sides</div>
</div>
<!-- Games -->
<div class="project games">
<div class="project-title">GAMES PROJECT</div>
<div class="box controller">
<div class="box-title">
<span class="flow-num flow-num-green">4</span>
<span>GameTablesController#cleanup</span>
<span class="tag tag-controller">CONTROLLER</span>
</div>
<div class="box-body">
Validates IDs limit (500).<br>
Creates <code>CaIntegrationMessage</code> (stores <code>archived_at</code>).<br>
<b>Async:</b> creates <code>CaAsyncOperation</code>, enqueues worker.<br>
<b>Sync:</b> calls service directly, returns result.
</div>
</div>
<div class="arrow-section"><span class="arrow">&#8595; perform_async(operation.id)</span></div>
<div class="box worker">
<div class="box-title">
<span class="flow-num flow-num-green">5</span>
<span>GameTables::CleanupWorker</span>
<span class="tag tag-worker">WORKER</span>
</div>
<div class="box-body">
Locked: <code>lock: :while_executing</code> (SidekiqUniqueJobs).<br>
Reads params + <code>archived_at</code> from message body.<br>
On success: status &rarr; <code>deferred</code> (protected from cleanup).<br>
Stores <code>game_table_ids</code> in <code>success_messages</code>.
</div>
</div>
<div class="arrow-section"><span class="arrow">&#8595; Cleanup.call(..., archived_at:)</span></div>
<div class="box service">
<div class="box-title">
<span class="flow-num flow-num-green">6</span>
<span>GameTables::Cleanup</span>
<span class="tag tag-service">SERVICE</span>
</div>
<div class="box-body">
Both <b>delete</b> and <b>archive</b> actions archive first.<br>
Uses <code>archived_at</code> from RGS (same timestamp both sides).<br>
<code>update!(active: false, archived_at: @archived_at)</code><br>
Wrapped in <b>transaction</b> - rolls back ALL on failure.<br>
Batches of 100, <code>sleep(0.5)</code> between batches.
</div>
</div>
<div class="arrow-section"><span class="arrow">&#8595;</span></div>
<div class="box model">
<div class="box-title">
<span>GameTable</span>
<span class="tag tag-model">MODEL</span>
</div>
<div class="box-body">
<code>has_paper_trail</code> - versions tracked.<br>
Callbacks fire on <code>update!</code>.
</div>
</div>
<div style="margin: 24px 0; border-top: 1px dashed #475569; padding-top: 16px;">
<div style="font-size: 11px; color: #64748b; margin-bottom: 12px; text-align: center;">PHASE 2: After verification</div>
</div>
<div class="box purge">
<div class="box-title">
<span class="flow-num flow-num-red">7</span>
<span>GameTables::PurgeArchived</span>
<span class="tag tag-purge">SERVICE</span>
</div>
<div class="box-body">
<code>call_from_operation(ca_async_operation_id:)</code><br>
Reads <code>game_table_ids</code> from <code>CaAsyncOperation.success_messages</code>.<br>
<code>destroy!</code> per record - PaperTrail + callbacks.<br>
Error-tolerant: continues on failure.<br>
On success: operation status &rarr; <code>processed</code>.<br>
Then <code>CleanCaIntegrationMessagesWorker</code> cleans up normally.
</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#60a5fa"></div> Sidekiq Job</div>
<div class="legend-item"><div class="legend-dot" style="background:#f59e0b"></div> Service</div>
<div class="legend-item"><div class="legend-dot" style="background:#34d399"></div> Controller</div>
<div class="legend-item"><div class="legend-dot" style="background:#a78bfa"></div> Worker (locked)</div>
<div class="legend-item"><div class="legend-dot" style="background:#f472b6"></div> Model</div>
<div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div> Purge (phase 2)</div>
</div>
<div class="note">
<div class="note-title">Key Design Decisions</div>
<div class="note-body">
<ul>
<li>Both <b>delete</b> and <b>archive</b> actions archive first with <b>magic timestamp</b> (today 11:11:11) - hard-delete later once confirmed safe</li>
<li><b>Same <code>archived_at</code></b> passed from RGS to Games - both sides have identical timestamps</li>
<li>Wrapped in <b>transaction</b> - if one record fails, ALL roll back</li>
<li><b>RGS updates local records</b> before sending to Games - both sides stay in sync</li>
<li>RGS chains servers one-by-one via <code>perform_async</code> (CA-5072 pattern)</li>
<li>Games worker uses <b>lock: :while_executing</b> (SidekiqUniqueJobs)</li>
<li><code>CaAsyncOperation</code> status: <code>received &rarr; in_progress &rarr; deferred &rarr; processed</code></li>
<li><code>deferred</code> operations protected from <code>CleanCaIntegrationMessagesWorker</code></li>
<li>Max <b>500 IDs per request</b> - enforced on both sides</li>
<li><b>PurgeArchived</b> uses IDs from <code>success_messages</code> (primary key, no full table scan)</li>
</ul>
</div>
</div>
<div class="phase">
<div class="phase-title phase-1">Phase 1: Automated</div>
<div class="phase-body">RGS KickoffJob &rarr; CleanupJob &rarr; Cleanup service &rarr; Games API &rarr; CleanupWorker &rarr; Cleanup service &rarr; archive with magic timestamp</div>
</div>
<div class="phase">
<div class="phase-title phase-2">Phase 2: Manual (after verification)</div>
<div class="phase-body"><code>GameTables::PurgeArchived.call_from_operation(ca_async_operation_id: X)</code> &rarr; destroy! with callbacks &rarr; mark processed &rarr; cleanup worker removes records</div>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment