Created
May 7, 2026 11:48
-
-
Save milushov/1555407c555e3e2748d9c3efa789d085 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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: 1200px; 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; } | |
| .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; } | |
| .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: 120px; } | |
| .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; } | |
| .legend { max-width: 1200px; 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: 1200px; 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; } | |
| </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">↓ 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">↓ 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"> | |
| 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 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">⟶</div> | |
| <div class="http-detail"> | |
| /api/v1/client_area/<br>game_tables/cleanup<br><br> | |
| <code>{ game_table_ids,<br>action_type,<br>async: true }</code> | |
| </div> | |
| <div style="margin-top: 8px; font-size: 10px; color: #64748b;">max 500 IDs/req</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>.<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">↓ 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 from <code>CaAsyncOperation</code> → <code>CaIntegrationMessage.body</code>.<br> | |
| Updates operation status: <code>received → in_progress → processed/failed</code>. | |
| </div> | |
| </div> | |
| <div class="arrow-section"><span class="arrow">↓ Cleanup.call(...)</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 with magic timestamp.<br> | |
| <code>update!(active: false, archived_at: @archived_at)</code><br> | |
| All records get <b>same timestamp</b> — easy to find/query later.<br> | |
| Wrapped in <b>transaction</b> — rolls back ALL on any failure.<br> | |
| Batches of 100, <code>sleep(0.5)</code> between batches.<br> | |
| Hard-delete done later once confirmed safe. | |
| </div> | |
| </div> | |
| <div class="arrow-section"><span class="arrow">↓</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>destroy!</code> / <code>update!</code>.<br> | |
| </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 (throttled)</div> | |
| <div class="legend-item"><div class="legend-dot" style="background:#f472b6"></div> Model</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> — hard-delete later once confirmed safe</li> | |
| <li>All records in a batch get the <b>same <code>archived_at</code></b> — easy to find/query/rollback</li> | |
| <li>Wrapped in <b>transaction</b> — if one record fails, ALL roll back (data consistency)</li> | |
| <li><b>RGS updates local records</b> (destroy/archive) 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) — no DB overload</li> | |
| <li>Games worker uses <b>lock: :while_executing</b> (SidekiqUniqueJobs) — one job at a time</li> | |
| <li><code>CaIntegrationMessage</code> + <code>CaAsyncOperation</code> — full audit trail, status tracking</li> | |
| <li>Max <b>500 IDs per request</b> — enforced on both sides</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment