<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://theikalman.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://theikalman.github.io/" rel="alternate" type="text/html" /><updated>2026-07-29T18:04:13+00:00</updated><id>https://theikalman.github.io/feed.xml</id><title type="html">AjiYakin</title><subtitle>Learning journal of Aji. Software Engineering, Photography, Life Lesson, and more.</subtitle><author><name>AjiYakin</name></author><entry><title type="html">TIL: SSE (Server-Sent Events) - The forgotten brother of WebSockets</title><link href="https://theikalman.github.io/til-sse" rel="alternate" type="text/html" title="TIL: SSE (Server-Sent Events) - The forgotten brother of WebSockets" /><published>2026-07-28T18:33:00+00:00</published><updated>2026-07-28T18:33:00+00:00</updated><id>https://theikalman.github.io/til-sse</id><content type="html" xml:base="https://theikalman.github.io/til-sse"><![CDATA[<p>Up until last week, the only thing I knew when I needed to send real-time
updates to a browser was WebSockets. Until I cam across to this
<a href="https://bytebytego.com/guides/shortlong-polling-sse-websocket/">article</a>. Yes,
I am a bit afraid to tell you that, since I have more than a decade of
experience as a Software Engineer and yet I don’t know anything about SSE. I
might be stupid at this point, but, hey, at least I learned something new
today.</p>

<p>Even to this day, not a lot of my friends know about SSE, and I’m too afraid to
even ask them about it at this point.</p>

<p>I even once worked on a real-time stock trading app, and there was no single
person who knew about SSE around the team at the time. Where this is actually a
good case for SSE as there’s no need for two-way communication.</p>

<p>But…</p>

<h2 id="what-is-sse">What is SSE?</h2>

<p>It is basically a standardized way to send real-time updates to a browser.
However it sits on top of regular HTTP/HTTPS instead of it’s brother WebSockets
who sits on top of WS/WSS (upgraded protocol).</p>

<h2 id="how-it-work">How it work?</h2>

<p><img src="/postimages/sse-server-client-connection.jpeg" alt="SSE Connection" /></p>

<ol>
  <li>Client open a connection to a server using JavaScript’s <code class="language-plaintext highlighter-rouge">EventSource</code> API.</li>
  <li>&amp; 3. Server sends a response with specific headers and body:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">Content-Type: text/event-stream</code></li>
      <li><code class="language-plaintext highlighter-rouge">Cache-Control: no-cache</code></li>
      <li><code class="language-plaintext highlighter-rouge">Connection: keep-alive</code></li>
    </ul>

    <p>And for the body, SSE requires a special format:</p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>data: &lt;data&gt;\n\n
</code></pre></div>    </div>

    <p>So it should start with <code class="language-plaintext highlighter-rouge">data:</code> and end with <code class="language-plaintext highlighter-rouge">\n\n</code>. And there is additional
fields that can be used as well:</p>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">event: &lt;event-name&gt;\n</code> Can be used for routing named events in JavaScript with <code class="language-plaintext highlighter-rouge">addEventListener</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">id: &lt;event-id&gt;\n</code> For automatic reconnection tracking.</li>
    </ul>
  </li>
</ol>

<h2 id="best-use-cases-for-sse">Best use cases for SSE?</h2>

<ul>
  <li>LLM response</li>
  <li>Live feeds like financial ticker</li>
  <li>Real-time dashboards or telemetry</li>
  <li>Background jobs &amp; notifications</li>
</ul>

<h2 id="demo">Demo</h2>

<p>Simple demo of SSE with Go and JavaScript.</p>

<p>File structure:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── index.html
└── main.go
</code></pre></div></div>

<p>Content of <code class="language-plaintext highlighter-rouge">main.go</code>:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"fmt"</span>
	<span class="s">"net/http"</span>
	<span class="s">"time"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">sseHandler</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
	<span class="c">// 1. Set SSE-required headers</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Header</span><span class="p">()</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">,</span> <span class="s">"text/event-stream"</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Header</span><span class="p">()</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"Cache-Control"</span><span class="p">,</span> <span class="s">"no-cache"</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Header</span><span class="p">()</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"Connection"</span><span class="p">,</span> <span class="s">"keep-alive"</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Header</span><span class="p">()</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"Access-Control-Allow-Origin"</span><span class="p">,</span> <span class="s">"*"</span><span class="p">)</span> <span class="c">// For local dev</span>

	<span class="c">// 2. Ensure ResponseWriter supports flushing</span>
	<span class="n">flusher</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">w</span><span class="o">.</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">Flusher</span><span class="p">)</span>
	<span class="k">if</span> <span class="o">!</span><span class="n">ok</span> <span class="p">{</span>
		<span class="n">http</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"Streaming unsupported!"</span><span class="p">,</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusInternalServerError</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="c">// 3. Stream data periodically until the client disconnects</span>
	<span class="n">count</span> <span class="o">:=</span> <span class="m">1</span>
	<span class="k">for</span> <span class="p">{</span>
		<span class="k">select</span> <span class="p">{</span>
		<span class="k">case</span> <span class="o">&lt;-</span><span class="n">r</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
			<span class="c">// Client closed the connection</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Client disconnected"</span><span class="p">)</span>
			<span class="k">return</span>
		<span class="k">case</span> <span class="o">&lt;-</span><span class="n">time</span><span class="o">.</span><span class="n">After</span><span class="p">(</span><span class="m">1</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)</span><span class="o">:</span>
			<span class="c">// SSE format requires starting with "data: " and ending with double newlines "\n\n"</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Fprintf</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"data: Message #%d at %s</span><span class="se">\n\n</span><span class="s">"</span><span class="p">,</span> <span class="n">count</span><span class="p">,</span> <span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span><span class="o">.</span><span class="n">Format</span><span class="p">(</span><span class="s">"15:04:05"</span><span class="p">))</span>
			<span class="n">flusher</span><span class="o">.</span><span class="n">Flush</span><span class="p">()</span> <span class="c">// Send immediately over the wire</span>
			<span class="n">count</span><span class="o">++</span>
		<span class="p">}</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">http</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/events"</span><span class="p">,</span> <span class="n">sseHandler</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Server running on http://localhost:8080"</span><span class="p">)</span>
	<span class="n">http</span><span class="o">.</span><span class="n">ListenAndServe</span><span class="p">(</span><span class="s">":8080"</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Content of <code class="language-plaintext highlighter-rouge">index.html</code>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
<span class="nt">&lt;head&gt;</span>
  <span class="nt">&lt;meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;title&gt;</span>SSE Demo<span class="nt">&lt;/title&gt;</span>
<span class="nt">&lt;/head&gt;</span>
<span class="nt">&lt;body&gt;</span>
  <span class="nt">&lt;h1&gt;</span>Live SSE Stream<span class="nt">&lt;/h1&gt;</span>
  <span class="nt">&lt;ul</span> <span class="na">id=</span><span class="s">"log"</span><span class="nt">&gt;&lt;/ul&gt;</span>

  <span class="nt">&lt;script&gt;</span>
    <span class="c1">// Open connection to the Go server's SSE endpoint</span>
    <span class="kd">const</span> <span class="nx">eventSource</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventSource</span><span class="p">(</span><span class="dl">'</span><span class="s1">http://localhost:8080/events</span><span class="dl">'</span><span class="p">);</span>

    <span class="c1">// Triggered every time the server sends data</span>
    <span class="nx">eventSource</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">log</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">log</span><span class="dl">'</span><span class="p">);</span>
      <span class="kd">const</span> <span class="nx">item</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">li</span><span class="dl">'</span><span class="p">);</span>
      <span class="nx">item</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
      <span class="nx">log</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">item</span><span class="p">);</span>
    <span class="p">};</span>

    <span class="c1">// Handle network errors or disconnects</span>
    <span class="nx">eventSource</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">EventSource failed:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
    <span class="p">};</span>
  <span class="nt">&lt;/script&gt;</span>
<span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>Then run the server with <code class="language-plaintext highlighter-rouge">go run main.go</code> and open <code class="language-plaintext highlighter-rouge">index.html</code> in browswer.
And it should look like this:</p>

<p><img src="/postimages/sse-demo.gif" alt="SSE Demo" /></p>]]></content><author><name>AjiYakin</name></author><category term="Dev" /><category term="Documentation" /><category term="Development" /><category term="Documentation" /><category term="TIL" /><summary type="html"><![CDATA[Up until last week, the only thing I knew when I needed to send real-time updates to a browser was WebSockets. Until I cam across to this article. Yes, I am a bit afraid to tell you that, since I have more than a decade of experience as a Software Engineer and yet I don’t know anything about SSE. I might be stupid at this point, but, hey, at least I learned something new today.]]></summary></entry><entry><title type="html">Building a Distributed Matchmaking System in Go</title><link href="https://theikalman.github.io/building-performant-and-resilience-matchmaking-game-server" rel="alternate" type="text/html" title="Building a Distributed Matchmaking System in Go" /><published>2026-07-03T18:00:00+00:00</published><updated>2026-07-03T18:00:00+00:00</updated><id>https://theikalman.github.io/building-performant-and-resilience-matchmaking-game-server</id><content type="html" xml:base="https://theikalman.github.io/building-performant-and-resilience-matchmaking-game-server"><![CDATA[<p>How can I solve the double-booking problem, handle worker crashes, and scale to
thousands of concurrent players with Redis Lua scripts and fault-tolerant Go
services.</p>

<h2 id="the-problem">The Problem</h2>

<p>When you build a matchmaking system for an online game, the naive approach is
simple: a player clicks “Find Match,” the server scans the pool of waiting
players, picks the closest MMR, and forms a match. This works fine - until you
need to scale.</p>

<p>Once you deploy multiple matchmaking workers to handle traffic, you hit a
classic distributed systems problem: the double-booking race condition.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Worker A: "Player X has MMR 1500. I'll match them with Player Y."
Worker B: "Player X has MMR 1500. I'll match them with Player Z."
</code></pre></div></div>

<p>Two workers read the same state, both form matches, and Player X ends up in two
games at once. Your players are unhappy.</p>

<p>And it gets worse. What if a worker claims 10 players, starts computing a match,
and then crashes? Those players are stuck in limbo - the system thinks they’re
being processed, but nobody is handling them. They never get a match, and they
can’t re-queue.</p>

<p>This post walks through how I built a production-grade matchmaking system in Go
that solves both of these problems using Redis Lua scripting, a heartbeat-based
supervisor pattern, and dynamic window expansion for match quality.</p>

<h2 id="architecture-overview">Architecture Overview</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                         [ WebSocket Clients ]
                                  │
                                  ▼
                        [ Edge API Gateway ]
                                  │
         ┌────────────────────────┴────────────────────────┐
         ▼                                                 ▼
 [ Matchmaking Service ]                           [ Matchmaking Service ]
   (Stateless Ingress)                               (Stateless Ingress)
         │                                                 │
         └────────────────────────┬────────────────────────┘
                                  ▼
                     [ Redis Enterprise Cluster ]
                (Sorted Sets, Hashes, Streams/PubSub)
                                  ▲
         ┌────────────────────────┴────────────────────────┐
         ▼                                                 ▼
 [ Match Engine Worker ]                           [ Match Engine Worker ]
    (Pool Allocation)                                 (Pool Allocation)
         │                                                 │
         └────────────────────────┬────────────────────────┘
                                  ▼
                    [ Dedicated Server Manager ]
</code></pre></div></div>

<p>The system has four decoupled components, each independently scalable:</p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Role</th>
      <th>Scale</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Ingress</td>
      <td>HTTP/WebSocket API - accepts tickets</td>
      <td>Horizontal (N instances)</td>
    </tr>
    <tr>
      <td>Redis</td>
      <td>State coordination - queues, locks, metadata</td>
      <td>Cluster mode</td>
    </tr>
    <tr>
      <td>Workers</td>
      <td>Match algorithm - claims, evaluates, matches</td>
      <td>Horizontal (N instances)</td>
    </tr>
    <tr>
      <td>Supervisor</td>
      <td>Fault tolerance - reclaims stranded tickets</td>
      <td>Singleton (or low-replica). Reclaim is a full <code class="language-plaintext highlighter-rouge">SCAN</code> over <code class="language-plaintext highlighter-rouge">mm:hb:*</code> keys (idempotent, safe to replica, but costs O(N) in worker count)</td>
    </tr>
    <tr>
      <td>Server Manager</td>
      <td>gRPC control plane - game server pool</td>
      <td>Singleton</td>
    </tr>
  </tbody>
</table>

<p>The key insight: Redis is the single source of truth. No worker holds mutable
state. Every operation - claiming tickets, heartbeating, releasing, reclaiming -
runs as an atomic Lua script inside Redis. This eliminates race conditions
without distributed locks.</p>

<h2 id="atomic-ticket-claiming-with-lua">Atomic Ticket Claiming with Lua</h2>

<p>The heart of the system is a Lua script that atomically claims tickets from the
queue. Here’s what it does:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- ClaimTickets Lua Script</span>
<span class="c1">-- Atomically: read, lock, remove, and store tickets</span>

<span class="kd">local</span> <span class="n">candidates</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s2">"ZRANGEBYSCORE"</span><span class="p">,</span>
    <span class="n">queueKey</span><span class="p">,</span> <span class="n">mmrMin</span><span class="p">,</span> <span class="n">mmrMax</span><span class="p">,</span> <span class="s2">"LIMIT"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">count</span><span class="p">)</span>

<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">ticketID</span> <span class="k">in</span> <span class="nb">ipairs</span><span class="p">(</span><span class="n">candidates</span><span class="p">)</span> <span class="k">do</span>
    <span class="kd">local</span> <span class="n">locked</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s2">"SET"</span><span class="p">,</span> <span class="n">lockKey</span><span class="p">,</span> <span class="n">workerID</span><span class="p">,</span>
                              <span class="s2">"NX"</span><span class="p">,</span> <span class="s2">"EX"</span><span class="p">,</span> <span class="n">lockTTL</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">locked</span> <span class="k">then</span>
        <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s2">"ZREM"</span><span class="p">,</span> <span class="n">queueKey</span><span class="p">,</span> <span class="n">ticketID</span><span class="p">)</span>
        <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s2">"HSET"</span><span class="p">,</span> <span class="n">processingKey</span><span class="p">,</span> <span class="n">ticketID</span><span class="p">,</span> <span class="n">jsonData</span><span class="p">)</span>
    <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Because this runs inside Redis’ single-threaded event loop, two workers
cannot claim the same ticket. The <code class="language-plaintext highlighter-rouge">NX</code> flag on <code class="language-plaintext highlighter-rouge">SET</code> ensures only one lock is
ever granted. The <code class="language-plaintext highlighter-rouge">EX</code> with TTL ensures locks are automatically released if the
worker crashes - the foundation of its fault tolerance.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────┐         ┌──────────┐
│ Worker A │         │ Worker B │
└─────┬────┘         └─────┬────┘
      │                    │
      │  EVALSHA claim     │  EVALSHA claim
      │  (same mmr range)  │  (same mmr range)
      ├────────────────────►│
      │                    │
      ▼                    ▼
 ┌──────────────────────────────┐
 │         Redis Lua            │
 │                              │
 │  ZRANGEBYSCORE → get tickets │
 │  For each:                   │
 │    SET lock NX EX ttl        │
 │    └── Worker A wins lock    │
 │    └── Worker B sees EXISTS  │
 │        → skips               │
 │  ZREM + HSET (only A's)      │
 └──────────────────────────────┘
</code></pre></div></div>

<h3 id="the-full-lua-script-family">The Full Lua Script Family</h3>

<p>Five scripts handle the complete lifecycle:</p>

<ol>
  <li>Claim Tickets - atomically move tickets from queue → processing</li>
  <li>Release Tickets - return tickets from processing → queue (no match found)</li>
  <li>Complete Tickets - remove matched tickets from processing + delete locks</li>
  <li>Heartbeat - refresh worker heartbeat + all lock TTLs</li>
  <li>Reclaim Tickets - find stale workers, return their tickets to queue</li>
</ol>

<p><img src="/postimages/lua-script-flow.svg" alt="Lua Script Flow" width="600" style="max-width: 100%; height: auto;" /></p>

<h2 id="fault-tolerance-the-supervisor-pattern">Fault Tolerance: The Supervisor Pattern</h2>

<p>When a worker claims tickets, it sets a lock with a TTL (default: 30 seconds).
It then starts a dedicated <code class="language-plaintext highlighter-rouge">Heartbeater</code> goroutine that refreshes this lease
(and all per-ticket lock TTLs) every 2 seconds via a Lua script. The
heartbeater is decoupled from the consumer goroutines: on context cancellation
it just returns - graceful ticket release is driven by the caller through
<code class="language-plaintext highlighter-rouge">Worker.Stop</code> -&gt; <code class="language-plaintext highlighter-rouge">redis.ReleaseAllTickets</code>, not by the heartbeater itself:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// internal/worker/heartbeat.go</span>
<span class="c">// Heartbeater refreshes the worker's lease and every lock TTL owned by it.</span>
<span class="c">// Runs every HeartbeatInterval (2s) in its own goroutine.</span>
<span class="k">func</span> <span class="p">(</span><span class="n">h</span> <span class="o">*</span><span class="n">Heartbeater</span><span class="p">)</span> <span class="n">Run</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">ticker</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">NewTicker</span><span class="p">(</span><span class="n">h</span><span class="o">.</span><span class="n">cfg</span><span class="o">.</span><span class="n">HeartbeatInterval</span><span class="p">)</span>
    <span class="k">defer</span> <span class="n">ticker</span><span class="o">.</span><span class="n">Stop</span><span class="p">()</span>

    <span class="k">for</span> <span class="p">{</span>
        <span class="k">select</span> <span class="p">{</span>
        <span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
            <span class="k">return</span>
        <span class="k">case</span> <span class="o">&lt;-</span><span class="n">ticker</span><span class="o">.</span><span class="n">C</span><span class="o">:</span>
            <span class="n">n</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">h</span><span class="o">.</span><span class="n">redis</span><span class="o">.</span><span class="n">Heartbeat</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">h</span><span class="o">.</span><span class="n">cfg</span><span class="o">.</span><span class="n">WorkerID</span><span class="p">,</span> <span class="n">h</span><span class="o">.</span><span class="n">cfg</span><span class="o">.</span><span class="n">LockTTL</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
                <span class="n">h</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">Warn</span><span class="p">(</span><span class="s">"heartbeat failed"</span><span class="p">,</span> <span class="s">"error"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
                <span class="n">HeartbeatFailures</span><span class="o">.</span><span class="n">Inc</span><span class="p">()</span>
                <span class="k">continue</span>
            <span class="p">}</span>
            <span class="n">LocksRefreshed</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="kt">float64</span><span class="p">(</span><span class="n">n</span><span class="p">))</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If a worker crashes, the heartbeat stops. Within 30 seconds, the lock TTL
expires. The Supervisor - a dedicated service that scans Redis every 5 seconds -
finds the expired heartbeat and reclaims all stranded tickets. It is split into
a <code class="language-plaintext highlighter-rouge">run</code> loop and a <code class="language-plaintext highlighter-rouge">reclaim</code> cycle (with metrics), and uses the standard
library’s <code class="language-plaintext highlighter-rouge">log/slog</code> rather than a third-party logger:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// internal/supervisor/reclaim.go</span>
<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Supervisor</span><span class="p">)</span> <span class="n">run</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">ticker</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">NewTicker</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">cfg</span><span class="o">.</span><span class="n">SuperviseInterval</span><span class="p">)</span> <span class="c">// 5s</span>
    <span class="k">defer</span> <span class="n">ticker</span><span class="o">.</span><span class="n">Stop</span><span class="p">()</span>

    <span class="k">for</span> <span class="p">{</span>
        <span class="k">select</span> <span class="p">{</span>
        <span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
            <span class="k">return</span>
        <span class="k">case</span> <span class="o">&lt;-</span><span class="n">ticker</span><span class="o">.</span><span class="n">C</span><span class="o">:</span>
            <span class="n">s</span><span class="o">.</span><span class="n">reclaim</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Supervisor</span><span class="p">)</span> <span class="n">reclaim</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">ReclaimAttempts</span><span class="o">.</span><span class="n">Inc</span><span class="p">()</span>
    <span class="n">start</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span>

    <span class="n">count</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">redis</span><span class="o">.</span><span class="n">ReclaimStaleTickets</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">s</span><span class="o">.</span><span class="n">cfg</span><span class="o">.</span><span class="n">LockTTL</span><span class="p">)</span>
    <span class="n">ReclaimDuration</span><span class="o">.</span><span class="n">Observe</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Since</span><span class="p">(</span><span class="n">start</span><span class="p">)</span><span class="o">.</span><span class="n">Seconds</span><span class="p">())</span>

    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="n">s</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">Warn</span><span class="p">(</span><span class="s">"reclaim cycle failed"</span><span class="p">,</span> <span class="s">"error"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
        <span class="n">ReclaimFailures</span><span class="o">.</span><span class="n">Inc</span><span class="p">()</span>
        <span class="k">return</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="n">count</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
        <span class="n">s</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">Info</span><span class="p">(</span><span class="s">"reclaimed stale tickets"</span><span class="p">,</span> <span class="s">"count"</span><span class="p">,</span> <span class="n">count</span><span class="p">)</span>
        <span class="n">ReclaimedTickets</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="kt">float64</span><span class="p">(</span><span class="n">count</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The reclaim Lua script iterates all heartbeat keys, checks their TTL, and
atomically moves any expired worker’s processing tickets back to the queue.
Other workers pick them up on the next poll cycle.</p>

<video src="/postimages/heartbeat-reclaim.mp4" width="700" style="max-width: 100%; height: auto;" controls="" muted="" loop="" playsinline=""></video>

<p>I measure the result in the <a href="#fault-recovery">Performance Benchmarks</a>
section: when a worker is killed mid-batch, its in-flight tickets hold in
<code class="language-plaintext highlighter-rouge">mm:proc:*</code> for the lease window, then the supervisor reclaims them and the
remaining workers drain to zero. Zero tickets lost.</p>

<h2 id="match-quality-window-expansion-and-batch-selection">Match Quality: Window Expansion and Batch Selection</h2>

<p>The second design challenge is match quality under variable load. During peak
hours, the queue is full of candidates, so I can afford to be picky. During
off-peak hours, I need to widen the search to avoid players waiting forever.</p>

<p>The matchmaker package provides a time-weighted expansion of the MMR search
window, used by the <code class="language-plaintext highlighter-rouge">Engine.FindMatch</code> seed-based matching path:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">SearchWindow</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">MinMMR</span> <span class="kt">int</span>
    <span class="n">MaxMMR</span> <span class="kt">int</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">CalculateSearchWindow</span><span class="p">(</span><span class="n">ticketMMR</span> <span class="kt">int</span><span class="p">,</span> <span class="n">waitTime</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">,</span> <span class="n">cfg</span> <span class="n">Config</span><span class="p">)</span> <span class="n">SearchWindow</span> <span class="p">{</span>
    <span class="n">delta</span> <span class="o">:=</span> <span class="n">cfg</span><span class="o">.</span><span class="n">MMRDeltaInitial</span> <span class="o">+</span> <span class="kt">int</span><span class="p">(</span><span class="n">cfg</span><span class="o">.</span><span class="n">MMRScalingFactor</span><span class="o">*</span><span class="n">waitTime</span><span class="o">.</span><span class="n">Seconds</span><span class="p">())</span>
    <span class="c">// Cap at ±500 to prevent absurd matches</span>
    <span class="k">if</span> <span class="n">delta</span> <span class="o">&gt;</span> <span class="m">500</span> <span class="p">{</span>
        <span class="n">delta</span> <span class="o">=</span> <span class="m">500</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">SearchWindow</span><span class="p">{</span>
        <span class="n">MinMMR</span><span class="o">:</span> <span class="n">ticketMMR</span> <span class="o">-</span> <span class="n">delta</span><span class="p">,</span>
        <span class="n">MaxMMR</span><span class="o">:</span> <span class="n">ticketMMR</span> <span class="o">+</span> <span class="n">delta</span><span class="p">,</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At time zero, a player with MMR 1500 searches within ±50 MMR (1500 ± 50). After
waiting 10 seconds, the window expands to 1500 ± 150. After 45 seconds, it’s
±500 - the maximum.</p>

<p>However, the high-throughput consumer uses a different strategy. Instead of
narrowing the Redis <code class="language-plaintext highlighter-rouge">ZRANGEBYSCORE</code> range per-seed (which limits how many
candidates a single poll can return), the consumer claims a batch of up to
<code class="language-plaintext highlighter-rouge">MaxPlayers</code> tickets across the full MMR range and then selects the best matches
locally:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">e</span> <span class="o">*</span><span class="n">Engine</span><span class="p">)</span> <span class="n">FormMatches</span><span class="p">(</span><span class="n">candidates</span> <span class="p">[]</span><span class="o">*</span><span class="n">domain</span><span class="o">.</span><span class="n">Ticket</span><span class="p">)</span> <span class="p">[]</span><span class="o">*</span><span class="n">domain</span><span class="o">.</span><span class="n">Match</span> <span class="p">{</span>
    <span class="c">// Sort by MMR, then greedily form as many matches as possible.</span>
    <span class="c">// Each iteration picks the contiguous window with the smallest</span>
    <span class="c">// MMR spread, forms a match, removes those tickets, and recurses</span>
    <span class="c">// on the remainder. This drains the entire batch per poll.</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">e</span> <span class="o">*</span><span class="n">Engine</span><span class="p">)</span> <span class="n">selectBestWindow</span><span class="p">(</span><span class="n">sorted</span> <span class="p">[]</span><span class="n">domain</span><span class="o">.</span><span class="n">Ticket</span><span class="p">)</span> <span class="p">[]</span><span class="n">domain</span><span class="o">.</span><span class="n">Ticket</span> <span class="p">{</span>
    <span class="c">// Sliding window over sorted candidates.</span>
    <span class="c">// Pick the group of 2-MaxPlayers players that minimizes MMR spread.</span>
    <span class="c">// Prefers tighter groups over larger ones.</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This batch-claim + tightest-spread selection approach trades slightly wider
MMR matches for dramatically higher throughput. A seed-based window of ±50 MMR
on a 10-player queue produces almost no matches and massive release/re-claim
churn; the batch approach claims 16 tickets and immediately forms 8 tight
matches. In practice, the tightest-spread selection within a random batch still
produces high-quality matches because the sliding window naturally finds the
closest MMR pairs.</p>

<p>One subtlety worth calling out: minimizing raw MMR spread with no
normalization for group size structurally biases <code class="language-plaintext highlighter-rouge">selectBestWindow</code> toward
<em>smaller</em> matches. Extending a contiguous sorted window from <em>N</em> to <em>N+1</em>
players can only keep the spread the same or grow it - it can never shrink -
so the tightest 2-player window is almost always tighter than the tightest
4-player window. Left unchecked, a mode with <code class="language-plaintext highlighter-rouge">MaxPlayers = 4</code> will tend to
keep pairing off the closest 2 players instead of forming full 4-player
matches, even when a perfectly reasonable 4-player match is available in the
same batch. I address this either by normalizing spread by group size (e.g.
dividing by <code class="language-plaintext highlighter-rouge">N</code> or <code class="language-plaintext highlighter-rouge">N-1</code> before comparing windows) or by requiring the
selector to prefer the target match size and only fall back to smaller
matches once the batch is exhausted.</p>

<video src="/postimages/batch-match-selection.mp4" width="700" style="max-width: 100%; height: auto;" controls="" muted="" loop="" playsinline=""></video>

<p><em>You can configure the playback of the video if it is too fast</em></p>

<p>The <code class="language-plaintext highlighter-rouge">CalculateSearchWindow</code> API remains available for deployment modes that
prioritize match quality over throughput - for example, a ranked-mode queue
with lower concurrency where per-seed windowing is viable.</p>

<h2 id="performance-benchmarks">Performance Benchmarks</h2>

<p>I ran the system through three benchmark scenarios to validate the design.</p>

<h3 id="latency-vs-concurrent-players">Latency vs. Concurrent Players</h3>

<p><img src="/postimages/latency_vs_concurrency.png" alt="Latency vs Concurrent Players" /></p>

<p>The system maintains sub-20ms p50 latency up to 1000 concurrent players, with
p95 staying under 75ms - well within the 500ms SLA target. Even at peak load,
p99 remains under 100ms. The curve is flat and predictable across the entire
concurrency range - no catastrophic degradation as load increases.</p>

<h3 id="queue-drain-rate">Queue Drain Rate</h3>

<p><img src="/postimages/drain_rate.png" alt="Drain Rate" /></p>

<p>With 10000 tickets seeded into the queue and 3 workers running, the system drains
the entire queue in ~9 seconds at a steady ~1,200 tickets/s. The drain is
near-linear (no stalls, no backpressure oscillations) right up to the empty
state. Processing depth stays at zero across every sample - each batch of
claimed tickets is matched, published, and immediately cleaned up via the
CompleteTickets script, so the processing hash never accumulates entries.
This is the central correctness property: matched tickets are HDEL-ed from
<code class="language-plaintext highlighter-rouge">mm:proc:*</code> and their lock keys DEL-ed in the same cycle, rather than leaking
into the processing hash for the worker’s lifetime (which previously caused
<code class="language-plaintext highlighter-rouge">processing_depth</code> to grow monotonically and stranded tickets to be
double-matched on supervisor reclamation). The clean, bounded drain confirms
predictable throughput with zero ticket leakage.</p>

<h3 id="fault-recovery">Fault Recovery</h3>

<p><img src="/postimages/fault_recovery.png" alt="Fault Recovery" /></p>

<p>When a worker is killed mid-processing (dashed red line), its in-flight tickets
stay stranded in <code class="language-plaintext highlighter-rouge">mm:proc:*</code> while its heartbeat lease counts down. ~15 seconds
later the supervisor detects the expired lease and reclaims the stranded
tickets - the chart shows the processing depth step down to zero at that point,
and the remaining workers drain both the reclaimed batch and the original
backlog. The system absorbs the failure transparently - players don’t see
their tickets vanish or get stuck.</p>

<h2 id="implementation-details">Implementation Details</h2>

<h3 id="project-structure">Project Structure</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmd/
├── ingress/          # HTTP/WebSocket API gateway
├── worker/           # Match engine background worker
├── supervisor/       # Ticket reclamation loop
└── server-manager/   # gRPC game server pool

internal/
├── domain/           # Ticket, Match, MMR models
├── redis/            # Client + Lua scripts
├── matchmaker/       # Window expansion, candidate selection
├── worker/           # Pool, consumer, heartbeat
├── supervisor/       # Reclamation logic
├── servermgr/        # gRPC server manager
└── telemetry/        # Prometheus metrics

deployments/
├── docker/           # Multi-stage Dockerfiles
├── docker-compose.yml
└── docker-compose.loadtest.yml
</code></pre></div></div>

<h3 id="tech-stack">Tech Stack</h3>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Technology</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Language</td>
      <td>Go 1.25.5</td>
    </tr>
    <tr>
      <td>State coordination</td>
      <td>Redis 7 (ZSETs, Hashes, Lua, PubSub)</td>
    </tr>
    <tr>
      <td>API</td>
      <td>HTTP/JSON + WebSocket (gorilla/websocket)</td>
    </tr>
    <tr>
      <td>Control plane</td>
      <td>gRPC (protobuf)</td>
    </tr>
    <tr>
      <td>Metrics</td>
      <td>Prometheus + Grafana (provisioned)</td>
    </tr>
    <tr>
      <td>Load testing</td>
      <td>k6 (containerized) + Go custom client</td>
    </tr>
    <tr>
      <td>Testing</td>
      <td>Go testing + real Redis (integration &amp; e2e via build tags)</td>
    </tr>
  </tbody>
</table>

<h3 id="redis-key-schema">Redis Key Schema</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mm:queue:{region}      → ZSET  (score=MMR, member=ticketID)
mm:tkt:{ticketID}      → HASH  (ticket metadata)
mm:proc:{workerID}     → HASH  (processing tickets)
mm:lock:{ticketID}     → STRING (TTL-based distributed lease)
mm:hb:{workerID}       → STRING (heartbeat with TTL)
mm:mch:{matchID}       → HASH  (match metadata)
</code></pre></div></div>

<h3 id="configuration-12-factor">Configuration (12-factor)</h3>

<p>Every parameter is configurable via environment variables:</p>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Default</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MMR_DELTA_INITIAL</code></td>
      <td>50</td>
      <td>Initial MMR search window (±)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MMR_SCALING_FACTOR</code></td>
      <td>10</td>
      <td>Window expansion per second of wait</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">LOCK_TTL</code></td>
      <td>30s</td>
      <td>Distributed lock time-to-live</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HEARTBEAT_INTERVAL</code></td>
      <td>2s</td>
      <td>How often workers refresh leases</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CLAIM_BATCH_SIZE</code></td>
      <td>10</td>
      <td>Max tickets claimed per poll</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MIN_PLAYERS_PER_MATCH</code></td>
      <td>2</td>
      <td>Minimum players to form a match</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MAX_PLAYERS_PER_MATCH</code></td>
      <td>8</td>
      <td>Maximum players per match</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">WORKER_POOL_SIZE</code></td>
      <td>GOMAXPROCS</td>
      <td>Consumer goroutines per worker</td>
    </tr>
  </tbody>
</table>

<h3 id="testing-strategy">Testing Strategy</h3>

<p>The codebase includes three tiers of testing:</p>

<table>
  <thead>
    <tr>
      <th>Tier</th>
      <th>What</th>
      <th>How</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Unit tests</td>
      <td>Domain models, window expansion, server pool logic</td>
      <td>Pure Go, no dependencies</td>
    </tr>
    <tr>
      <td>Integration tests</td>
      <td>Redis Lua scripts, worker lifecycle, supervisor reclaim</td>
      <td>Real Redis on <code class="language-plaintext highlighter-rouge">localhost:6379</code> (<code class="language-plaintext highlighter-rouge">-tags=integration</code>)</td>
    </tr>
    <tr>
      <td>E2E tests</td>
      <td>Full pipeline: submit → match → server allocation → WS notification</td>
      <td>Docker compose + real services</td>
    </tr>
    <tr>
      <td>Chaos tests</td>
      <td>Kill workers mid-operation, verify fault tolerance</td>
      <td>Shell script + <code class="language-plaintext highlighter-rouge">docker kill -9</code></td>
    </tr>
  </tbody>
</table>

<p>Lua script atomicity is tested with concurrent goroutines racing to claim the
same tickets. From <code class="language-plaintext highlighter-rouge">internal/redis/client_test.go</code>, the
<code class="language-plaintext highlighter-rouge">TestClaim_NoDoubleBooking</code> test seeds 20 tickets, then spawns 4 workers
identified by fresh UUIDs (not <code class="language-plaintext highlighter-rouge">worker-N</code> IDs), each of which loops 3 times
claiming a batch of up to 10 tickets across the full MMR range:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// From internal/redis/client_test.go: TestClaim_NoDoubleBooking</span>
<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">20</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
    <span class="n">require</span><span class="o">.</span><span class="n">NoError</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">client</span><span class="o">.</span><span class="n">EnqueueTicket</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">makeTicket</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="m">1500</span><span class="p">,</span> <span class="s">"us-east"</span><span class="p">)))</span>
<span class="p">}</span>

<span class="k">var</span> <span class="n">mu</span> <span class="n">sync</span><span class="o">.</span><span class="n">Mutex</span>
<span class="n">allClaims</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">)</span>

<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
<span class="k">for</span> <span class="n">w</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">w</span> <span class="o">&lt;</span> <span class="m">4</span><span class="p">;</span> <span class="n">w</span><span class="o">++</span> <span class="p">{</span>
    <span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
    <span class="k">go</span> <span class="k">func</span><span class="p">(</span><span class="n">workerID</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
        <span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">3</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
            <span class="n">tickets</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">client</span><span class="o">.</span><span class="n">ClaimTickets</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">workerID</span><span class="p">,</span> <span class="s">"us-east"</span><span class="p">,</span> <span class="m">10</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">9999</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">||</span> <span class="nb">len</span><span class="p">(</span><span class="n">tickets</span><span class="p">)</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
                <span class="k">continue</span>
            <span class="p">}</span>
            <span class="n">mu</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
            <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">tkt</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">tickets</span> <span class="p">{</span>
                <span class="n">allClaims</span><span class="p">[</span><span class="n">tkt</span><span class="o">.</span><span class="n">ID</span><span class="p">]</span><span class="o">++</span>
            <span class="p">}</span>
            <span class="n">mu</span><span class="o">.</span><span class="n">Unlock</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}(</span><span class="n">uuid</span><span class="o">.</span><span class="n">New</span><span class="p">()</span><span class="o">.</span><span class="n">String</span><span class="p">())</span>
<span class="p">}</span>
<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>

<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">count</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">allClaims</span> <span class="p">{</span>
    <span class="n">assert</span><span class="o">.</span><span class="n">Equal</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="n">count</span><span class="p">,</span> <span class="s">"ticket should be claimed at most once"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">SET ... NX EX ttl</code> inside the ClaimTickets Lua script guarantees each
ticket lock is acquired by exactly one worker, so across 4 × 3 = 12 competing
claim attempts the assertion never sees a ticket claimed more than once.</p>

<h2 id="what-i-learned">What I Learned</h2>

<h3 id="1-lua-scripts-are-the-killer-feature-of-redis">1. Lua scripts are the killer feature of Redis</h3>

<p>Before Lua scripts, the only way to do atomic multi-key operations was with
<code class="language-plaintext highlighter-rouge">WATCH</code>/<code class="language-plaintext highlighter-rouge">MULTI</code>/<code class="language-plaintext highlighter-rouge">EXEC</code> - optimistic locking that fails under contention. Lua
scripts execute atomically on a single thread, which means the claim operation
reads, locks, removes, and stores in one shot. No contention, no retries, no
double-booking.</p>

<h3 id="2-heartbeat--ttl-is-simpler-than-leader-election">2. Heartbeat + TTL is simpler than leader election</h3>

<p>Instead of implementing Raft or Paxos for fault tolerance, I use a 30-second
lease with a background heartbeat. The supervisor doesn’t elect new leaders - it
just scans for expired leases and returns tickets to the pool. This is
eventually consistent (it takes up to 30 seconds for a crash to be detected),
but for matchmaking, that’s perfectly acceptable. Players tolerate a 30-second
wait far more than they tolerate getting stuck in queue limbo forever.</p>

<h3 id="3-batch-selection-beats-per-seed-windowing-for-throughput">3. Batch selection beats per-seed windowing for throughput</h3>

<p>I initially tried narrowing the Redis <code class="language-plaintext highlighter-rouge">ZRANGEBYSCORE</code> range to a per-seed MMR
window (±50 at t=0, expanding with wait time). On paper this produces perfect
match quality - but in practice it caused massive release/re-claim churn: a
worker claims 16 tickets, finds only 2 within the window, and releases the other
14 back to the queue, where the next worker repeats the cycle. Throughput
collapsed by 100x at low concurrency. The fix was to claim across the full MMR
range and select the tightest-spread matches locally via <code class="language-plaintext highlighter-rouge">FormMatches</code>. The
sliding-window algorithm still produces high-quality matches (it finds the
closest MMR pairs in every batch), and the <code class="language-plaintext highlighter-rouge">CalculateSearchWindow</code> API remains
available for ranked modes where match quality matters more than throughput.</p>

<h3 id="4-test-with-real-redis-not-mocks">4. Test with real Redis, not mocks</h3>

<p>Integration tests run against a real Redis instance (<code class="language-plaintext highlighter-rouge">localhost:6379</code>, or
<code class="language-plaintext highlighter-rouge">REDIS_TEST_ADDR</code> when set - see <code class="language-plaintext highlighter-rouge">tests/testutil/redis.go</code>) rather than a mock.
This caught bugs that unit tests never would - subtle Lua scripting errors,
incompatible Redis versions, and race conditions that only manifest under
concurrent access. The <code class="language-plaintext highlighter-rouge">-tags=integration</code> build tag keeps these separate from
fast unit tests, so I still get quick feedback during development. I
deliberately did not adopt a container-per-test harness like testcontainers-go:
the suite assumes an externally provided Redis (started via <code class="language-plaintext highlighter-rouge">make docker-up</code> or
any reachable instance), which keeps iteration fast and matches how CI is
provisioned.</p>

<h3 id="5-gos-concurrency-model-maps-naturally-to-the-domain">5. Go’s concurrency model maps naturally to the domain</h3>

<p>The worker is a pool of consumer goroutines (one per CPU by default, sized via
<code class="language-plaintext highlighter-rouge">GOMAXPROCS</code>) plus a single heartbeat goroutine - <code class="language-plaintext highlighter-rouge">N+1</code> goroutines coordinated
with a <code class="language-plaintext highlighter-rouge">sync.WaitGroup</code> and cancelled via <code class="language-plaintext highlighter-rouge">context.Context</code>. There are no
inter-goroutine channels: each consumer shares state through the <code class="language-plaintext highlighter-rouge">Worker</code> struct
and through Redis, and the match engine (<code class="language-plaintext highlighter-rouge">FormMatches</code>) runs <em>inside</em> each
consumer’s poll loop rather than on a separate “main matchmaking” goroutine.
Each consumer polls Redis, claims a batch, forms matches, allocates a server,
and publishes the result end-to-end. The heartbeater just refreshes lease TTLs
on a ticker. Go’s lightweight goroutines (not OS threads) let it scale to
hundreds of workers per host without breaking a sweat.</p>

<hr />

<p>The source code for this system design exploration is available at my <a href="https://github.com/theikalman/matchmakinggames">GitHub
Repository</a>.</p>]]></content><author><name>AjiYakin</name></author><category term="Dev" /><category term="Documentation" /><category term="Development" /><category term="Documentation" /><summary type="html"><![CDATA[How can I solve the double-booking problem, handle worker crashes, and scale to thousands of concurrent players with Redis Lua scripts and fault-tolerant Go services.]]></summary></entry><entry><title type="html">My Experience Integrating AI Into My Workflow for The Last Two Months</title><link href="https://theikalman.github.io/my-experiences-of-integrating-ai-to-my-workflow" rel="alternate" type="text/html" title="My Experience Integrating AI Into My Workflow for The Last Two Months" /><published>2026-07-02T14:38:00+00:00</published><updated>2026-07-02T14:38:00+00:00</updated><id>https://theikalman.github.io/my-experiences-of-integrating-ai-to-my-workflow</id><content type="html" xml:base="https://theikalman.github.io/my-experiences-of-integrating-ai-to-my-workflow"><![CDATA[<p>The narrative around AI in software engineering usually oscillates between two
extremes: it is either a magic wand that replaces developers or just-another
autocomplete that generates bugs.</p>

<p>After spending the last two months trying to embed AI into my daily
engineering workflow, I have realized the truth is far more nuanced. It is an
incredible force multiplier, but it comes with hidden costs, both financial and
cognitive.</p>

<p>For the background, here is the tech stack that I use on daily basis:</p>
<ul>
  <li>I am developing CRUD app for multitenant aoutomotive workshop</li>
  <li>Golang on the backend</li>
  <li>Flutter on the frontend</li>
</ul>

<h3 id="the-tech-stack--tooling-shift">The Tech Stack &amp; Tooling Shift</h3>

<p>My experiment was split into two AI agent provider that I use.</p>

<ul>
  <li>Month 1 (VSCode + Copilot): I started with GitHub Copilot, simply because
it is cheap and also because my manager wanted to experiment with Copilot
while he himself uses Claude Code from day 1. The Copilot integrated
directly into Visual Studio Code. This felt like a natural extension of
traditional development, giving inline suggestions, quick chat sidebars,
and a relatively low friction point.</li>
  <li>I switched gears to Claude Code, mostly because GitHub changed its pricing
to a usage-based model. Suddenly, my token usage went through the roof;
I maxed out my allowance in just three days, whereas I used to only hit
the 50% mark (granted, I used to use it less). It was a pretty steep pricing
jump. Moving to Claude’s CLI tool and keeping the AI interface in the
terminal feels a lot less distracting. The workflow just feels smoother,
which makes sense since I prefer Neovim over VS Code anyway.</li>
</ul>

<p>As the weeks went by, my velocity skyrocketed. The more I used these tools, the
faster features went from ideation to production. But the real breakthrough
wasn’t just using AI; it was learning how to talk to it.</p>

<h3 id="from-blind-prompting-to-context-driven-directives">From “Blind Prompting” to Context-Driven Directives</h3>

<p>In the beginning, I asked the AI questions from a user perspective or in
high-level language. And I feel less-satisfied with the result of it.</p>

<p>I quickly discovered that targeting specific files yields vastly superior and
faster results than letting the AI guess the context. Instead of asking
“Implement a user blocking feature,” I learned to say “Modify
<code class="language-plaintext highlighter-rouge">user_repository.go</code>, <code class="language-plaintext highlighter-rouge">user_handler.go</code>, and <code class="language-plaintext highlighter-rouge">profile_screen.dart</code> to support a
blocking mechanism.” By feeding it the exact boundaries of the task, the
accuracy of the output reached near-perfection.</p>

<h4 id="for-the-case-of-holistic-feature-development">For the Case of Holistic Feature Development</h4>

<p>Traditionally, I would build the backend endpoint in Go, test it, and then
switch context to Flutter to consume it. AI changed my mental model.</p>

<p>Instead of treating the frontend and backend as separate silos, I found
myself instructing the AI to build out the feature as a whole ecosystem.
I started asking it to crank out the Go endpoint and the Flutter UI in a
single breath. At first, I thought this habit contradicted my previous
realization that detailed prompts work better than high-level “user-view”
ones. But I realized I was actually hitting a sweet spot right in the
middle: I was taking my own user-perspective goals and immediately
translating them into a deeply technical blueprint. This became my go-to
approach for building big features from scratch. The real turning point
for me, though, was forcing myself to write a detailed implementation plan
into a Markdown file first. It allowed me to thoroughly review the
architecture and track my own progress as things came together.</p>

<p>The Catch? Token Costs. Flutter and frontend code in general inherently
consume significantly more tokens than concise backend Go code. Feeding UI
layouts, state management, and widgets into the context window gets expensive
quickly. While the holistic approach saved me massive amounts of
context-switching time, it definitely hit the wallet harder.</p>

<h3 id="the-cognitive-trade-offs-atrophy-of-the-mind">The Cognitive Trade-Offs: Atrophy of the Mind</h3>

<p>While my output speed has never been higher, I began noticing unsettling shifts
in my own engineering skills.</p>

<h4 id="1-forgetting-the-syntax">1. Forgetting the Syntax</h4>

<p>Slowly but surely, I am forgetting how to code without an assistant. When the
AI handles the boilerplate, the syntax, and the typing, your muscle memory
begins to fade.</p>

<h4 id="2-outsourcing-investigation">2. Outsourcing Investigation</h4>

<p>I have noticed a decline in my urge to dive deep into debugging. Instead of
reading through tracebacks or manually analyzing database states, my first
instinct now is to dump raw logs, database query results, and error outputs
directly into the AI and ask it to investigate. It is highly efficient, but it
feels like outsourcing the soul of engineering.</p>

<h4 id="3-losing-touch-with-implementation-details">3. Losing Touch with Implementation Details</h4>

<p>I find myself caring less and less about the granular implementation details of
the code itself. As long as the integration tests pass and the feature works
smoothly, I move on. As a craftsman, this is a compromise I don’t entirely
love.</p>

<h4 id="keeping-the-mind-sharp-outside-of-work">Keeping the Mind Sharp Outside of Work</h4>

<p>To combat this cognitive decline, I have had to <em>consciously</em> schedule
intentional training sessions outside of work hours. I force myself to do
manual programming, practice data structures, and work through algorithms
without any AI assistance. Paradoxically, using AI to save time at work has
made my personal schedule much busier, just to maintain my mental edge.</p>

<h3 id="guardrails--the-reality-of-the-ai-myth">Guardrails &amp; The Reality of the “AI Myth”</h3>

<p>Despite using these tools heavily, I maintain a strict boundaries policy: <em>I
do not give AI access to everything</em>. Total autonomy is a security and
operational risk I am not willing to take. Man-in-the-middle verification is
still a mandatory part of my workflow.</p>

<p>Furthermore, the industry hype has created an “AI Myth” among clients.</p>

<p>Clients now expect complex features to be completed in a matter of minutes
because they believe the AI does all the work. I find myself spending more time
and effort managing expectations, educating clients, and explaining the
realities of software architecture than I used to.</p>

<h3 id="the-verdict-the-widening-gap-in-software-engineering">The Verdict: The Widening Gap in Software Engineering</h3>

<p>Ultimately, these past two months have proven to me that AI is not a
replacement for software engineers. It is a tool that we have to master
to stay competitive. AI is not going to replace the profession anytime
soon because software engineering is about so much more than just churning
out lines of code; it is about systems thinking, security, compliance,
and understanding human needs.</p>

<p>However, AI will drastically widen the gap between two types of developers:</p>

<ol>
  <li>The Code Workers: Those who rely on AI solely to churn out repetitive,
templated tasks, such as standard CRUD applications.</li>
  <li>The Systems Architects: Those who use AI to blast through the
boilerplate so they can focus their human intelligence on system
architecture, deep technical design, and high-level problem-solving.</li>
</ol>

<p>I think, AI won’t take your job (at least now), but an engineer leveraging
AI to think at a higher architectural level just might.</p>]]></content><author><name>AjiYakin</name></author><category term="Dev" /><category term="Documentation" /><category term="Development" /><category term="Documentation" /><summary type="html"><![CDATA[The narrative around AI in software engineering usually oscillates between two extremes: it is either a magic wand that replaces developers or just-another autocomplete that generates bugs.]]></summary></entry><entry><title type="html">My First Custom Split Keyboard with Raspberry Pi Pico</title><link href="https://theikalman.github.io/my-first-custom-split-keyboard" rel="alternate" type="text/html" title="My First Custom Split Keyboard with Raspberry Pi Pico" /><published>2025-09-07T14:00:00+00:00</published><updated>2025-09-07T14:00:00+00:00</updated><id>https://theikalman.github.io/my-first-custom-split-keyboard</id><content type="html" xml:base="https://theikalman.github.io/my-first-custom-split-keyboard"><![CDATA[<p>A few months ago, I started experiencing wrist pain — specifically in my left
hand — a clear sign of RSI (Repetitive Strain Injury). As a software engineer,
I type all day, and I realized I couldn’t ignore the discomfort any longer.</p>

<p>So I decided to take matters into my own hands — literally — by building a
custom split keyboard that better fit my ergonomic needs. The result? Not only
did I learn a lot through the process, but my RSI pain is now completely gone
after just two months of daily use.</p>

<hr />

<h2 id="cardboard-and-printed-layout">Cardboard and Printed Layout</h2>

<p>Before I jump and build the real keyboard, I wanted to get feel of split
keyboard first, and since I don’t have access to it yet, I decided to just
print it on the paper and just try to lay my hand there.</p>

<p>Next, after I get the feel on what kind of layout I wanted, I started with a
simple prototype. I printed out a layout based on an ortholinear QWERTY design,
lay it on top of cardboard and then started to make a hole for the MX-style
switches. This early test helped me understand how my fingers would travel
across the keys and how much spacing felt natural.</p>

<p>I didn’t want to switch to something like Dvorak or Colemak because sometimes I
still wanted to be able to use my laptop built-in keyboard. So, I wanted to
keep the familiar QWERTY layout but remove the traditional staggered rows —
which I’ve come to realize aren’t really ergonomic at all. Ortholinear was the
perfect middle ground: it’s cleaner, easier on the hands, and still intuitive.</p>

<p><img src="/postimages/IMG_20250326_122404-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250326_122321-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250326_150536-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" /></p>

<hr />

<h2 id="building-the-case-with-a-cnc-machine">Building the Case with a CNC Machine</h2>

<p>Because I don’t have a 3D printer, I took a different route. My family happens
to have a CNC machine that is used to use for making their logo from acrylic
for their hijab business, so I designed the keyboard case as a sandwich-style
build with that.</p>

<p><img src="/postimages/IMG_20250327_130202-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250327_130445-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250327_135606-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250330_103405-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250330_122529-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250330_122540-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250330_124242-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250404_081853-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250404_081924-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250404_082008-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250409_234659-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" /></p>

<hr />

<h2 id="powered-by-raspberry-pi-pico-and-kmk-firmware">Powered by Raspberry Pi Pico and KMK Firmware</h2>

<p>For the brains of the keyboard, I chose the <em>Raspberry Pi Pico</em>, this is
because its cheaper compared to the other such as <em>pro micro</em> or <em>nice!nano</em> —
one for each half. I’m running KMK firmware, a Python-based firmware built for
custom keyboards. KMK is especially nice if you’re already comfortable with
Python; it makes defining layers, combos, and key behavior straightforward.</p>

<p><img src="/postimages/IMG_20250411_094339-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250411_094348-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250417_081701-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" /></p>

<hr />

<h2 id="2-months-in-no-more-rsi">2 Months In: No More RSI</h2>

<p>After two months of using this keyboard as my daily driver, the results are
clear: my wrist pain is gone. The split design lets me keep my shoulders
relaxed and wrists straight, while the ortholinear layout reduces finger
movement strain. It’s a night-and-day difference from standard keyboards for
me.</p>

<p><img src="/postimages/IMG_20250423_151918-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" /></p>

<hr />

<h2 id="whats-next-making-it-more-portable">What’s Next: Making It More Portable</h2>

<p>Right now, the keyboard uses MX-style switches, which are pretty bulky. For my
next build, I’m exploring low-profile switch options to make a more portable
version — something I can toss into a backpack and use on the go. I’m also
considering integrating a custom cable solution or going wireless if power
efficiency allows.</p>

<p><img src="/postimages/IMG_20250417_134233-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" />
<img src="/postimages/IMG_20250417_134240-small.jpg" alt="My First Custom Keyboard with RaspBerry Pi" /></p>

<hr />

<h2 id="final-thoughts">Final Thoughts</h2>

<p>This project started as a way to deal with wrist pain, but it ended up becoming
one of the most satisfying and useful DIY builds I’ve ever done. I combined
software, hardware, and a bit of CNC machining to create something uniquely
mine — and it solved a real problem.</p>

<p>If you’ve ever struggled with RSI or just want to try something ergonomic and
custom, I highly recommend diving into the world of DIY keyboards. You don’t
need a 3D printer, and you don’t need to switch to a weird layout — just start
simple and iterate. Even there’s a lot of pre-made, read-to-build custom split
keyboard that is available in some eCommerce website, you probably wanted to go
that route if you prefer to just build it.</p>]]></content><author><name>AjiYakin</name></author><category term="Dev" /><category term="Documentation" /><category term="Development" /><category term="Documentation" /><summary type="html"><![CDATA[A few months ago, I started experiencing wrist pain — specifically in my left hand — a clear sign of RSI (Repetitive Strain Injury). As a software engineer, I type all day, and I realized I couldn’t ignore the discomfort any longer.]]></summary></entry><entry><title type="html">Debugging PHP App in NeoVim with Launch Configuration</title><link href="https://theikalman.github.io/debugging-php-app-in-neovim" rel="alternate" type="text/html" title="Debugging PHP App in NeoVim with Launch Configuration" /><published>2025-05-06T06:00:00+00:00</published><updated>2025-05-06T06:00:00+00:00</updated><id>https://theikalman.github.io/debugging-php-app-in-neovim</id><content type="html" xml:base="https://theikalman.github.io/debugging-php-app-in-neovim"><![CDATA[<p>Sometimes I need to debug PHP code in two different situations: a CLI (command-line) app and a web server app. While the way to run each one is slightly different, the steps are subtle, and I often forget the correct order for each case.</p>

<h3 id="neovim-plugin">NeoVim Plugin</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">mfussenegger/nvim-dap</code></li>
  <li><code class="language-plaintext highlighter-rouge">kristijanhusak/vim-dadbod-ui</code></li>
</ul>

<h3 id="project-directory-structure">Project Directory Structure</h3>
<p>Assuming we have this project directory structure:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src
└── App.php -- This is the entry point for CLI app
index.php   -- This is the entry point for Server app
composer.json
flake.nix   -- Nix flake file (NixOS)
.user.ini   -- PHP ini configuration
.vscode
└── launch.json -- Launch configuration
</code></pre></div></div>

<p>I am currently using Nix package manager to setup environment for each project that I have. With nix it is much more convenient to setup PHP environment since I can enable and include necessary PHP extension that I need to have, in this case I need to have xdebug extension.</p>

<p><code class="language-plaintext highlighter-rouge">flake.nix</code>:</p>
<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nv">description</span> <span class="o">=</span> <span class="s2">"debugphp"</span><span class="p">;</span>

  <span class="nv">inputs</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">nixpkgs</span><span class="o">.</span><span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:NixOS/nixpkgs/nixos-unstable"</span><span class="p">;</span>
    <span class="nv">nix-shell</span><span class="o">.</span><span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:loophp/nix-shell"</span><span class="p">;</span>
    <span class="nv">systems</span><span class="o">.</span><span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:nix-systems/default"</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="nv">outputs</span> <span class="o">=</span>
    <span class="nv">inputs</span><span class="o">@</span><span class="p">{</span>
      <span class="nv">self</span><span class="p">,</span>
      <span class="nv">flake-parts</span><span class="p">,</span>
      <span class="nv">systems</span><span class="p">,</span>
      <span class="o">...</span>
    <span class="p">}:</span>
    <span class="nv">flake-parts</span><span class="o">.</span><span class="nv">lib</span><span class="o">.</span><span class="nv">mkFlake</span> <span class="p">{</span> <span class="kn">inherit</span> <span class="nv">inputs</span><span class="p">;</span> <span class="p">}</span> <span class="p">{</span>
      <span class="nv">systems</span> <span class="o">=</span> <span class="kr">import</span> <span class="nv">systems</span><span class="p">;</span>

      <span class="nv">perSystem</span> <span class="o">=</span>
        <span class="p">{</span>
          <span class="nv">config</span><span class="p">,</span>
          <span class="nv">self</span><span class="err">'</span><span class="p">,</span>
          <span class="nv">inputs</span><span class="err">'</span><span class="p">,</span>
          <span class="nv">pkgs</span><span class="p">,</span>
          <span class="nv">system</span><span class="p">,</span>
          <span class="nv">lib</span><span class="p">,</span>
          <span class="o">...</span>
        <span class="p">}:</span>
        <span class="kd">let</span>
          <span class="nv">php</span> <span class="o">=</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">api</span><span class="o">.</span><span class="nv">buildPhpFromComposer</span> <span class="p">{</span>
            <span class="nv">src</span> <span class="o">=</span> <span class="nv">inputs</span><span class="o">.</span><span class="nv">self</span><span class="p">;</span>
            <span class="nv">php</span> <span class="o">=</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">php83</span><span class="p">;</span> <span class="c"># Change to php56, php70, ..., php81, php82, php83 etc.</span>
          <span class="p">};</span>
        <span class="kn">in</span>
        <span class="p">{</span>
          <span class="nv">_module</span><span class="o">.</span><span class="nv">args</span><span class="o">.</span><span class="nv">pkgs</span> <span class="o">=</span> <span class="kr">import</span> <span class="nv">self</span><span class="o">.</span><span class="nv">inputs</span><span class="o">.</span><span class="nv">nixpkgs</span> <span class="p">{</span>
            <span class="kn">inherit</span> <span class="nv">system</span><span class="p">;</span>
            <span class="nv">overlays</span> <span class="o">=</span> <span class="p">[</span> <span class="nv">inputs</span><span class="o">.</span><span class="nv">nix-shell</span><span class="o">.</span><span class="nv">overlays</span><span class="o">.</span><span class="nv">default</span> <span class="p">];</span>
            <span class="nv">config</span><span class="o">.</span><span class="nv">allowUnfree</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
          <span class="p">};</span>

          <span class="nv">devShells</span><span class="o">.</span><span class="nv">default</span> <span class="o">=</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">mkShellNoCC</span> <span class="p">{</span>
            <span class="nv">name</span> <span class="o">=</span> <span class="s2">"php-devshell"</span><span class="p">;</span>
            <span class="nv">buildInputs</span> <span class="o">=</span> <span class="p">[</span>
              <span class="nv">php</span>
              <span class="nv">php</span><span class="o">.</span><span class="nv">packages</span><span class="o">.</span><span class="nv">composer</span>
              <span class="nv">pkgs</span><span class="o">.</span><span class="nv">phpunit</span>
            <span class="p">];</span>
          <span class="p">};</span>

          <span class="nv">apps</span> <span class="o">=</span> <span class="p">{</span>
            <span class="c"># nix run .#composer -- --version</span>
            <span class="nv">composer</span> <span class="o">=</span> <span class="p">{</span>
              <span class="nv">type</span> <span class="o">=</span> <span class="s2">"app"</span><span class="p">;</span>
              <span class="nv">program</span> <span class="o">=</span> <span class="nv">lib</span><span class="o">.</span><span class="nv">getExe</span> <span class="p">(</span>
                <span class="nv">pkgs</span><span class="o">.</span><span class="nv">writeShellApplication</span> <span class="p">{</span>
                  <span class="nv">name</span> <span class="o">=</span> <span class="s2">"composer"</span><span class="p">;</span>

                  <span class="nv">runtimeInputs</span> <span class="o">=</span> <span class="p">[</span>
                    <span class="nv">php</span>
                    <span class="nv">php</span><span class="o">.</span><span class="nv">packages</span><span class="o">.</span><span class="nv">composer</span>
                  <span class="p">];</span>

                  <span class="nv">text</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">                    </span><span class="si">${</span><span class="nv">lib</span><span class="o">.</span><span class="nv">getExe</span> <span class="nv">php</span><span class="o">.</span><span class="nv">packages</span><span class="o">.</span><span class="nv">composer</span><span class="si">}</span><span class="s2"> "$@"</span><span class="err">
</span><span class="s2">                  ''</span><span class="p">;</span>
                <span class="p">}</span>
              <span class="p">);</span>
            <span class="p">};</span>

            <span class="c"># nix run .#phpunit -- --version</span>
            <span class="nv">phpunit</span> <span class="o">=</span> <span class="p">{</span>
              <span class="nv">type</span> <span class="o">=</span> <span class="s2">"app"</span><span class="p">;</span>
              <span class="nv">program</span> <span class="o">=</span> <span class="nv">lib</span><span class="o">.</span><span class="nv">getExe</span> <span class="p">(</span>
                <span class="nv">pkgs</span><span class="o">.</span><span class="nv">writeShellApplication</span> <span class="p">{</span>
                  <span class="nv">name</span> <span class="o">=</span> <span class="s2">"phpunit"</span><span class="p">;</span>

                  <span class="nv">runtimeInputs</span> <span class="o">=</span> <span class="p">[</span> <span class="nv">php</span> <span class="p">];</span>

                  <span class="nv">text</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">                    </span><span class="si">${</span><span class="nv">lib</span><span class="o">.</span><span class="nv">getExe</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">phpunit</span><span class="si">}</span><span class="s2"> "$@"</span><span class="err">
</span><span class="s2">                  ''</span><span class="p">;</span>
                <span class="p">}</span>
              <span class="p">);</span>
            <span class="p">};</span>
          <span class="p">};</span>
        <span class="p">};</span>
    <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">composer.json</code>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ajiyakin/debugphp"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sample project to demonstrate how to debug PHP in NeoVim"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"project"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"require"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"guzzlehttp/guzzle"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^7.9"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"require-dev"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"phpunit/phpunit"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^8.5"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"ext-xdebug"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"phpunit/php-code-coverage"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^7.0"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="s2">"MIT"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"autoload"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"psr-4"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"Ajiyakin\\Debugphp\\"</span><span class="p">:</span><span class="w"> </span><span class="s2">"src/"</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"authors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AjiYakin"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ajiyakin91@gmail.com"</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.vscode/launch.json</code>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"$schema"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://raw.githubusercontent.com/mfussenegger/dapconfig-schema/master/dapconfig-schema.json"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0.2.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"configurations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Listen for Xdebug"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"php"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"request"</span><span class="p">:</span><span class="w"> </span><span class="s2">"launch"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">9003</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Built-in Server with Xdebug"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"php"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"request"</span><span class="p">:</span><span class="w"> </span><span class="s2">"launch"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"runtimeArgs"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"-S"</span><span class="p">,</span><span class="w"> </span><span class="s2">"localhost:8080"</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">9003</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.user.ini</code> (per-directory php ini configuration file):</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">xdebug.mode</span><span class="p">=</span><span class="s">debug</span>
<span class="py">xdebug.client_host</span><span class="p">=</span><span class="s">0.0.0.0</span>
<span class="py">xdebug.client_port</span><span class="p">=</span><span class="s">9003</span>
<span class="py">xdebug.start_with_request</span><span class="p">=</span><span class="s">yes</span>
<span class="py">xdebug.idekey</span><span class="p">=</span><span class="s">NEOVIM</span>
</code></pre></div></div>

<h3 id="for-cli-app">For CLI App</h3>
<p>Here is running order for CLI App:</p>
<ol>
  <li>Add breakpoint in <code class="language-plaintext highlighter-rouge">App.php</code></li>
  <li>Run the <em>Listen for Xdebug</em> from launch configuration</li>
  <li>Run the cli with command: <code class="language-plaintext highlighter-rouge">XDEBUG_CONFIG="idekey=NEOVIM" php -c .user.ini ./vendor/bin/phpunit src/App.php</code></li>
</ol>

<h3 id="for-server-app">For Server App</h3>
<p>It is much more simple to run server app:</p>
<ol>
  <li>Add breakpoint in <code class="language-plaintext highlighter-rouge">index.php</code></li>
  <li>Run <em>Built-in Server with Xdebug</em></li>
  <li>Trigger breakpoint by sending request to the corresponding routing/endpoint.</li>
</ol>

<h3 id="notes">Notes</h3>
<p>I am not sure why the server is not shutting down when I stop the debugger for server app, I will need to figure out later, but a workaround for this is to manually kill the server with this command:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo kill</span> <span class="nt">-9</span> <span class="si">$(</span>lsof <span class="nt">-t</span> <span class="nt">-i</span> tcp:8080<span class="si">)</span>
</code></pre></div></div>]]></content><author><name>AjiYakin</name></author><category term="Dev" /><category term="Documentation" /><category term="Development" /><category term="Documentation" /><summary type="html"><![CDATA[Sometimes I need to debug PHP code in two different situations: a CLI (command-line) app and a web server app. While the way to run each one is slightly different, the steps are subtle, and I often forget the correct order for each case.]]></summary></entry><entry><title type="html">Harapan dan Kekecewaan - Mengatur Expektasi</title><link href="https://theikalman.github.io/harapan-dan-kekecewaan-mengatur-expektasi" rel="alternate" type="text/html" title="Harapan dan Kekecewaan - Mengatur Expektasi" /><published>2020-06-10T06:00:00+00:00</published><updated>2020-06-10T06:00:00+00:00</updated><id>https://theikalman.github.io/harapan-dan-kekecewaan-mengatur-expektasi</id><content type="html" xml:base="https://theikalman.github.io/harapan-dan-kekecewaan-mengatur-expektasi"><![CDATA[<p>Sekitar 2 bulan yang lalu, saya menyelesaikan membaca buku dengan judul
<em>Sebuah Seni untuk Bersikap Bodo Amat</em>, ini adalah versi terjemahan dari versi
bahasa inggrisnya yaitu <em>The Subtle Art of Not Giving a F*ck</em>. Buku ini
berhasil membuka mata saya tentang cara pandang saya terhadap harapan saya
sendiri. Sebagai background, saya ini orang yang senang sekali berhayal,
berharap, bermimpi, apapun lah istilahnya, yang kemudian mendorong saya untuk
berekspektasi bahwasannya sesuatu atau seseorang tersebut adalah harus sama
dengan apa yang saya ekspektasikan, yang mana seringkali tidak sesuai.</p>

<p>Buku ini mencoba memberikan gambaran tentang bagaimana susahnya berharap
terhadap sesuatu atau seseorang lalu kemudian kita berharap sesuatu atau
seseorang tersebut akan persis sesuai dengan apa yang kita harapkan, yang
akhirnya kita akan memberikan perhatian lebih terhadap hal ini, dan perhatian
inilah yang akan membuat kita lupa untuk hanya mengerjakan apa-apa yang
sebenarnya penting buat kita saja. Perhatian berlebihan ini yang bisa membuat
orang menyalahkan keadaan, menyalahkan orang lain, menyalahkan lingkungan dan
mungkin dalam tingkat yang lebih parah kita bisa jadi malah ikut campur sama
urusan orang diluar keharusan kita. Ikut campur disini tidak hanya langsung
berinteraksi yah, tapi “sekedar ikut memikirkan” atau bahkan silent judgment
juga menurut saya sudah bagian dari ikut campur.</p>

<p>Kata-kata yang paling saya ingat dalam buku tersebut adalah</p>

<blockquote>
  <p>Kita tidak bertanggung jawab untuk apa yang orang lain lakukan terhadap kita,
tapi kita bertanggung jawab penuh terhadap reaksi yang kita lakukan terhadap
sikap seseorang tersebut.</p>
</blockquote>

<p>Pada tahap ini saya sudah tidak mau lagi berharap sesuatu atau seseorang
itu menjadi apa yang saya fikirkan, tapi kalo ada kesempatan yang bisa saya
bantu untuk menjadikan sesuatu atau seseorang itu lebih baik ya saya coba
bantu. Dengan begini, saya tidak lagi repot dengan kekecewaan, saya jadi lebih
sadar tentang apa yang sebenarnya harus saya lakukan. Dan pastinya jadi lebih
<em>less stress</em>.</p>

<p>Kalo lagi ngomongin mencoba mengubah apa yang diluar kendali kita itu saya
jadi teringat pepatah, saya sendiri lupa siapa yang mengatakannya,
pepatahnya seperti ini</p>

<blockquote>
  <p>Ketika remaja, saya berusaha mengubah dunia. Ketika dewasa saya berusaha
mengubah lingkungan saya, dan sekarang saya hanya berusaha mengubah
diri saya sendiri.</p>
</blockquote>

<p>Jadi buat saya, sudah cukup berharap pada sesuatu atau seseorang, cukuplah
berharap pada yang pasti tidak mengecewakan, yaitu Alloh SWT. Dan mulailah
mengubah diri sendiri karena itu yang paling mudah dilakukan dan kita punya
kontrol penuh terhadap diri kita sendiri.</p>

<hr />

<p>Tulisan ini agak sedikit melenceng dari kebiasa saya, yaitu hanya menulis
soal teknikal di blog ini. Tapi ya apa boleh buat, saya ingin mendokumentasikan
apapun yang saya pelajari dalam hal apapun. Semoga ini bisa menjadi pelajaran
juga bagi yang membaca.</p>]]></content><author><name>AjiYakin</name></author><category term="Learning" /><category term="Life learn" /><category term="Book" /><summary type="html"><![CDATA[Sekitar 2 bulan yang lalu, saya menyelesaikan membaca buku dengan judul Sebuah Seni untuk Bersikap Bodo Amat, ini adalah versi terjemahan dari versi bahasa inggrisnya yaitu The Subtle Art of Not Giving a F*ck. Buku ini berhasil membuka mata saya tentang cara pandang saya terhadap harapan saya sendiri. Sebagai background, saya ini orang yang senang sekali berhayal, berharap, bermimpi, apapun lah istilahnya, yang kemudian mendorong saya untuk berekspektasi bahwasannya sesuatu atau seseorang tersebut adalah harus sama dengan apa yang saya ekspektasikan, yang mana seringkali tidak sesuai.]]></summary></entry><entry><title type="html">Gagal Shutdown di Lenovo Flex 14 dengan Linux Kernel 5.3 (Ubuntu 18.04)</title><link href="https://theikalman.github.io/gagal-shutdown-lenovo-flex-ubuntu-18.04-kernel-5.3" rel="alternate" type="text/html" title="Gagal Shutdown di Lenovo Flex 14 dengan Linux Kernel 5.3 (Ubuntu 18.04)" /><published>2020-06-01T08:15:00+00:00</published><updated>2020-06-01T08:15:00+00:00</updated><id>https://theikalman.github.io/gagal-shutdown-lenovo-flex-ubuntu-18.04-kernel-5.3</id><content type="html" xml:base="https://theikalman.github.io/gagal-shutdown-lenovo-flex-ubuntu-18.04-kernel-5.3"><![CDATA[<p>Setelah sekitar hampir 2 minggu yang lalu pas buka tas dan lihat laptop
masih nyala, agak heran juga karena gak biasanya linux kayak gini,
dulu pernah ngamalin (sekitar 5-6 tahun lalu) tapi itu di Windows bukan
Linux. Baru setelah satu minggu ada waktu buat start lakuin research karena
harus ngurus ini itu dulu (momen idul fitri) ketahuan lah penyebabnya
kenapa, itupun setelah 1 minggu research juga :)</p>

<p>Laptop Saya adalah Lenovo Flex 14. Laptop kantor sebenarnya, lol :)</p>

<p>Soal penyebabnya, Saya masih ragu sebenarnya, namun gambaran besarnya
adalah karena kernel Linux versi 5.3 yang sekarang nempel di Ubuntu 18.04
Saya itu ada bug yang entah bagaimana gagal membunuh semua proses yang
jalan di komputer Saya tersebut. Berikut referensi yang berkaitan dengan
masalah ini:</p>

<ul>
  <li><a href="https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1594023">https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1594023</a></li>
  <li><a href="https://askubuntu.com/questions/125844/shutdown-does-not-power-off-computer">https://askubuntu.com/questions/125844/shutdown-does-not-power-off-computer</a></li>
  <li><a href="https://unix.stackexchange.com/questions/457967/shutdown-does-not-power-off-why">https://unix.stackexchange.com/questions/457967/shutdown-does-not-power-off-why</a></li>
</ul>

<p>Untuk fix, ada dua solusi yang Saya temukan dari deretan resource diatas,
nambah <code class="language-plaintext highlighter-rouge">acpi=force</code> di grub pada saat linux dijalankan, dan yang kedua
adalah upgrade kernel. Saya sudah coba yang pertama, set
<code class="language-plaintext highlighter-rouge">GRUB_CMDLINE_LINUX_DEFAULT</code> di file grub menjadi</p>

<p><code class="language-plaintext highlighter-rouge">GRUB_CMDLINE_LINUX_DEFAULT="quiet splash acpi=force"</code></p>

<p>dari yang asalnya</p>

<p><code class="language-plaintext highlighter-rouge">GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"</code></p>

<p>yang, ternyata tidak berhasil, komputer Saya tetap tidak bisa mati secara
normal. LCD screen mati, tapi backlight keyboard tetap nyala, pun tombol
power tetap mengindikasikan nyala, kipas ya juga sama masih kedengaran
tetep muter.</p>

<p>Solusi yang berhasil adalah yang kedua, yaitu upgrade kernel Linux, meskipun
sebenarnya agak takut juga karena takutnya malah gagal dan, ya harus benerin
dari awal. Ini bukan takut sih, tapi lebih ke males :smile:</p>

<p>Saya coba lihat versi kernel Linux terakhir, ternyata yang terakhir dirilis
(versi stable) adalah <code class="language-plaintext highlighter-rouge">5.6.14</code> sementara yang terinstall di komputer Saya
adalah <code class="language-plaintext highlighter-rouge">5.3</code>. Ya sudah Saya putuskan coba install yang terakhir tersebut.</p>

<p><img src="/postimages/2020-06-01-gagal-shutdown-lenovo-flex-ubuntu-18.04-kernel-5.3_kernel_latest_version.png" alt="Latest Linux Kernel Version" /></p>

<p>Cara installnya Saya ikutin langkah-langkahnya sesuai yang ada di sini:</p>

<p><a href="https://www.tecmint.com/upgrade-kernel-in-ubuntu/">https://www.tecmint.com/upgrade-kernel-in-ubuntu/</a></p>

<p>Kurang lebih download file berikut</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>linux-headers-5.6.14-050614_5.6.14-050614.202005200733_all.deb
linux-headers-5.6.14-050614-generic_5.6.14-050614.202005200733_amd64.deb
linux-image-unsigned-5.6.14-050614-generic_5.6.14-050614.202005200733_amd64.deb
linux-modules-5.6.14-050614-generic_5.6.14-050614.202005200733_amd64.deb
</code></pre></div></div>

<p>dari sini: <a href="https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.6.14/">https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.6.14/</a></p>

<p>Setelah itu install semua file tersebut dengan perintah: <code class="language-plaintext highlighter-rouge">sudo dpkg -i *.deb</code></p>

<blockquote>
  <p>Catatan:</p>

  <p>Pada saat nginstall, ada warning, kurang lebih warningnya mengatakan
bahwa ada beberapa firmware yang hilang dibawah folder <code class="language-plaintext highlighter-rouge">/lib/firmware/amdgpu/</code>.
Salah satu firmware yang hilang adalah <code class="language-plaintext highlighter-rouge">navi12_asd.bin</code> tapi setelah Saya coba
cari-cari di <a href="https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/tree/amdgpu">repo firmware linuxnya</a>
gak nemu, akhirnya tidak Saya hiraukan warning tersebut, walhasil tetep
jalan kok :smile:</p>
</blockquote>

<p>Dan setelah langkah tersebut, reboot komputer dan setelah nyala lagi, pastiin
kernel yang diinstall tadi sudah berhasil di load dengan command <code class="language-plaintext highlighter-rouge">uname -rs</code>,
kalo yang muncul versi <code class="language-plaintext highlighter-rouge">5.6.14</code> ya berarti berhasil. Dan di kasus Saya, berhasil.
Saya jadi bisa shutdown komputer Saya secara normal dan matinya <em>sangat super cepat</em>,
gak kayak Windows yang super lemot kalo matiin komputer. Lol.</p>

<p>Ok, itu saja, Saya juga udah males nulisnya, bye.</p>]]></content><author><name>AjiYakin</name></author><category term="Troubleshot" /><category term="Linux" /><category term="Lenovo Flex 14" /><summary type="html"><![CDATA[Setelah sekitar hampir 2 minggu yang lalu pas buka tas dan lihat laptop masih nyala, agak heran juga karena gak biasanya linux kayak gini, dulu pernah ngamalin (sekitar 5-6 tahun lalu) tapi itu di Windows bukan Linux. Baru setelah satu minggu ada waktu buat start lakuin research karena harus ngurus ini itu dulu (momen idul fitri) ketahuan lah penyebabnya kenapa, itupun setelah 1 minggu research juga :)]]></summary></entry><entry><title type="html">EFS Ter-Mount dan Terbaca Tapi ECS Tidak Berhasil Menyimpan Data Ke EFS</title><link href="https://theikalman.github.io/efs-termount-dan-terbaca-tapi-ecs-tidak-berhasil-menyimpan-ke-efs" rel="alternate" type="text/html" title="EFS Ter-Mount dan Terbaca Tapi ECS Tidak Berhasil Menyimpan Data Ke EFS" /><published>2017-01-19T04:08:00+00:00</published><updated>2017-01-19T04:08:00+00:00</updated><id>https://theikalman.github.io/efs-termount-dan-terbaca-tapi-ecs-tidak-berhasil-menyimpan-ke-efs</id><content type="html" xml:base="https://theikalman.github.io/efs-termount-dan-terbaca-tapi-ecs-tidak-berhasil-menyimpan-ke-efs"><![CDATA[<p>Seharian kemarin, berkutat sama AWS (lagi). Ya kan emang kerjaannya pake AWS :smile:
Ada hal yang aneh ketika cluster di ECS yang sudah saya set-up agar nge-mount
EFS tidak bisa menulis ke EFS, ato singkatnya ECS tidak berhasil menulis data
ke EFS.</p>

<p>Tapi sebelum Saya ngasih tau alesan kenapa bisa kejadian seperti itu, dan
menceritakan bagaimana Saya nge-fix masalah tersebut, ada baiknya Saya kasih
gambaran umum dulu apa itu EFS, ECS dan AWS itu sendiri. Biar yang baca,
meski pemula gak bingung :smile:</p>

<h2 id="aws-amazon-web-service">AWS (Amazon Web Service)</h2>
<p>Singkatnya ini services yang ditawarkan Amazon untuk para pengembang, terutama
dilingkungan web, ya gak jauh-jauh sama cloud-computing-lah. Service yang
ditawarkan juga beragam, dari mulai VPS, AWS-Lambda (bisa jalanin kodingan
tanpa butuh server, maksudnya kita gak perlu set-up server), termasuk kalo
yang suka ngopre IoT, AWS juga nyediain yang namanya AWS IoT, dan services
yang lainnya banyak (banget), jadi gak bisa saya sebutin – lagian gak dibayar
juga sama AWS-nya :smile:.</p>

<h2 id="ecs-ec2-container-service">ECS (EC2 Container Service)</h2>
<p>Sebelumnya kalo belum tahu apa itu EC2, EC2 itu ya anggaplah komputer biasa
(aslinya virtual-machine). Terus, nyambungnya sama ECS apaan? ECS sendiri
sebenarnya (kalo saya bilang) service yang disediain AWS untuk manage
docker yang ada di EC2-nya itu sendiri. Apa itu docker? Kayaknya yang ini
tolong cari tahu sendiri ya :smile: ECS ini ketika kita buat instance
docker (container – docker run kalo di lokal komputermu) akan membuat 
sendiri EC2 instance untuk nyimpen container docker-nya tersebut,
juga, kalau kita jalanin 2 atau lebih docker container ya ECS bisa kita
set supaya nambah sendiri (kalo misal kurang) EC2-nya.</p>

<h2 id="efs-elastic-file-system">EFS (Elastic File System)</h2>
<p>Kalo yang ini, anggaplah storage biasa, yang bisa dilepas-pasang
ke EC2 tadi itu, yang mana EC2 itu juga dipake sama ECS.</p>

<p>Penjelasannya singkat saja, hanya untuk memberi gambaran istilah-istilah dari
masalah yang Saya coba dokumentasikan disini. Oke, kalo gitu lanjut.</p>

<h2 id="aku-mau-ngapain">Aku Mau Ngapain?</h2>
<p>Niat Saya (sebenarnya kami, karena ini kerjaan kantor :smile:), adalah
karena si ECS ini sering di-restart (tiap release – dan releasenya bisa
1-2 kali seminggu), jadi ECS gak bisa nyimpen data secara persistent, ato
tetap, jadi pas di restart ya datanya ilang, sementara data yang harus
di proses (yang sebelumnya di download dulu) itu lumayan besar, jadi
ya risih ajah harus download berulang-ulang. Jadi ya niatnya datanya
disimpen di EFS, EFSnya di mount ke ECS, nanti ECS nyimpen datanya
di EFS yang ke-mount tersebut. Singkatnya gitu.</p>

<h2 id="konfigurasi">Konfigurasi</h2>
<p>Konfigurasinya sebenarnya sudah berhasil ngikutin <a href="https://aws.amazon.com/blogs/compute/using-amazon-efs-to-persist-data-from-amazon-ecs-containers/">ini</a>.
Cuma untuk launch-configuration-nya, pake script yang sederhana, gak
seribet yang ditulis di blog tersebut, kayak gini:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># Configure ECS cluster</span>
<span class="nb">echo </span><span class="nv">ECS_CLUSTER</span><span class="o">=</span>&lt;NAMA_CLUSTER_DI_ECS&gt; <span class="o">&gt;&gt;</span> /etc/ecs/ecs.config

<span class="c"># EFS configurations</span>
yum <span class="nt">-y</span> <span class="nb">install </span>nfs-utils
<span class="nb">mkdir</span> <span class="nt">-p</span> &lt;PATH_FOLDER_KEMANA_EFS_TSB_AKAN_DI_MOUNT&gt;
mount <span class="nt">-t</span> nfs4 <span class="nt">-o</span> <span class="nv">nfsvers</span><span class="o">=</span>4.1,rsize<span class="o">=</span>1048576,wsize<span class="o">=</span>1048576,hard,timeo<span class="o">=</span>600,retrans<span class="o">=</span>2 &lt;DISINI_DNS_EFSNYA&gt; &lt;PATH_FOLDER_KEMANA_EFS_TSB_AKAN_DI_MOUNT&gt;
</code></pre></div></div>

<p>Dan ini berhasil, disebut berhasilnya gimana? Well, ngeceknya
bisa pake command <code class="language-plaintext highlighter-rouge">df -T</code>, akan keliatan apa saja yang ke mount
ke EC2 tersebut. Dan setelah coba ditulisi, berhasil juga, datanya
ke store ke EFS, begitu juga kalo EFSnya dicoba di mount dari EC2
yang lain, terbaca juga EFS beserta file yang sudah disimpan tadi.</p>

<h2 id="masalah">Masalah</h2>
<p>Salahnya, Saya terlalu yakin kalo si ECS (docker) ini akan berhasil
nge-mount EFS (sesuai konfigurasi path diatas) ke containernya :smile:
Sampe akhirnya kejadian aneh datanya gak mau ke store di EFS.
Padahal dicek dari EC2, EFSnya sudah ke mount. Dan… dicoba ditulisi
dari container dockernya (yang dijalanin otomatis sama si ECS),
datanya kesimpen di EC2nya doang, bukan di EFS.</p>

<p>Iya, Saya yang salah, tolong jangan banting gelas :smile:</p>

<h2 id="solusi">Solusi</h2>
<p>Setelah seharian berkutat, akhirnya nemu <a href="https://forums.aws.amazon.com/thread.jspa?threadID=214845">ini</a>
yang cuma disuruh restart docker daemon sama agent ECSnya doang :smile:</p>

<p>Dicoba…</p>

<p>Dan yup, berhasil. Terus kalo mau otomatis gimana? Ah biasa, disimpen
di launch-configuration-nya itu loh. Tempelin script dibawah ini dipaling
bawah di script launch-configuration-nya, setelah nge-mount EFSnya:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Restart docker daemon and ecs (docker-agent),</span>
<span class="c"># so docker would be able to "see" available EFS</span>
service docker restart <span class="o">&amp;&amp;</span> start ecs
</code></pre></div></div>

<p>Versi lengkap launch-configuration-nya:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># Configure ECS cluster</span>
<span class="nb">echo </span><span class="nv">ECS_CLUSTER</span><span class="o">=</span>&lt;NAMA_CLUSTER_DI_ECS&gt; <span class="o">&gt;&gt;</span> /etc/ecs/ecs.config

<span class="c"># EFS configurations</span>
yum <span class="nt">-y</span> <span class="nb">install </span>nfs-utils
<span class="nb">mkdir</span> <span class="nt">-p</span> &lt;PATH_FOLDER_KEMANA_EFS_TSB_AKAN_DI_MOUNT&gt;
mount <span class="nt">-t</span> nfs4 <span class="nt">-o</span> <span class="nv">nfsvers</span><span class="o">=</span>4.1,rsize<span class="o">=</span>1048576,wsize<span class="o">=</span>1048576,hard,timeo<span class="o">=</span>600,retrans<span class="o">=</span>2 &lt;DISINI_DNS_EFSNYA&gt; &lt;PATH_FOLDER_KEMANA_EFS_TSB_AKAN_DI_MOUNT&gt;

<span class="c"># Restart docker daemon and ecs (docker-agent),</span>
<span class="c"># so docker would be able to "see" available EFS</span>
service docker restart <span class="o">&amp;&amp;</span> start ecs
</code></pre></div></div>

<h2 id="lesson-learned">Lesson Learned</h2>

<h3 id="baca-dokumentasi-secara-seksama">Baca dokumentasi secara seksama</h3>

<p>Karena ternyata, di dokumentasinya ada notes, yang persis “menyinggung”
masalah ini :smile:</p>

<p>Dokumentasinya ada <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_data_volumes.html">disini</a>.
Kutipan notes-nya:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>For operating systems that use devicemapper (such as Amazon Linux and the Amazon
ECS-optimized AMI), only file systems that are available when the Docker daemon
is started will be available to Docker containers. You can use a cloud boothook 
to mount your file system before the Docker daemon starts, or you can restart 
the Docker daemon and the Amazon ECS container agent after the file system is 
mounted to make the file system available to your container volume mounts.
</code></pre></div></div>

<p>Sekian untuk hari ini, semoga bermanfaat :thumbsup:</p>

<p>Dan mohon do’akan supaya Saya konsisten nulis di blog karatan ini.</p>]]></content><author><name>AjiYakin</name></author><category term="Troubleshot" /><category term="AWS" /><category term="ECS" /><category term="EFS" /><category term="DevOps" /><summary type="html"><![CDATA[Seharian kemarin, berkutat sama AWS (lagi). Ya kan emang kerjaannya pake AWS :smile: Ada hal yang aneh ketika cluster di ECS yang sudah saya set-up agar nge-mount EFS tidak bisa menulis ke EFS, ato singkatnya ECS tidak berhasil menulis data ke EFS.]]></summary></entry><entry><title type="html">Setting Touch-Pad di Elementary OS Freya</title><link href="https://theikalman.github.io/setting-touch-pad-di-elementary-os-freya" rel="alternate" type="text/html" title="Setting Touch-Pad di Elementary OS Freya" /><published>2015-12-13T02:44:00+00:00</published><updated>2015-12-13T02:44:00+00:00</updated><id>https://theikalman.github.io/setting-touch-pad-di-elementary-os-freya</id><content type="html" xml:base="https://theikalman.github.io/setting-touch-pad-di-elementary-os-freya"><![CDATA[<p>Kebetulan 2 minggu yang lalu saya beli laptop baru dan… ya biasa, instalasi
dan ya gitu, melakukan beberapa setting supaya lebih ‘gue banget’. Kebetulan
juga (banyak kebetulannya) saya liat video macbook yang ‘nggeser workspacenya
enak banget pake 4 jari, srettt… Walhasil, sayapun jadi <em>kabita</em> dan
langsung nyari-nyari tools apa yang bisa saya pake.</p>

<p>Okeh, akhirnya ada beberapa tools yang ternyata bisa saya gunakan untuk
memaksimalkan kemampuan touch-pad saya tersebut, setelah sebelumnya cuma
dipake buat scroll atas bawah pakek dua jari doang :smile:</p>

<p>Tools yang bisa dipake bisa macem-macem, dan ternyata secara bawaan di
elementary os juga ternyata sudah ada, yaitu <a href="https://launchpad.net/canonical-multitouch/ginn">ginn</a> dan setelah saya
coba-coba browsing dan coba melakukan beberapa konfigurasi dengan hasil
hampir 70% touch-pad saya tidak bekerja dengan baik. Sebenarnya sama ginnya
(touch-pad saya kedetect) hanya saja entah kenapa <a href="https://launchpad.net/canonical-multitouch/ginn">ginn</a> gak ngejalanin
perintahnya. Terus saya coba-coba install aplikasi lain, dan nemulah
<a href="https://github.com/JoseExposito/touchegg">touchegg</a>. Awalnya agak ragu juga, soalnya direpositorynya sendiri
update-an terakhir adalah 2 tahun lalu, sedangkan saya baru aja beli
laptop 2 minggu yang lalu, bisa saja hardware saya tidak compatible,
fikir saya. Tapi setelah saya coba akhirnya justru malah hasilnya lebih baik,
dan langsung ajah saya coba cari-cari konfigurasi yang orang lain pakek buat
aplikasi ini, dan akhirnya nemu juga. Saya lupa dari mana sumbernya :smile:
tapi mudah-mudahan ‘mpunya’ gak ngamuk :smile: dan saya backup langsung di
<a href="https://gist.github.com/ajiyakin/5a7254158852cbe901ce">gists</a> saya, supaya nanti saya bisa pakek lagi kalo suatu saat saya install
ulang lagi.</p>

<p>Oke, berikut ini step buat masang <a href="https://github.com/JoseExposito/touchegg">touchegg</a> di elementary os:</p>

<ol>
  <li>Install <a href="https://github.com/JoseExposito/touchegg">touchegg</a> dengan command: <code class="language-plaintext highlighter-rouge">sudo apt-get install touchegg</code> atau build
langsung dari source codenya.</li>
  <li>Tambahkan file konfigurasinya, kopi dari <a href="https://gist.github.com/ajiyakin/5a7254158852cbe901ce">sini</a>, dan simpan di
<code class="language-plaintext highlighter-rouge">~/.config/touchegg/touchegg.conf</code></li>
  <li>Tambahkan <a href="https://github.com/JoseExposito/touchegg">touchegg</a> agar dijalankan pas start-up, caranya ada di
<em>System Settings</em> dibagian <em>Applications</em> dan di tab <em>Startup</em> dan klik
tanda plus (+) di bagian kiri bawah dan tuliskan <code class="language-plaintext highlighter-rouge">/usr/bin/touchegg</code> lalu
tekan enter.</li>
  <li>Selesai, coba logout dan login lagi. Terus coba geser workspace dengan 4
jari (kekiri atau kekanan). Atau expands workspace dengan slide 4 jari
kearah atas.</li>
</ol>

<p>Itu ajah. Untuk distro linux yang lain mungkin tidak terlalu jauh berbeda
:smile:</p>]]></content><author><name>AjiYakin</name></author><category term="Troubleshot" /><category term="Linux" /><category term="Troubleshot" /><summary type="html"><![CDATA[Kebetulan 2 minggu yang lalu saya beli laptop baru dan… ya biasa, instalasi dan ya gitu, melakukan beberapa setting supaya lebih ‘gue banget’. Kebetulan juga (banyak kebetulannya) saya liat video macbook yang ‘nggeser workspacenya enak banget pake 4 jari, srettt… Walhasil, sayapun jadi kabita dan langsung nyari-nyari tools apa yang bisa saya pake.]]></summary></entry><entry><title type="html">Apa Itu DevOps</title><link href="https://theikalman.github.io/apa-itu-devops" rel="alternate" type="text/html" title="Apa Itu DevOps" /><published>2015-11-17T19:21:00+00:00</published><updated>2015-11-17T19:21:00+00:00</updated><id>https://theikalman.github.io/apa-itu-devops</id><content type="html" xml:base="https://theikalman.github.io/apa-itu-devops"><![CDATA[<p>Awalnya saya agak mengkerutkan dahi ketika <em>lead</em> programmer saya bilang
“rencananya kita mau pakek <strong>Ansible</strong>, supaya proses <em>release</em> jadi lebih
enteng”. Segera setelah itu, (masih didepan <em>lead</em> saya) langsung saya buka
<em>browser</em> terus masukkin keyword “Ansible”. Haha, ternyata saya baru sadar
kalo saya hidup dalam “kardus”.</p>

<p>Gak lama kemudian <em>lead</em> saya langsung bilang “coba kamu cari-cari tentang
DevOps”. Oke, CuriousMode:On.</p>

<p>Saya mulai baca-baca dan, yup, sekarang mulai faham langkah-langkah bagaimana
proses dari mulai pembuatan aplikasi (coding) sampe ke tahap <em>packaging</em>
dan publishing (sekalipun cuma gambaran). Saya memang <em>programmer</em> baru,
tepatnya baru kerja.</p>

<p>Jujur saja, diawal saja saya kenal <a href="https://www.github.com">github</a> saya agak aneh, kenapa harus
pakai <em>github</em>? Kenapa gak <em>copy</em>-<em>paste</em> manual aja? Memang serepot apa sih
<em>versioning</em> (yang dulu saya menterjemahkan mentah kata ini) sampai harus
pake <em>tools</em> macam <em>github</em> segala? Sekarang saya mulai sadar dengan itu
semua, ya maklumlah dulu seringnya ngoding sendiri jadi ya sekarang perlahan
mulai faham apa yang membedakan antara <em>freelancer</em> dengan
<em>full stack programmer</em>, salah satunya ya penggunaan <em>tools</em> itu. <em>Freelancer</em>
lebih sering memilih <em>tools</em> yang memang ya berguna buat dia (kerja sendiri)
ketimbang <em>full stack programmer</em> yang sering kali milih <em>tools</em> untuk
kemudahan kolaborasi. Oke stop, balik kejalur pembahasan.</p>

<p>Saya jadi bingung, apa hubungannya cerita diatas sama judul?</p>

<p>Secara tidak langsung memang agak gak nyambung (bodo amat, blog saya ini),
yang pasti, <em>tools</em>-<em>tools</em> yang saya sebutkan diatas adalah tidak
lain salah satu dari <em>tools</em> untuk kolaborasi dalam rangka
<em>software development</em>. Dimana sebenarnya sering kali timbul banyak masalah
ketika kita ngoding bareng-bareng, entah itu pas proses penulisan kode,
atau bahkan pada saat proses mau rilis aplikasi. Dan diartikel ini saya coba
nulis apa yang ada dikepala saya soal proses rilis, terutama <em>issue</em> yang
sedang <em>“hot”</em> akhir-akhir ini diproses rilis tersebut. Langsung mulai deh…</p>

<h2 id="bagaiamana-proses-rilis-itu">Bagaiamana proses rilis itu?</h2>
<p>Sebelum saya nulis soal “apa itu <em>DevOps</em>?”, saya mau cerita soal bagaimana
proses rilis aplikasi itu secara garis besar (tentunya ditempat saya). Ini
penting karena nanti akan nyambung dengan judul diatas.</p>

<p>Yaiyalah, kalo gak nyambung ngapain ditulis, tulalit nih penulis.</p>

<p>Anggaplah sekarang kita sudah beres koding dengan keringat dingin mengucur
(seember – karena <em>deadline</em>) dan kita akan merilisnya, dan biasanya
berikut ini adalah langkah-langkah yang dilakukan:</p>

<ol>
  <li>Mindahin <em>source code</em> ke <em>server live</em>.</li>
  <li>Melakukan beberapa konfigurasi yang dibutuhkan.</li>
</ol>

<p>Kelihatannya <em>simple</em> ya?</p>

<p>Pada kenyataanya, sebelum mindahin kodingan ke <em>server live</em> biasanya ada
proses menjalankan semua <em>test</em> yang ada di <em>source code</em> tersebut. Kalo
sering ngelakuin <a href="https://en.wikipedia.org/wiki/Test-driven_development">TDD</a> mungkin gak aneh yah. Kalo hasil tesnya lancar ya
lanjut, kalo ada yang ‘bengkok’ ya dicek ulang, dibenerin dulu.</p>

<p>Proses mindahin kodingan dari tempat <em>development</em> ini juga beda-beda, tapi
kebanyakan sudah <em>clone</em> langsung dari <em>repository</em> ketimbang ngopi atau
ngupload kodingan secara manual lewat <a href="https://en.wikipedia.org/wiki/File_Transfer_Protocol">FTP</a>. Oke, tahap ini mungkin masih
dibilang gampang–lah. Sekalipun ya ribet juga harus login ke <em>server</em>,
terus ngejalanin <code class="language-plaintext highlighter-rouge">git clone</code> atau <code class="language-plaintext highlighter-rouge">git pull origin master</code>. Belum lagi kalo
lupa naro foldernya dimana, ato lupa servernya dimana (yang ini ngarang
biar ‘extreme’) kan bikin repot, tapi udah terlanjur bilang gampang, ya
gampangin aja lah.</p>

<p>Lanjut ke proses kedua, konfigurasi. Nah ini yang agak ribet. Misal sehabis
mindahin <em>source code</em> baru kita harus ngubah beberapa settingan kayak
<em>variable development</em> atau ngeset server jadi mode <em>maintenance</em> ketika
proses rilis ini, ato harus nyeting <em>cron job</em>, dll. Proses ini yang
sebenernya ribet, tapi ‘kelihatan’ gampang dan berulang-ulang, yang padahal
ngabisin waktu banyak, padahal misalnya hanya salah setting <em>cron job</em>, salah
masukin nama <em>file</em> misalnya.</p>

<p>Dari paragrap diatas sekarang mulai ada gambaran gimana proses rilis aplikasi
dari tempat <em>development</em> ke tempat laip (red: <em>live</em>). Dan ya agak ribet
juga, terlebih sebenarnya proses yang berulang-ulang seharusnya memang lebih
baik diotomatisasi biar meminimalisir kesalahan manusia.</p>

<h2 id="devops-itu-apa"><em>DevOps</em> itu apa?</h2>
<p>Sekarang kita baru ‘nginjek’ kata ini, <em>DevOps</em> sendiri menurut <a href="https://en.wikipedia.org/wiki/DevOps">wikipedia</a>
dikatakan campuran kata dari <em>Development</em> dan <em>Operations</em>. Dimana ini
nunjukin tugas-tugas yang berkaitan dengan proses <em>development</em> dan hal-hal
operasional lain atau staf IT lain, ya kayak situkang ngurusin <em>server</em> yang
tugasnya ngupload <em>file</em> kodingan kita ke <em>server live</em> tadi itu, atau
bahkan QA (Quality Assurance) – atau tukang tes.</p>

<h3 id="tugas-devops">Tugas <em>DevOps</em></h3>
<p>Dari pengertian singkat diatas sekarang setidaknya kita tahu bahwa tugas
<em>DevOps</em> itu ya jadi jembatan antara tukang koding, tukang tes, dan tukang
ngurus server. Kalo memang kita sering rilis secara berkala ya sebaiknya
memang <em>aware</em> sama pentingnya <em>DevOps</em> ini, terutama otomatisasi, karena
tentu saja kita nggak mau kalo harus melakukan tugas berulang-ulang yang
memang sebenarnya bisa diotomatisasi, yang pasti bisa menghemat waktu. Nah,
untuk beberapa <em>tools</em> yang berkaitan dengan <em>DevOps</em> ini ada listnya,
sebagai remainder saya juga sih, nih:</p>

<ul>
  <li><a href="http://jenkins-ci.org/">Jenkins</a></li>
  <li><a href="http://www.ansible.com/">Ansible</a></li>
  <li><a href="http://www.seleniumhq.org/">Selenium</a></li>
  <li><a href="https://www.docker.com/">Docker</a></li>
  <li><a href="https://www.atlassian.com/software/bamboo">Bamboo</a></li>
  <li><a href="https://travis-ci.org/">Travis CI</a></li>
  <li>dan lain-lain.</li>
</ul>

<p>Oke, rasanya sudah capek saya nulis. Capek baca nggak? Syukurdeh (mau capek
mau enggak tetep saya sukurin, haha…).</p>

<p>Sekian aja dulu tulisan tentang <em>DevOps</em>, mungkin postingan selanjutnya
saya bakalan nulis tentang tutorial ato hal-hal lain yang masih berkaitan
sama <em>DevOps</em> ini, soalnya memang lagi <em>curious</em> banget sama yang satu ini.</p>

<p>See you…</p>]]></content><author><name>AjiYakin</name></author><category term="DevOps" /><category term="DevOps" /><category term="Learning" /><summary type="html"><![CDATA[Awalnya saya agak mengkerutkan dahi ketika lead programmer saya bilang “rencananya kita mau pakek Ansible, supaya proses release jadi lebih enteng”. Segera setelah itu, (masih didepan lead saya) langsung saya buka browser terus masukkin keyword “Ansible”. Haha, ternyata saya baru sadar kalo saya hidup dalam “kardus”.]]></summary></entry></feed>