Created
May 7, 2026 15:03
-
-
Save milushov/6a2e422d9b64f17455519cfbb815eec8 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: 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">↓ 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"> | |
| 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">⟶</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">↓ 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 → <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">↓ 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">↓</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 → <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 → in_progress → deferred → 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 → CleanupJob → Cleanup service → Games API → CleanupWorker → Cleanup service → 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> → destroy! with callbacks → mark processed → 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