Skip to content

Instantly share code, notes, and snippets.

@milushov
Created May 7, 2026 10:49
Show Gist options
  • Select an option

  • Save milushov/46554e490102c4fdb419129277c8a2e4 to your computer and use it in GitHub Desktop.

Select an option

Save milushov/46554e490102c4fdb419129277c8a2e4 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: 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) &bull; 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">
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>
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">&#10230;</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">&#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 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">&#8595; 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">
<b>Delete:</b> <code>destroy!</code> per record → PaperTrail + callbacks.<br>
&nbsp;&nbsp;Error-tolerant: continues on failure, collects errors.<br>
<b>Archive:</b> <code>update!</code> per record in <b>transaction</b>.<br>
&nbsp;&nbsp;Sets <code>status: 'archived', archived_at:, active: false</code>.<br>
&nbsp;&nbsp;Rolls back ALL on any 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>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><b>Delete</b> uses <code>destroy!</code> (not <code>delete_all</code>) — triggers PaperTrail + callbacks per reviewer feedback</li>
<li><b>Archive</b> wraps in a <b>transaction</b> — if one record fails, ALL roll back (data consistency)</li>
<li><b>Delete</b> is error-tolerant — continues on individual failures, collects errors</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