<?xml version="1.0"?>
	<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
		<channel>
			<title>Atabak - Thoughts and Experiences</title>
			<link>https://atabakkh.github.io</link>
			<description>Research papers and articles on cloud computing, DevOps, and software engineering</description>
			<language>en-us</language>
			<pubDate>Sun, 13 Sep 2026 14:54:30 UTC</pubDate>
			<lastBuildDate>Sun, 13 Sep 2026 14:54:30 UTC</lastBuildDate>
			<docs>http://cyber.law.harvard.edu/rss/rss.html</docs>
			<generator>http://github.com/thedereck/gh-pages-blog</generator>
			<atom:link href="https://atabakkh.github.io/rss.xml" rel="self" type="application/rss+xml" />

			
				<managingEditor>atabakkheirkhah@gmail.com</managingEditor>
			

			
				<webMaster>atabakkheirkhah@gmail.com</webMaster>
			

			
				<ttl>300</ttl>
			

			

			
				<item>
					
						<title>Your History Is Not a List: What I Learned Building a Time-Aware Sequential Recommender</title>
					
					<link>https://atabakkh.github.io/ai/2026/09/13/your-history-is-not-a-list.html</link>

					

					<pubDate>Sun, 13 Sep 2026 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/ai/2026/09/13/your-history-is-not-a-list</guid>

					
						<category><![CDATA[ AI ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>A static profile says what a user has liked, searched, viewed or bought. A sequence says how that interest evolved, and how recently. After a LinkedIn feed-ranking conversation, I ran a small synthetic PoC to test whether order and elapsed time actually add predictive signal.</em></p>

<p><strong>Context:</strong> In August 2026, Software Engineering Daily hosted LinkedIn VP of Engineering Tim Jurka on <a href="https://softwareengineeringdaily.com/podcasts/how-llms-are-reshaping-recommendation-systems/">How LLMs Are Reshaping Recommendation Systems</a>. The useful idea was not “replace the ranker with an LLM”. It was: treat the user’s path as a sequence, closer to next-token prediction than to a bag of interests. LinkedIn’s Feed Sequential Recommender (Feed SR) paper reports a <strong>2.10% increase in time spent</strong> in online A/B tests after replacing a DCNv2 ranker. I did not want to copy that architecture. I wanted a smaller, falsifiable question I could answer with code.</p>

<blockquote>
  <p><strong>Does preserving sequence and elapsed time improve next-topic prediction over simple popularity and static-preference baselines?</strong></p>
</blockquote>

<p>This article is that experiment. Synthetic data, chronological hold-out, five models, one shuffle control. Personal lab - not a production ranker.</p>

<hr />

<h2 id="the-experiment-in-one-picture">The experiment in one picture</h2>

<p><img src="/img/diagrams/sequential-rec-poc-flow.svg" alt="Static profile versus sequential path, and the five models compared in the PoC" width="750" /></p>

<p>Recommendation does not have to be framed only as <em>which item is most relevant to this user?</em> It can also be framed as <em>given the path this user has taken so far, what is most likely to be useful next?</em></p>

<p>That sounds like a small change in wording. It is not. Order changes meaning.</p>

<p>The PoC deliberately avoids Transformers at first. It compares five progressively more informed models:</p>

<ol>
  <li><strong>Global popularity</strong> - what is popular across everyone?</li>
  <li><strong>Personal frequency</strong> - what has this user historically consumed most?</li>
  <li><strong>Ordered transition</strong> - given the latest topic, what usually comes next?</li>
  <li><strong>Gap-aware sequential</strong> - given the latest topic <em>and the elapsed time</em>, what usually comes next?</li>
  <li><strong>Shuffled-order control</strong> - what happens if I intentionally destroy chronology?</li>
</ol>

<p>The fifth model is the real test. If the sequential model performs equally well after I shuffle each user’s history, I have not proved that sequence matters.</p>

<hr />

<h2 id="what-the-data-looks-like">What the data looks like</h2>

<p>The dataset is synthetic by design. I generated <strong>1,000 users and 26,020 timestamped interactions</strong> using a fixed seed, making repeated runs deterministic in my local implementation. Exact reproduction would also require publishing the generator and evaluation code.</p>

<p>Each event contains four fields: <code class="language-plaintext highlighter-rouge">user_id</code>, <code class="language-plaintext highlighter-rouge">timestamp</code>, <code class="language-plaintext highlighter-rouge">topic</code>, <code class="language-plaintext highlighter-rouge">action</code>.</p>

<p>Here is a sample journey for one generated user:</p>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Topic</th>
      <th>Action</th>
      <th>What the sequence suggests</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2026-01-08</td>
      <td>Containers</td>
      <td>View</td>
      <td>Early infrastructure interest</td>
    </tr>
    <tr>
      <td>2026-01-13</td>
      <td>Kubernetes</td>
      <td>View</td>
      <td>Moves deeper into orchestration</td>
    </tr>
    <tr>
      <td>2026-01-31</td>
      <td>Kubernetes</td>
      <td>View</td>
      <td>Reinforces the topic</td>
    </tr>
    <tr>
      <td>2026-02-05</td>
      <td>Platform Engineering</td>
      <td>View</td>
      <td>Broadens from technology to practice</td>
    </tr>
    <tr>
      <td>2026-02-06</td>
      <td>Kubernetes</td>
      <td>View</td>
      <td>Revisits a prerequisite</td>
    </tr>
    <tr>
      <td>2026-02-09</td>
      <td>Platform Engineering</td>
      <td>Like</td>
      <td>Stronger intent signal</td>
    </tr>
    <tr>
      <td>2026-02-10</td>
      <td>Internal Developer Platforms</td>
      <td>View</td>
      <td>Natural adjacent topic</td>
    </tr>
    <tr>
      <td>2026-02-13</td>
      <td>Platform Engineering</td>
      <td>Save</td>
      <td>High-interest action</td>
    </tr>
    <tr>
      <td>2026-02-17</td>
      <td>Internal Developer Platforms</td>
      <td>View</td>
      <td>Continues the journey</td>
    </tr>
    <tr>
      <td>2026-02-20</td>
      <td>Developer Experience</td>
      <td>View</td>
      <td>Moves toward the outcome of platform engineering</td>
    </tr>
    <tr>
      <td>2026-03-11</td>
      <td>Python</td>
      <td>Save</td>
      <td>Long gap, then a different local direction</td>
    </tr>
  </tbody>
</table>

<p>A static model sees a collection of topics and frequencies. A sequence-aware model sees something closer to a journey. The March 11 Python event also shows why time may matter: a transition after one day may mean “continue the current task”; a transition after three weeks may mean “the user has returned with a new goal.”</p>

<p><strong>Why synthetic data?</strong> I do not have a clean, large, publishable history of my own reading. Pretending that I do would make the story sound more personal and the experiment less honest. Synthetic data is appropriate for demonstrating the mechanism, evaluation design, and failure modes. It is <strong>not</strong> evidence that real users will produce the same gains.</p>

<h3 id="how-the-journeys-were-generated">How the journeys were generated</h3>

<p>The generator contains several plausible technical learning paths. One runs Cloud Fundamentals -&gt; Containers -&gt; Kubernetes -&gt; Platform Engineering -&gt; Internal Developer Platforms -&gt; Developer Experience -&gt; FinOps. Another runs Python -&gt; Data Engineering -&gt; Embeddings -&gt; Recommender Systems -&gt; Sequential Recommendation -&gt; LLM Ranking -&gt; Evaluation.</p>

<p>Users do not move perfectly from left to right. The generator allows repetition, revisiting earlier concepts, skipping ahead, exploration, and longer gaps. A perfectly deterministic sequence would make the experiment trivial.</p>

<hr />

<h2 id="evaluation-hide-the-future">Evaluation: hide the future</h2>

<p>For every user, interactions are sorted chronologically. The last event is held out as the test target. If the history is <code class="language-plaintext highlighter-rouge">A -&gt; B -&gt; C -&gt; D -&gt; E</code>, the model receives <code class="language-plaintext highlighter-rouge">A -&gt; B -&gt; C -&gt; D</code> and must rank <code class="language-plaintext highlighter-rouge">E</code> as highly as possible. For the gap-aware model, the held-out event’s timestamp represents the simulated recommendation-request time; the held-out topic and action remain hidden.</p>

<p>I use <strong>Hit@3</strong> as the primary metric: is the true next topic in the model’s top three predictions? I also report MRR@3 and NDCG@3. This is a per-user chronological split, not a global temporal cutoff. It prevents future events from the same user entering that user’s training history, but it does not model catalogue-wide changes over calendar time.</p>

<hr />

<h2 id="five-models">Five models</h2>

<p><strong>1. Global popularity</strong> recommends the same most-popular topics to everyone. Intentionally simple, and useful: a recommender that cannot beat popularity has not yet earned its complexity.</p>

<p><strong>2. Personal frequency</strong> knows the user, but not the order of the user’s history. It asks what topics this person has interacted with most often. That is a traditional static preference profile.</p>

<p><strong>3. Ordered transitions</strong> preserve chronology. For every adjacent pair of topics in the training histories, the model counts transitions and estimates <code class="language-plaintext highlighter-rouge">P(next_topic = j | current_topic = i)</code>. A first-order Markov recommender:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">user</span> <span class="ow">in</span> <span class="n">users</span><span class="p">:</span>
    <span class="n">history</span> <span class="o">=</span> <span class="n">sort_by_time</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">events</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">current</span><span class="p">,</span> <span class="n">nxt</span> <span class="ow">in</span> <span class="n">adjacent_pairs</span><span class="p">(</span><span class="n">history</span><span class="p">):</span>
        <span class="n">transitions</span><span class="p">[</span><span class="n">current</span><span class="p">][</span><span class="n">nxt</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</code></pre></div></div>

<p>At prediction time, the current topic becomes the context. No embeddings. No GPU. No attention. That simplicity is intentional.</p>

<p><strong>4. Gap-aware sequential</strong> adds a deliberately crude time signal. Each transition belongs to one of two buckets: short gap (<code class="language-plaintext highlighter-rouge">&lt;= 7</code> days) or long gap (<code class="language-plaintext highlighter-rouge">&gt; 7</code> days). The model therefore learns two versions of its transition patterns: <code class="language-plaintext highlighter-rouge">P(next | current, short_gap)</code> and <code class="language-plaintext highlighter-rouge">P(next | current, long_gap)</code>.</p>

<p>If I read about Kubernetes yesterday, platform engineering may be the natural continuation. If my last Kubernetes interaction was two months ago, the next topic may be driven more by a new goal than by the old session.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gap_days</span> <span class="o">=</span> <span class="p">(</span><span class="n">next_time</span> <span class="o">-</span> <span class="n">current_time</span><span class="p">).</span><span class="n">days</span>
<span class="n">bucket</span> <span class="o">=</span> <span class="s">"short"</span> <span class="k">if</span> <span class="n">gap_days</span> <span class="o">&lt;=</span> <span class="mi">7</span> <span class="k">else</span> <span class="s">"long"</span>
<span class="n">transitions</span><span class="p">[</span><span class="n">bucket</span><span class="p">][</span><span class="n">current</span><span class="p">][</span><span class="n">next_topic</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</code></pre></div></div>

<p>This is not sophisticated temporal modeling. It is a probe: <strong>does time add any signal at all?</strong></p>

<p><strong>5. Shuffled-order control</strong> keeps the same topics but shuffles their order before learning transitions. The resulting model still knows which topics co-occur in similar users. What it loses is the actual journey. If performance barely changes, chronology was not doing much work.</p>

<hr />

<h2 id="results">Results</h2>

<p>The deterministic run uses seed 42 and evaluates 1,000 held-out next events.</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th style="text-align: right">Hit@3</th>
      <th style="text-align: right">MRR@3</th>
      <th style="text-align: right">NDCG@3</th>
      <th style="text-align: right">95% bootstrap CI for Hit@3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Global popularity</td>
      <td style="text-align: right">0.560</td>
      <td style="text-align: right">0.455</td>
      <td style="text-align: right">0.482</td>
      <td style="text-align: right">0.529–0.590</td>
    </tr>
    <tr>
      <td>Personal frequency</td>
      <td style="text-align: right">0.670</td>
      <td style="text-align: right">0.556</td>
      <td style="text-align: right">0.585</td>
      <td style="text-align: right">0.640–0.700</td>
    </tr>
    <tr>
      <td>Ordered transition</td>
      <td style="text-align: right">0.794</td>
      <td style="text-align: right">0.684</td>
      <td style="text-align: right">0.712</td>
      <td style="text-align: right">0.768–0.819</td>
    </tr>
    <tr>
      <td><strong>Gap-aware sequential</strong></td>
      <td style="text-align: right"><strong>0.819</strong></td>
      <td style="text-align: right"><strong>0.693</strong></td>
      <td style="text-align: right"><strong>0.725</strong></td>
      <td style="text-align: right"><strong>0.795–0.842</strong></td>
    </tr>
    <tr>
      <td>Shuffled-order control</td>
      <td style="text-align: right">0.655</td>
      <td style="text-align: right">0.561</td>
      <td style="text-align: right">0.585</td>
      <td style="text-align: right">0.626–0.684</td>
    </tr>
  </tbody>
</table>

<p><img src="/img/diagrams/sequential-rec-results.svg" alt="Hit@3 comparison across popularity, frequency, shuffled, ordered, and gap-aware models" width="750" /></p>

<p>The absolute numbers are less important than the controlled differences.</p>

<p><strong>Static preference -&gt; sequence.</strong> Moving from personal frequency to ordered transitions increases Hit@3 from 0.670 to 0.794: <strong>+12.4 percentage points</strong>. The paired bootstrap 95% CI for the improvement is about +9.4 to +15.6 points. Inside this synthetic environment, knowing <em>what tends to follow what</em> adds substantial information beyond knowing <em>what the user tends to like</em>.</p>

<p><strong>Sequence -&gt; sequence + time.</strong> Adding the short/long gap feature increases Hit@3 from 0.794 to 0.819: another <strong>+2.5 percentage points</strong> (paired 95% CI about +1.2 to +3.8). That prevents an exaggerated conclusion. The experiment does <strong>not</strong> say that time is the dominant factor. It says:</p>

<blockquote>
  <p><strong>Order carries most of the gain. A crude time signal adds a smaller incremental gain.</strong></p>
</blockquote>

<p>That is a more interesting result than forcing the article to support a “time-aware AI changes everything” headline.</p>

<h3 id="the-strongest-evidence-shuffle-the-history">The strongest evidence: shuffle the history</h3>

<p>The gap-aware sequential model achieves Hit@3 = 0.819. The shuffled-order control achieves 0.655. The difference is <strong>+16.4 percentage points</strong> (95% CI: +13.5 to +19.3).</p>

<p>The shuffled model is not useless. Related topics still co-occur in the same users, so it retains some signal. But when chronology is destroyed, a large amount of predictive power disappears. That is the core finding of this PoC.</p>

<h3 id="a-concrete-time-aware-example">A concrete time-aware example</h3>

<p>One generated user had this recent history:</p>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Topic</th>
      <th>Action</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2026-02-14</td>
      <td>FinOps</td>
      <td>View</td>
    </tr>
    <tr>
      <td>2026-02-15</td>
      <td>FinOps</td>
      <td>Save</td>
    </tr>
    <tr>
      <td>2026-02-18</td>
      <td>Cost Optimization</td>
      <td>View</td>
    </tr>
    <tr>
      <td>2026-02-22</td>
      <td>Cost Optimization</td>
      <td>Like</td>
    </tr>
    <tr>
      <td>2026-02-25</td>
      <td>Autoscaling</td>
      <td>View</td>
    </tr>
    <tr>
      <td>2026-03-01</td>
      <td>FinOps</td>
      <td>Save</td>
    </tr>
    <tr>
      <td>2026-03-07</td>
      <td>FinOps</td>
      <td>Like</td>
    </tr>
    <tr>
      <td>2026-03-10</td>
      <td>Cost Optimization</td>
      <td>View</td>
    </tr>
    <tr>
      <td><strong>2026-03-24</strong></td>
      <td><strong>Observability</strong></td>
      <td><strong>Save - held-out target</strong></td>
    </tr>
  </tbody>
</table>

<p>Notice the <strong>14-day gap</strong> before the held-out target.</p>

<p>Personal frequency and ordinary transitions both ranked FinOps, Cost Optimization, Autoscaling. The gap-aware model ranked FinOps, SLOs, Observability. It still did not rank the correct topic first, but it moved it into the top three.</p>

<p>That is the kind of behavior the time feature is intended to capture: after a longer break, the local transition pattern can differ from the immediate-session pattern. One example does not prove the model is generally better. The aggregate evaluation above is what matters. The example makes the mechanism easier to see.</p>

<hr />

<h2 id="why-not-a-transformer-first">Why not a Transformer first</h2>

<p>It would have been easy to build a tiny SASRec-style model and make this article look more advanced. That would have weakened the experiment. Before adding a high-capacity architecture, I want to know whether simple signals already explain most of the result.</p>

<p>This question is especially relevant now. A 2026 Spotify Research paper, <a href="https://arxiv.org/abs/2608.19833">Do Sequential Recommendation Benchmarks Really Require Higher-Order Sequence Modelling?</a>, evaluates strong recency-weighted pairwise methods against Transformer-based sequential recommenders. On several commonly used benchmarks, the simpler probes match or outperform reproduced Transformer baselines. MovieLens-20M is a clear exception, where the Transformer retains a substantial lead.</p>

<p>The lesson is not “Transformers are unnecessary.”</p>

<p>The lesson is:</p>

<blockquote>
  <p><strong>A complex model should have to beat strong simple baselines that already capture popularity, recency, and pairwise transitions.</strong></p>
</blockquote>

<p>That is why this PoC begins with a Markov-style transition model rather than a neural network. If a future Transformer beats it materially on the same chronological evaluation, then we have evidence that higher-order context contributes something real.</p>

<hr />

<h2 id="what-this-poc-shows---and-what-it-does-not">What this PoC shows - and what it does not</h2>

<p><strong>What it demonstrates.</strong> Within the generated dataset: a static personal profile beats global popularity; preserving chronology beats the static profile; adding a simple elapsed-time feature improves the sequential model slightly; destroying chronology significantly reduces performance.</p>

<p>That means the setup isolates three different kinds of signal: preference, plus sequence, plus time.</p>

<p><strong>What it does not prove.</strong> The dataset deliberately contains sequential structure. A sequence-aware model <em>should</em> discover it. The measured gains - 12.4 points, 16.4 points, or any other number - must <strong>not</strong> be presented as expected production uplift.</p>

<p>Real recommendation systems are harder: users have multiple simultaneous interests; items are dynamic and often new; actions have different intent strengths; candidate retrieval and ranking are separate problems; relevance is only one objective. Diversity, freshness, safety, creator quality, fairness, and long-term value also matter. Offline metrics do not guarantee online product gains.</p>

<p>The honest conclusion is narrower:</p>

<blockquote>
  <p><strong>The PoC demonstrates a method for testing whether chronology and time carry predictive signal. The next step is to run the same ablation on real interaction logs.</strong></p>
</blockquote>

<hr />

<h2 id="from-poc-to-a-real-architecture">From PoC to a real architecture</h2>

<p>A production system would be much larger than this experiment, but the conceptual layers remain understandable:</p>

<p><img src="/img/diagrams/sequential-rec-architecture.svg" alt="Layers from interaction events through sequential ranking to a policy-aware feed" width="750" /></p>

<p>At small scale, the “sequential ranker” could literally be the transition model used here. At large scale, that component might become a Transformer or another history encoder. The long-term profile, context features, policy rules, and product objectives can then be fused into the final ranking decision.</p>

<p>The architecture can become complex. The measurement principle should remain simple:</p>

<blockquote>
  <p>Does the new model beat strong static, recency, and transition baselines on chronological offline evaluation - and then survive an online experiment?</p>
</blockquote>

<hr />

<h2 id="what-i-would-build-next">What I would build next</h2>

<p>The next step should <strong>not</strong> be “add more layers.” I would proceed in this order:</p>

<ol>
  <li>Replace synthetic histories with real chronological events</li>
  <li>Weight actions differently: view, then like, then save</li>
  <li>Use several recent history items with recency decay</li>
  <li>Add context such as device, session, or time of day if relevant</li>
  <li>Build a SASRec-style or Transformer model</li>
  <li>Compare it against the strongest simple sequential baseline</li>
  <li>Only then consider production serving and online testing</li>
</ol>

<p>The third step is particularly important. Instead of conditioning only on the latest topic, a stronger non-neural model can aggregate evidence from several recent interactions:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>score(candidate) =
    w1 * P(candidate | last_item)
  + w2 * P(candidate | item_before_last)
  + w3 * P(candidate | older_item)

w1 &gt; w2 &gt; w3
</code></pre></div></div>

<p>That produces a much stronger test for whether a Transformer is learning genuine higher-order structure rather than simply reproducing recency-weighted pairwise statistics.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>I started this experiment because of an LLM discussion. The useful lesson is not really about LLMs. It is about representation.</p>

<p>A user profile represented as <code class="language-plaintext highlighter-rouge">{Kubernetes, AI, FinOps, Platform Engineering}</code> is not equivalent to Kubernetes -&gt; Platform Engineering -&gt; Developer Experience -&gt; FinOps. The first says <strong>what exists in the history</strong>. The second also says <strong>how the history evolved</strong>. When timestamps are included, we can begin to distinguish an active journey from an old preference.</p>

<p>The sentence I would keep from this entire PoC:</p>

<blockquote>
  <p><strong>A user’s history is not only a list of preferences. It is an ordered path through changing intent.</strong></p>
</blockquote>

<p>The experiment is still small and synthetic. That is fine. A PoC should not pretend to be production evidence. What it gives me is something more useful than a copied architecture: a falsifiable method.</p>

<p>Preserve the sequence. Measure it. Destroy the sequence. Measure again. Add time. Measure again. Then increase model complexity only when the data justifies it.</p>

<p>That is the part I would carry into a real recommendation system.</p>

<hr />

<h2 id="references">References</h2>

<ul>
  <li>Software Engineering Daily, <a href="https://softwareengineeringdaily.com/podcasts/how-llms-are-reshaping-recommendation-systems/">How LLMs Are Reshaping Recommendation Systems</a>, 18 August 2026</li>
  <li>Hertel et al., <a href="https://arxiv.org/abs/2602.12354">An Industrial-Scale Sequential Recommender for LinkedIn Feed Ranking</a>, arXiv:2602.12354, 2026</li>
  <li>Petrov et al., <a href="https://arxiv.org/abs/2608.19833">Do Sequential Recommendation Benchmarks Really Require Higher-Order Sequence Modelling?</a>, arXiv:2608.19833, 2026</li>
  <li>Spotify Research, <a href="https://github.com/spotify-research/sequential-capacity-probes">sequential-capacity-probes</a></li>
</ul>

<p><em>PoC details: 1,000 synthetic users, 26,020 interactions, chronological leave-last-event-out evaluation, seed 42, Hit@3 / MRR@3 / NDCG@3, and bootstrap confidence intervals. The data and results are synthetic and should be interpreted as a demonstration of methodology rather than expected production uplift.</em></p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>A Platform Without Ownership Becomes a Graveyard</title>
					
					<link>https://atabakkh.github.io/cloud/2026/06/18/platform-without-ownership-graveyard.html</link>

					

					<pubDate>Thu, 18 Jun 2026 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2026/06/18/platform-without-ownership-graveyard</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>Pipelines still ran. Dashboards still loaded. Nobody deleted anything, fixed definitions, or admitted the mart nobody used cost thousands a month. That was a graveyard - not a platform.</em></p>

<p><strong>Context:</strong> By mid-2026 many enterprises had years of cloud data investment and dozens of AI experiments - and the same failure mode kept showing up: no clear ownership for data products. I have seen this as much as a leadership problem as an architecture problem.</p>

<hr />

<h2 id="how-graveyards-form">How graveyards form</h2>

<p>Migration ends; SI leaves; runbooks thin; bus factor is one. Hero engineer moves teams; knowledge walks out. “Shared responsibility” - everyone assumes someone else owns freshness. New initiatives layer on: shadow marts, duplicate pipelines, more indexes. Cost grows quietly; nobody deletes because nobody will sign the deletion.</p>

<p>The platform looks alive. Accountability is dead.</p>

<hr />

<h2 id="symptoms">Symptoms</h2>

<p>Incidents start with “who owns this table?” Same metric built three ways with three numbers. Pipelines green while quality checks failed for weeks. Access requests go to a generic mailing list. Decommission dies in committee. AI teams rebuild customer 360 for every project.</p>

<hr />

<h2 id="ownership-is-capacity-to-change-not-blame">Ownership is capacity to change, not blame</h2>

<p><img src="/img/diagrams/data-product-ownership.svg" alt="Data product ownership roles" width="750" /></p>

<table>
  <thead>
    <tr>
      <th>Role</th>
      <th>Accountable for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Business owner</td>
      <td>Definitions, priority, acceptable delay, telling consumers</td>
    </tr>
    <tr>
      <td>Technical owner</td>
      <td>Pipeline health, cost, schema evolution, runbooks</td>
    </tr>
    <tr>
      <td>Platform steward</td>
      <td>Standards, contracts registry, shared tooling, escalation</td>
    </tr>
  </tbody>
</table>

<p>One person can wear multiple hats in a small team. Zero people wearing them is the graveyard pattern.</p>

<p>Contract fragment I pushed for on critical tables:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">dataset</span><span class="pi">:</span> <span class="s">curated.orders</span>
<span class="na">owners</span><span class="pi">:</span>
  <span class="na">business</span><span class="pi">:</span> <span class="s">order-management</span>
  <span class="na">technical</span><span class="pi">:</span> <span class="s">data-platform-core</span>
<span class="na">consumers</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">bi_revenue_dashboard</span>
  <span class="pi">-</span> <span class="s">support_copilot</span>   <span class="c1"># automated consumer = ownership risk</span>
<span class="na">decommission_candidate</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">monthly_cost_eur_est</span><span class="pi">:</span> <span class="m">4200</span>
</code></pre></div></div>

<hr />

<h2 id="what-i-push-for-in-the-leadership-seat">What I push for in the leadership seat</h2>

<p>Name owners before publishing datasets. No owner -&gt; not production for critical tiers.</p>

<p>Measure cost and usage per domain. Unused assets with cost are decommission candidates - with owner approval.</p>

<p>Treat decommissioning as a success metric. Reward turning things off after cutover.</p>

<p>On-call follows the data product for critical paths - same seriousness as customer-facing APIs.</p>

<p>When agents depend on a table, ownership includes that automated consumer risk. Put it in the contract.</p>

<hr />

<h2 id="cleaning-a-graveyard-without-stopping-the-business">Cleaning a graveyard without stopping the business</h2>

<p><strong>Month 1:</strong> Inventory - datasets, last access, monthly cost, named owner or <code class="language-plaintext highlighter-rouge">NONE</code>.</p>

<p><strong>Month 2:</strong> Stabilize ownership on the top revenue/compliance products first.</p>

<p><strong>Month 3:</strong> Merge duplicate marts; delete unused staging with sign-off; document lineage.</p>

<p><strong>Ongoing:</strong> No new production without owner fields. Platform gates the deploy.</p>

<p>Unglamorous. Also what separates platforms that scale AI from platforms that accumulate debt.</p>

<hr />

<h2 id="link-to-the-technical-leader-trap">Link to the technical leader trap</h2>

<p>Senior engineers often become the implicit owner of everything. Feels helpful. Creates a graveyard in one person’s head.</p>

<p>Leadership means refusing to be the permanent owner, making ownership visible and rewarded, escalating unowned critical assets, building standards so ownership is lighter - not heavier. Solving everything yourself until you are the bottleneck is the other side of the same problem.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>A platform without ownership becomes a graveyard: running pipelines, stale trust, rising cost, nobody accountable for change. Naming owners, measuring usage and cost, decommissioning on purpose, treating data products like production services - that is what helped.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Why AI Projects Fail Between Prototype and Production</title>
					
					<link>https://atabakkh.github.io/ai/2026/05/07/ai-projects-fail-prototype-to-production.html</link>

					

					<pubDate>Thu, 07 May 2026 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/ai/2026/05/07/ai-projects-fail-prototype-to-production</guid>

					
						<category><![CDATA[ AI ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>The demo worked. The pilot dashboard looked green. Then production asked for SLAs, cost predictability, audit trails, and the same answer on Tuesday that you got on Monday.</em></p>

<p><strong>Context:</strong> By spring 2026, the industry story was pilots to production. The graveyard I kept seeing was full of prototypes that never crossed the gap - not because the model was wrong, but because the operating system around the model was never built.</p>

<hr />

<h2 id="the-prototype-trap">The prototype trap</h2>

<p><img src="/img/diagrams/prototype-to-production-gates.svg" alt="Prototype to production gates" width="750" /></p>

<p>Prototypes optimize for visible magic: hand-picked examples, clean indexes, tolerant users, no formal SLO or cost ceiling, manual cleanup when outputs look bad.</p>

<p>Production optimizes for reliability under variance: messy data, bad inputs, regulated and revenue-critical decisions, FinOps and audit, users who trust confident wrong answers.</p>

<p>Skills overlap partially. Infrastructure and discipline overlap less than teams expect.</p>

<hr />

<h2 id="seven-failure-modes-in-the-gap">Seven failure modes in the gap</h2>

<p><strong>1. No production definition of “good enough”.</strong> Pilot: “users liked it.” Production needs measured time saved, error/override rate, cost per task, p95 latency - with kill criteria. Without metrics you cannot tell if v2 is better.</p>

<p><strong>2. Freshness and scope drift.</strong> Prototype index built once. Production tables and docs change hourly. Answers go wrong silently. Freshness SLAs, re-index pipelines, <code class="language-plaintext highlighter-rouge">as_of</code> in context.</p>

<p><strong>3. Cost curves that do not scale.</strong> Demo: 50 queries/day. Production: 50,000 with retrieval, re-ranking, multi-step agents. Cost per successful outcome, budgets, caching, right-time tiers, route by complexity.</p>

<p><strong>4. Missing fallbacks.</strong> Model or retrieval fails -&gt; errors or hallucinations. Deterministic fallbacks, kill switches, degraded UX - tested, not improvised.</p>

<p><strong>5. Security added late.</strong> Broad access “to move fast”. Agents compound exposure. Least privilege, retrieval boundaries, approval tiers.</p>

<p><strong>6. No ownership after handoff.</strong> Data science built it; ops inherits it; data platform was never in the room. Named owners for model, inputs, runtime.</p>

<p><strong>7. Change management ignored.</strong> Users do not know when to trust, when to override, what changed in v2. Release notes, confidence cues, feedback loops.</p>

<hr />

<h2 id="gate-i-would-use-before-go-live">Gate I would use before go-live</h2>

<table>
  <thead>
    <tr>
      <th>Gate</th>
      <th>Requirement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Data</td>
      <td>Contracts + freshness SLAs on inputs</td>
    </tr>
    <tr>
      <td>Quality</td>
      <td>Golden-set eval; regression on prompt/model change</td>
    </tr>
    <tr>
      <td>Reliability</td>
      <td>p95 within tier; fallback tested</td>
    </tr>
    <tr>
      <td>Cost</td>
      <td>Projected monthly cost at expected volume; ceilings</td>
    </tr>
    <tr>
      <td>Security</td>
      <td>Access review; audit logs; residency</td>
    </tr>
    <tr>
      <td>Operations</td>
      <td>Runbook, on-call owner, kill switch drill</td>
    </tr>
    <tr>
      <td>Governance</td>
      <td>Risk tier; approvals for high-risk actions</td>
    </tr>
  </tbody>
</table>

<p>Fail a critical row -&gt; stay in bounded pilot. Do not call it production.</p>

<hr />

<h2 id="pattern-that-worked">Pattern that worked</h2>

<p>From personal lab copilot experiments and common enterprise patterns:</p>

<ol>
  <li>One narrow workflow with clear baseline metrics</li>
  <li>Right-time freshness - not fake real-time</li>
  <li>Strict success gates - retire what does not pass</li>
  <li>Kill switch - prove reversibility</li>
  <li>Platform fixes in parallel - not “after AI succeeds”</li>
</ol>

<p>Production AI is systems integration. The model is one component.</p>

<hr />

<h2 id="after-demo-applause">After demo applause</h2>

<p>Words I used:</p>

<blockquote>
  <p>The demo proved the workflow is worth testing. Production needs the same gates as customer-facing services: data ownership, cost ceiling, fallback, audit. Here is the 8-week path and what we will not do until gates pass.</p>
</blockquote>

<p>Red flags: warehouse-wide access for the agent; no business owner for definitions; success metric is “delight” with no baseline; platform team learns about the project from security review.</p>

<p>What helped organizationally: single intake with risk tiers; platform + product + security sign-off for production; explicit retire list so failed pilots stop and budget returns to the platform.</p>

<p>Leaders who only celebrate demos inherit incidents. Leaders who gate honestly build durable capability.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>AI projects fail between prototype and production when teams ship demos into operational reality without SLOs, cost models, fallbacks, governance, and ownership. Explicit production gates closed that gap - not another hackathon.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Will AI Replace Engineers or Expose Weak Engineering?</title>
					
					<link>https://atabakkh.github.io/agile/2026/04/15/will-ai-replace-engineers-or-expose-weak-engineering.html</link>

					

					<pubDate>Wed, 15 Apr 2026 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/agile/2026/04/15/will-ai-replace-engineers-or-expose-weak-engineering</guid>

					
						<category><![CDATA[ Agile ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>Everyone is talking about AI. The real question is whether your team builds systems that last - or relies on a few heroes, knowledge nobody wrote down, and demos that fall apart in production.</em></p>

<p><strong>Context:</strong> By spring 2026, boards were asking engineering leaders about headcount, coding assistants, and whether they were “AI-native”. Engineers worried about staying relevant. Both miss the real point: AI makes strong engineering stronger, and weak engineering easier to see.</p>

<p><img src="/img/diagrams/ai-exposes-engineering.svg" alt="AI as a stress test of weak versus strong engineering" width="750" /></p>

<hr />

<h2 id="what-ai-was-actually-good-at-in-engineering-orgs">What AI was actually good at in engineering orgs</h2>

<p>Boilerplate and scaffolding. Explaining unfamiliar code. Drafting tests, docs, SQL, Terraform. Faster exploration and PoCs. Summarizing incidents and logs.</p>

<p>That compresses local task time - especially for people who already know what “good” looks like.</p>

<hr />

<h2 id="what-it-did-not-replace">What it did not replace</h2>

<p>Accountability for production outcomes. Trade-off judgment under business constraints. Cross-team negotiation - contracts, priorities, decommissioning. Trust repair after an incident. Knowing what <em>not</em> to build. Operating messy legacy while migrating safely.</p>

<p>Orgs pay for judgment under uncertainty, not token output.</p>

<hr />

<h2 id="weak-engineering-it-exposed">Weak engineering it exposed</h2>

<table>
  <thead>
    <tr>
      <th>Weakness</th>
      <th>How it showed up</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>No data contracts</td>
      <td>Every copilot rebuilds joins; answers disagree</td>
    </tr>
    <tr>
      <td>No ownership</td>
      <td>Agents query tables nobody maintains</td>
    </tr>
    <tr>
      <td>No SLOs</td>
      <td>“AI was wrong” with no freshness evidence</td>
    </tr>
    <tr>
      <td>Hero culture</td>
      <td>Copilot helps juniors; seniors still bottleneck</td>
    </tr>
    <tr>
      <td>Demo-driven delivery</td>
      <td>Impressive prototype, no production gate</td>
    </tr>
    <tr>
      <td>Governance in slides</td>
      <td>Agents with excessive access</td>
    </tr>
  </tbody>
</table>

<p>AI did not create these problems. It raised the price of ignoring them.</p>

<p>Quick “is this platform ready for a pilot?” score I used in conversations. Tick the row if it is true today:</p>

<table>
  <thead>
    <tr>
      <th>Gate</th>
      <th>Ready?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Written data contracts on the tables the copilot will read</td>
      <td> </td>
    </tr>
    <tr>
      <td>Freshness SLAs, not only “the job is green”</td>
      <td> </td>
    </tr>
    <tr>
      <td>Named business and technical owners</td>
      <td> </td>
    </tr>
    <tr>
      <td>Kill switch / deterministic fallback</td>
      <td> </td>
    </tr>
    <tr>
      <td>Cost ceiling per task or per day</td>
      <td> </td>
    </tr>
    <tr>
      <td>Eval set with a baseline (time, errors, overrides)</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>Score under 4 -&gt; lab only. 4–5 -&gt; bounded pilot. 6 -&gt; discuss a production gate. Do not skip the empty rows because a demo looked good.</p>

<hr />

<h2 id="strong-engineering-it-amplified">Strong engineering it amplified</h2>

<p>Serving layers and contracts - retrieval and features cheaper to consume. Runbooks and tests. Platform standards so suggestions match house patterns. Measurement culture - eval harnesses and cost per task. Distributed ownership - fewer “only Maria knows” bottlenecks.</p>

<p>Mature platforms turn AI into leverage. Immature ones turn it into noise at scale.</p>

<hr />

<h2 id="what-i-saw-leaders-change-their-mind-on">What I saw leaders change their mind on</h2>

<p>Stop asking only: how many engineers can we cut with AI?</p>

<p>Start asking: where does tacit knowledge block delivery? Which platform gaps make every AI project bespoke? What production gates do pilots skip? How do we measure value (time saved, errors, cost) - not demo applause? Which workflows should not be automated yet?</p>

<p>Headcount planning without platform maturity is guesswork.</p>

<hr />

<h2 id="what-i-tell-individual-contributors">What I tell individual contributors</h2>

<p>I would not compete with the model on typing speed. What mattered more: system design and failure modes, data and domain understanding, production operation, clear writing and decision records, mentoring and review quality.</p>

<p>The engineer who only typed fast was already replaceable - by offshore, contractors, or templates. AI changes the margin, not the fundamentals.</p>

<hr />

<h2 id="where-i-would-still-invest">Where I would still invest</h2>

<table>
  <thead>
    <tr>
      <th>Invest</th>
      <th>Why</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Platform and data quality</td>
      <td>Makes AI outputs trustworthy</td>
    </tr>
    <tr>
      <td>Guardrails and governance</td>
      <td>Makes automation survivable</td>
    </tr>
    <tr>
      <td>Product thinking in platform teams</td>
      <td>Connects tech to outcomes</td>
    </tr>
    <tr>
      <td>Leadership paths for senior ICs</td>
      <td>Keeps judgment without forcing management</td>
    </tr>
    <tr>
      <td>Honest kill criteria for pilots</td>
      <td>Stops zombie demos</td>
    </tr>
  </tbody>
</table>

<p>Cutting platform investment while buying AI tools is the expensive path.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>AI will not replace engineers who build accountable, operable systems. It will expose teams that relied on heroics, ambiguous data, and demo culture. Treat it as a stress test of engineering maturity - and fix the platform and ownership problems it reveals.</p>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>From Technical Expert to Technical Leader: What Actually Changes</title>
					
					<link>https://atabakkh.github.io/agile/2026/03/20/from-expert-to-technical-leader.html</link>

					

					<pubDate>Fri, 20 Mar 2026 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/agile/2026/03/20/from-expert-to-technical-leader</guid>

					
						<category><![CDATA[ Agile ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>The promotion did not come with a new toolchain. It came with ambiguity, stakeholders who disagree, and problems that do not fit in an IDE.</em></p>

<p><strong>Context:</strong> In 2026 many orgs created staff engineer and technical lead paths without clear definitions. When I moved into those roles the question was always: what actually changes on Monday? Notes from platform, data, and cloud leadership - not generic management theory.</p>

<hr />

<h2 id="what-does-not-change">What does not change</h2>

<p>You still need technical judgment. You still read code, query plans, incident timelines. Credibility comes from understanding the work. If you stop being technically credible, good engineers route around you.</p>

<hr />

<h2 id="what-actually-changed-for-me">What actually changed for me</h2>

<p><strong>Calendar became the product.</strong> Experts optimize focus for deep work. Leaders optimize throughput: who is blocked, which decision is stuck, which risk is invisible upstairs, which standard would kill ten Slack threads. If 80% of the calendar is meetings, they need to produce owners, dates, and written decisions - not only “alignment”.</p>

<p><strong>Communication is a deliverable.</strong> Expert: “Partition pruning fails on this query.” Leader: “Finance dashboard may breach SLA; owner is data-platform; fix or workaround by Thursday; impact if numbers are wrong.” Same fact. Different accountability.</p>

<p><strong>Team graph, not call graph.</strong> Success is no longer “I shipped the fix”. It is: runbook used without you; junior owns a production change with review; two teams stop arguing because a contract exists; incident repeat rate drops.</p>

<p><strong>Durable over perfect.</strong> Can we operate this at 3am? Can another team extend it without calling us? Can we reverse it? Does ownership survive my next vacation?</p>

<p><strong>Translate both ways.</strong> Upward: risk into cost, revenue, compliance, timeline. Downward: business pressure into prioritized constraints, not vague urgency. Failure mode is absorbing anxiety from both sides until you burn out. Pass clarity, not stress.</p>

<hr />

<h2 id="skills-i-had-to-practice-on-purpose">Skills I had to practice on purpose</h2>

<p>Written decisions (one-page ADRs). Facilitation with a named decider. Delegation with owner + checkpoint + authority. Hiring and mentoring so someone else can take your old tasks. Saying no with trade-offs written down. Enough financial literacy to talk cost-to-serve and ROI of platform work.</p>

<p>No MBA required. Reps required.</p>

<hr />

<h2 id="mistakes-in-the-first-year">Mistakes in the first year</h2>

<p>Still competing with the team on coding speed. Avoiding ownership and priority fights. Over-engineering standards nobody reads - one useful runbook beats a 40-page manifesto. Under-communicating to executives (surprises destroy trust faster than bad news told early). Identity crisis: “Am I still an engineer?” Yes - with a broader definition of impact.</p>

<hr />

<h2 id="weekly-rhythm-that-helped">Weekly rhythm that helped</h2>

<p><img src="/img/diagrams/expert-to-leader-shift.svg" alt="Weekly rhythm after moving from expert to technical leader" width="750" /></p>

<table>
  <thead>
    <tr>
      <th>When</th>
      <th>Do this</th>
      <th>Do not do this</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Monday</td>
      <td>Review SLO, cost, incident trends</td>
      <td>Ignore silent quality drift</td>
    </tr>
    <tr>
      <td>Midweek</td>
      <td>Unblock one cross-team decision</td>
      <td>Implement it yourself</td>
    </tr>
    <tr>
      <td>Friday</td>
      <td>Write one artifact (ADR, runbook, contract)</td>
      <td>Leave decisions only in Slack</td>
    </tr>
    <tr>
      <td>Monthly</td>
      <td>1:1s on growth</td>
      <td>Status meetings that belong in tools</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="questions-for-your-manager">Questions for your manager</h2>

<p>What outcomes define success for me in six months? Which decisions are mine vs shared? What should I stop doing that I did as an IC? How will you measure team health, not only dates?</p>

<p>If they cannot answer, negotiate clarity before accepting infinite responsibility.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Expert -&gt; technical leader means primary output becomes clarity, ownership, standards, and leverage - not personal execution speed. Still engineering. The interface is human and organizational as much as technical.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>AI Governance Starts in the Architecture, Not in PowerPoint</title>
					
					<link>https://atabakkh.github.io/ai/2026/02/12/ai-governance-starts-in-architecture.html</link>

					

					<pubDate>Thu, 12 Feb 2026 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/ai/2026/02/12/ai-governance-starts-in-architecture</guid>

					
						<category><![CDATA[ AI ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>Governance decks showed up after the first incident. By then the architecture had already decided what agents could read, what was logged, and what could not be undone.</em></p>

<p><strong>Context:</strong> Early 2026 AI governance stopped feeling theoretical in organizations I watched. Audit pressure and production agents pushed the same conclusion: controls have to live in systems, not only in policy PDFs.</p>

<hr />

<h2 id="the-gap-i-kept-seeing">The gap I kept seeing</h2>

<p><img src="/img/diagrams/governance-in-architecture.svg" alt="Governance embedded in architecture layers" width="750" /></p>

<p>Most orgs had a policy committee, a principles page, a review for “high-risk” use cases. Fewer had enforced access on retrieval indexes, immutable audit for automated decisions, model and data version pins, or controls that actually block a deploy when violated.</p>

<p>Governance you cannot verify in code and logs is theatre.</p>

<hr />

<h2 id="controls-that-worked-when-they-were-in-the-architecture">Controls that worked when they were in the architecture</h2>

<p><strong>Classification drives behavior.</strong> Tag datasets and documents (<code class="language-plaintext highlighter-rouge">public_internal</code>, <code class="language-plaintext highlighter-rouge">confidential</code>, <code class="language-plaintext highlighter-rouge">regulated</code>, <code class="language-plaintext highlighter-rouge">pii</code>). Pipelines and agents read those tags for indexes, models, human review, retention. A spreadsheet does not count.</p>

<p><strong>Lineage for every production output.</strong> Model id + version, prompt/template version, source ids + versions, <code class="language-plaintext highlighter-rouge">as_of</code>, user or service identity. Store in queryable tables so audit can reconstruct in minutes - not only scattered logs.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- illustrative lineage / inference log (personal lab style)</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="n">ai</span><span class="p">.</span><span class="n">inference_events</span> <span class="p">(</span>
  <span class="n">event_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">ts</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="n">workflow</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">model_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">model_version</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">prompt_template_version</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">source_dataset_ids</span> <span class="n">ARRAY</span><span class="o">&lt;</span><span class="n">STRING</span><span class="o">&gt;</span><span class="p">,</span>
  <span class="n">as_of</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="n">user_or_sa</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">outcome</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">cost_eur</span> <span class="nb">NUMERIC</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">workflow</span><span class="p">,</span> <span class="n">outcome</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Access as code.</strong> IAM, row filters, masks, retrieval scopes in version-controlled config. Avoid: “the agent uses a service account that can read most of the warehouse.”</p>

<p><strong>Human-in-the-loop by risk tier.</strong> Read-only with logging. Draft/suggest with sampling review. Acts on systems of record need an approval gate. Forbidden actions blocked at the tool layer. Risk tier is a property of the workflow, not a meeting outcome.</p>

<p><strong>Eval and drift after launch.</strong> Golden-set regression on model/prompt changes. Sampled production quality. Alert when override rate or policy blocks spike. Kill switch drill on a schedule.</p>

<hr />

<h2 id="questions-auditors-ask---answers-architecture-should-already-have">Questions auditors ask - answers architecture should already have</h2>

<table>
  <thead>
    <tr>
      <th>Question</th>
      <th>Artifact</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>What data was used?</td>
      <td>Lineage + retrieval log</td>
    </tr>
    <tr>
      <td>Where is a user’s data retained, and how is each copy deleted?</td>
      <td>Deletion map covering sources, indexes, logs, caches, outputs, and backups</td>
    </tr>
    <tr>
      <td>Who approved this automation?</td>
      <td>Workflow config + approval audit</td>
    </tr>
    <tr>
      <td>Cross-border processing?</td>
      <td>Region-pinned infra + residency tags</td>
    </tr>
    <tr>
      <td>Model behavior change?</td>
      <td>Version pins + regression gates</td>
    </tr>
  </tbody>
</table>

<p>Engineers do not replace legal. They make compliance operable.</p>

<hr />

<h2 id="anti-patterns">Anti-patterns</h2>

<p>Governance review after production launch. One global “AI platform admin”. Logging prompts but not retrieved content hashes. Trusting a vendor “enterprise AI” badge without your own controls. Separate AI team with no data platform accountability.</p>

<hr />

<h2 id="a-30-day-sequence-i-saw-work">A 30-day sequence I saw work</h2>

<p>Week 1: classify datasets used by live or planned AI workflows.<br />
Week 2: lineage fields on inference and retrieval logs.<br />
Week 3: risk tiers + approval gate for one high-risk workflow.<br />
Week 4: kill-switch drill + audit reconstruction exercise.</p>

<p>Small verifiable steps beat a twelve-month policy program.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>AI governance belongs in architecture: classification, lineage, access, approval tiers, monitoring, kill switches. Principles on slides are necessary. Enforced system behavior is what keeps you out of the incident report.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Human Risk, Fast AI, Slow Thinking: Innovation, Greed, and the Quiet Risk to Our Data</title>
					
					<link>https://atabakkh.github.io/ai/2025/11/30/fast-ai-slow-thinking-data-security.html</link>

					

					<pubDate>Sun, 30 Nov 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/ai/2025/11/30/fast-ai-slow-thinking-data-security</guid>

					
						<category><![CDATA[ AI ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p>When I was doing my PhD, I spent years building computational models for cancer detection. It sounded glamorous from the outside - algorithms, prediction, science. In reality it was a constant confrontation with risk and of course data as bigggg considration.</p>

<p>The data I worked with was deeply personal: gene expression profiles, clinical records, diagnostic outcomes. If my models were wrong, real people paid the price. A “good enough” model was not good enough. We had to think about:</p>

<ul>
  <li>How the data was collected</li>
  <li>How it was stored and anonymised</li>
  <li>How predictions could be misinterpreted</li>
  <li>What could happen if something leaked</li>
</ul>

<p>This mindset is exactly what I miss today when I look at how many companies are “doing AI”.</p>

<p>Right now, the narrative is simple and brutal:</p>

<p><strong>Move fast, plug in AI everywhere, and worry about the details later.</strong></p>

<p>From my perspective, that’s not innovation. That’s negligence dressed up as progress.</p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ol>
  <li><a href="#background-the-data-leakage-crisis">Background: The Data Leakage Crisis</a></li>
  <li><a href="#the-problem-greedy-ai-adoption">The Problem: Greedy AI Adoption</a></li>
  <li><a href="#how-data-really-leaks-through-ai">How Data Really Leaks Through AI</a></li>
  <li><a href="#methodology-a-risk-based-framework">Methodology: A Risk-Based Framework</a></li>
  <li><a href="#implementation-five-step-secure-ai-architecture">Implementation: Five-Step Secure AI Architecture</a></li>
  <li><a href="#technical-controls-and-architecture">Technical Controls and Architecture</a></li>
  <li><a href="#governance-and-policy-framework">Governance and Policy Framework</a></li>
  <li><a href="#case-studies-and-real-world-examples">Case Studies and Real-World Examples</a></li>
  <li><a href="#conclusion-ai-with-discipline">Conclusion: AI with Discipline</a></li>
</ol>

<hr />

<h2 id="background-the-data-leakage-crisis">Background: The Data Leakage Crisis</h2>

<h3 id="the-everyday-ai-that-quietly-eats-your-data">The Everyday AI That Quietly Eats Your Data</h3>

<p>Let’s be honest about what’s actually happening inside every companies.</p>

<p>Employees are under pressure to deliver more, faster. They discover a large language model that can summarise, generate, and debug in seconds ( and honestly not eveyone underestand right know what are you doing???? ). So they start doing what feels natural:</p>

<ul>
  <li>Paste internal documents into chatbots to “summarise this for me”</li>
  <li>Paste source code and logs to “help me debug this”</li>
  <li>Paste HR policies, contracts, even customer data to “make this clearer”</li>
</ul>

<p>Nobody is trying to be malicious. People are just trying to survive their workday.</p>

<p>The problem is simple:</p>

<p><strong>The moment that data leaves your controlled environment and lands in a third-party AI tool, you’ve lost control.</strong></p>

<p>You don’t really know:</p>

<ul>
  <li>How long the data is stored</li>
  <li>How big the data would stored</li>
  <li>what data is going to be stored</li>
  <li>Who exactly can access it</li>
  <li>Whether it might be used for training</li>
  <li>How it might resurface in another context</li>
  <li>….</li>
</ul>

<p>In my PhD world, that level of uncertainty would have killed any project immediately. In today’s corporate world, it often doesn’t even stop a pilot.</p>

<h3 id="the-scale-of-the-problem">The Scale of the Problem</h3>

<p>Recent studies and incidents reveal the scope:</p>

<ul>
  <li><strong>2023</strong>: Samsung engineers leaked proprietary code via ChatGPT</li>
  <li><strong>2024</strong>: Multiple healthcare organizations exposed patient data through AI tools</li>
  <li><strong>Ongoing</strong>: Thousands of companies have no policy on AI data usage</li>
</ul>

<p>The pattern is consistent: well-intentioned employees, no guardrails, predictable outcomes.</p>

<hr />

<h2 id="the-problem-greedy-ai-adoption-vs-responsible-use">The Problem: Greedy AI Adoption vs. Responsible Use</h2>

<p>We can talk about ethics and principles, but let’s be direct: <strong>a lot of AI adoption is driven by greed and fear.</strong></p>

<h3 id="the-drivers">The Drivers</h3>

<ul>
  <li><strong>Greed for valuation</strong> - “We are now an AI company” is cheaper than actually fixing your product</li>
  <li><strong>Greed for KPIs</strong> - More users, more engagement, lower cost per ticket</li>
  <li><strong>Fear of missing out</strong> - “If we don’t do it, our competitors will”</li>
</ul>

<p>In this context, “AI strategy” often means:</p>

<ol>
  <li>Add AI to the landing page</li>
  <li>Integrate with the first big LLM API available</li>
  <li>Hope the security team signs off later</li>
</ol>

<p>This might “work” short term, especially in startups desperate for growth. But structurally it creates a fragile system:</p>

<ul>
  <li>No data classification</li>
  <li>No clear policy on PII</li>
  <li>No guardrails against employees leaking sensitive information</li>
  <li>No real incident response plan if something goes wrong</li>
</ul>

<p>You end up with a strange paradox:</p>

<p><strong>Companies shout about “AI transformation” while quietly gambling with the data that keeps them alive.</strong></p>

<h3 id="human-safety-and-company-safety-are-not-opposites">Human Safety and Company Safety Are Not Opposites</h3>

<p>There’s a dangerous myth that human safety and business interests are in tension.</p>

<p>From what I’ve seen on both sides - research and industry - the opposite is true:</p>

<ul>
  <li>If you misuse personal data, you lose customer trust</li>
  <li>If your AI behaves unpredictably in critical domains (health, finance, hiring), regulators and courts will eventually come</li>
  <li>If your internal knowledge, code, or strategy leaks through AI tools, your competitive advantage evaporates</li>
</ul>

<p>Protecting people and protecting the company are <strong>the same problem</strong> with different time horizons.</p>

<p>In my PhD project, we could not hide behind “we are just experimenting”. We had to assume:</p>
<ul>
  <li>Someone might act on these predictions.</li>
  <li>Someone might be harmed by them.</li>
  <li>Someone will ask us to defend our methods.</li>
</ul>

<p>Companies deploying AI at scale should assume exactly the same.</p>

<hr />

<h2 id="how-data-really-leaks-through-ai">How Data Really Leaks Through AI</h2>

<p>Most leaks don’t look like a Hollywood hack. They look like normal work.</p>

<h3 id="the-four-main-leakage-vectors">The Four Main Leakage Vectors</h3>

<h4 id="1-accidental-pasting">1. Accidental Pasting</h4>

<p><strong>Pattern:</strong></p>
<ul>
  <li>A developer pastes proprietary code into a chatbot</li>
  <li>A manager pastes an internal strategy deck for “summarisation”</li>
  <li>A recruiter pastes a list of candidates with full details to write “better outreach emails”</li>
</ul>

<p><strong>Why it happens:</strong></p>
<ul>
  <li>No clear policy on what’s allowed</li>
  <li>No technical controls preventing it</li>
  <li>Pressure to deliver quickly</li>
</ul>

<h4 id="2-shadow-tools">2. Shadow Tools</h4>

<p><strong>Pattern:</strong></p>
<ul>
  <li>Browser extensions, plugins, or unofficial AI helpers that quietly send snippets of screen content to external services</li>
  <li>Personal accounts used for “quick help” on work documents</li>
</ul>

<p><strong>Why it happens:</strong></p>
<ul>
  <li>IT doesn’t control all tools employees use</li>
  <li>Personal devices and accounts blur boundaries</li>
  <li>No monitoring of data egress</li>
</ul>

<h4 id="3-training-data-contamination">3. Training Data Contamination</h4>

<p><strong>Pattern:</strong></p>
<ul>
  <li>Scraped datasets that include PII or confidential documents</li>
  <li>Models later regurgitate pieces of those documents when probed in specific ways</li>
</ul>

<p><strong>Why it happens:</strong></p>
<ul>
  <li>Training data sources not vetted</li>
  <li>No data minimization in model training</li>
  <li>Models retain more information than expected</li>
</ul>

<h4 id="4-misconfiguration-and-bugs">4. Misconfiguration and Bugs</h4>

<p><strong>Pattern:</strong></p>
<ul>
  <li>Wrong access settings, test environments made public</li>
  <li>Shared logs that contain prompts and responses</li>
  <li>API keys exposed in code repositories</li>
</ul>

<p><strong>Why it happens:</strong></p>
<ul>
  <li>Rush to deploy</li>
  <li>No security review process</li>
  <li>Default settings are permissive</li>
</ul>

<p>None of this is spectacular. It’s routine. That’s exactly why it’s so dangerous.</p>

<h3 id="data-leakage-risk-matrix">Data Leakage Risk Matrix</h3>

<p><img src="/img/datleake.png" width="750" height="250" /></p>

<hr />

<h2 id="methodology-a-risk-based-framework">Methodology: A Risk-Based Framework</h2>

<p>If a company genuinely wants to adopt AI without playing roulette with data and trust, it needs something more mature than “let’s try this API”.</p>

<h3 id="core-principles">Core Principles</h3>

<ol>
  <li><strong>Data Classification First</strong> - Know what you have before you protect it</li>
  <li><strong>Risk-Based Approach</strong> - Not all AI features are equal</li>
  <li><strong>Defense in Depth</strong> - Technical controls + policy + training</li>
  <li><strong>Measurable Outcomes</strong> - Track incidents, near misses, trust metrics</li>
  <li><strong>Reversibility</strong> - Kill switches and fallback mechanisms</li>
</ol>

<h3 id="the-framework-structure">The Framework Structure</h3>

<p><img src="/img/framework.png" width="750" height="250" /></p>

<hr />

<h2 id="implementation-five-step-secure-ai-architecture">Implementation: Five-Step Secure AI Architecture</h2>

<p>A minimal, workable approach can be broken into five steps.</p>

<h3 id="step-1-classify-your-data-first">Step 1: Classify Your Data First</h3>

<p>Before touching AI, be explicit:</p>

<p><strong>Data Classification Levels:</strong></p>

<ul>
  <li><strong>Public</strong> - Can safely be sent anywhere (marketing copy, public documentation)</li>
  <li><strong>Internal Non-Sensitive</strong> - Inconvenient if leaked, but survivable (internal process docs, non-confidential policies)</li>
  <li><strong>Confidential</strong> - Source code, architecture, contracts, internal strategy</li>
  <li><strong>PII / Highly Sensitive</strong> - Customer data, HR data, health data, financial records</li>
</ul>

<p><strong>Clear Rules:</strong></p>

<ul>
  <li><strong>Public</strong>: OK with external AI tools</li>
  <li><strong>Internal Non-Sensitive</strong>: Only with approved tools and vendors</li>
  <li><strong>Confidential &amp; PII</strong>: Never to public AI; only via controlled, private models with contracts and technical safeguards</li>
</ul>

<p>If a company can’t answer “what data do we actually have and how sensitive is it?”, then it has no business deploying AI on top of it.</p>

<h3 id="step-2-classify-your-ai-use-cases-by-risk">Step 2: Classify Your AI Use Cases by Risk</h3>

<p>Not all AI features are equal. A marketing text generator and an AI system deciding who gets a loan do not belong in the same risk category.</p>

<p><strong>Risk Assessment Matrix:</strong></p>

<p>Define two dimensions:</p>

<p><img src="/img/dimention.png" width="750" height="250" /></p>

<ul>
  <li><strong>Data Sensitivity</strong> (low / high)</li>
  <li><strong>Human &amp; Business Impact</strong> (low / high)</li>
</ul>

<p>You then get four quadrants:</p>

<p><strong>Decision Framework:</strong></p>

<ul>
  <li><strong>Low sensitivity, low impact</strong>  –&gt; “Experiment freely, with basic hygiene”</li>
  <li><strong>High sensitivity or high impact</strong>  –&gt; “Formal assessment, documented controls, human oversight, and very careful rollout”</li>
</ul>

<p>Anything high-impact + high-sensitivity should be treated like a medical model: audited, tested, explainable, and reversible.</p>

<h3 id="step-3-design-ai-architecture-with-controls-not-just-features">Step 3: Design AI Architecture With Controls, Not Just Features</h3>

<p>A secure AI setup is more than “user  –&gt; LLM API”.</p>

<p>You need an internal layer that:</p>

<ul>
  <li>Filters and redacts sensitive data where possible (data minimisation)</li>
  <li>Enforces policies (“no PII to external models”)</li>
  <li>Logs all requests and responses for audit and incident analysis</li>
  <li>Routes sensitive workloads to private / VPC-isolated models</li>
</ul>

<p>Most companies go wrong by integrating AI <em>at the edge</em> (direct from browser to vendor). You want AI behind an <strong>internal gateway</strong> that you control.</p>

<h3 id="step-4-set-policy-ownership-and-training">Step 4: Set Policy, Ownership, and Training</h3>

<p>People are not going to guess the right behaviour. You must be explicit.</p>

<p><strong>Required Elements:</strong></p>

<ul>
  <li>A clear <strong>AI usage policy</strong>: what tools are allowed, forbidden, and under what constraints</li>
  <li>A clear <strong>owner</strong> (or committee) for AI risk and governance</li>
  <li>Regular <strong>training</strong> for employees: what is PII, what counts as confidential, concrete examples of “do” and “don’t”</li>
</ul>

<p>If AI is “everyone’s job” but nobody is accountable, risk will accumulate silently until something breaks.</p>

<h3 id="step-5-monitor-learn-and-treat-incidents-as-feedback">Step 5: Monitor, Learn, and Treat Incidents as Feedback</h3>

<p>Assume mistakes. Then build for them.</p>

<p><strong>Monitoring Requirements:</strong></p>

<ul>
  <li>Monitor AI usage and look for abnormal patterns</li>
  <li>Have a basic <strong>incident response plan</strong>: who is involved, what gets shut down, how you communicate</li>
  <li>After each incident or near miss, update policies and controls, not just the slide deck</li>
</ul>

<p>Companies that pretend “nothing bad will happen” with AI are signalling that they have no realistic understanding of how technology fails in the real world.</p>

<hr />

<h2 id="technical-controls-and-architecture">Technical Controls and Architecture</h2>

<h3 id="secure-ai-data-flow-architecture">Secure AI Data Flow Architecture</h3>

<p><img src="/img/data flow.png" width="750" height="250" /></p>

<h3 id="implementation-example-ai-gateway-service">Implementation Example: AI Gateway Service</h3>

<p><strong>Architecture Components:</strong></p>

<ol>
  <li><strong>API Gateway</strong> - Routes all AI requests</li>
  <li><strong>Data Loss Prevention (DLP)</strong> - Scans and redacts sensitive data</li>
  <li><strong>Policy Engine</strong> - Enforces data classification rules</li>
  <li><strong>Audit Logger</strong> - Records all AI interactions</li>
  <li><strong>Routing Layer</strong> - Directs requests to appropriate AI provider</li>
</ol>

<p><strong>Example Implementation (Python/FastAPI):</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
AI Gateway Service - Secure routing and policy enforcement
"""</span>
<span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span><span class="p">,</span> <span class="n">HTTPException</span><span class="p">,</span> <span class="n">Request</span>
<span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">Any</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>

<span class="c1"># Data classification
</span><span class="k">class</span> <span class="nc">DataClassification</span><span class="p">:</span>
    <span class="n">PUBLIC</span> <span class="o">=</span> <span class="s">"public"</span>
    <span class="n">INTERNAL</span> <span class="o">=</span> <span class="s">"internal"</span>
    <span class="n">CONFIDENTIAL</span> <span class="o">=</span> <span class="s">"confidential"</span>
    <span class="n">PII</span> <span class="o">=</span> <span class="s">"pii"</span>

<span class="c1"># DLP Service
</span><span class="k">class</span> <span class="nc">DLPService</span><span class="p">:</span>
    <span class="s">"""Data Loss Prevention - detects and redacts sensitive data"""</span>
    
    <span class="k">def</span> <span class="nf">classify_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">content</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="s">"""Classify data sensitivity level"""</span>
        <span class="c1"># PII detection patterns
</span>        <span class="n">pii_patterns</span> <span class="o">=</span> <span class="p">[</span>
            <span class="sa">r</span><span class="s">'\b\d{3}-\d{2}-\d{4}\b'</span><span class="p">,</span>  <span class="c1"># SSN
</span>            <span class="sa">r</span><span class="s">'\b\d{16}\b'</span><span class="p">,</span>  <span class="c1"># Credit card
</span>            <span class="sa">r</span><span class="s">'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'</span><span class="p">,</span>  <span class="c1"># Email
</span>        <span class="p">]</span>
        
        <span class="kn">import</span> <span class="nn">re</span>
        <span class="k">for</span> <span class="n">pattern</span> <span class="ow">in</span> <span class="n">pii_patterns</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">re</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">pattern</span><span class="p">,</span> <span class="n">content</span><span class="p">):</span>
                <span class="k">return</span> <span class="n">DataClassification</span><span class="p">.</span><span class="n">PII</span>
        
        <span class="c1"># Confidential keywords
</span>        <span class="n">confidential_keywords</span> <span class="o">=</span> <span class="p">[</span><span class="s">'proprietary'</span><span class="p">,</span> <span class="s">'confidential'</span><span class="p">,</span> <span class="s">'internal strategy'</span><span class="p">]</span>
        <span class="k">if</span> <span class="nb">any</span><span class="p">(</span><span class="n">keyword</span> <span class="ow">in</span> <span class="n">content</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">confidential_keywords</span><span class="p">):</span>
            <span class="k">return</span> <span class="n">DataClassification</span><span class="p">.</span><span class="n">CONFIDENTIAL</span>
        
        <span class="k">return</span> <span class="n">DataClassification</span><span class="p">.</span><span class="n">PUBLIC</span>
    
    <span class="k">def</span> <span class="nf">redact_sensitive</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">content</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">classification</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="s">"""Redact sensitive data based on classification"""</span>
        <span class="k">if</span> <span class="n">classification</span> <span class="o">==</span> <span class="n">DataClassification</span><span class="p">.</span><span class="n">PII</span><span class="p">:</span>
            <span class="c1"># Redact PII patterns
</span>            <span class="kn">import</span> <span class="nn">re</span>
            <span class="n">content</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s">'\b\d{3}-\d{2}-\d{4}\b'</span><span class="p">,</span> <span class="s">'[SSN-REDACTED]'</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
            <span class="n">content</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s">'\b\d{16}\b'</span><span class="p">,</span> <span class="s">'[CARD-REDACTED]'</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
            <span class="n">content</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">sub</span><span class="p">(</span>
                <span class="sa">r</span><span class="s">'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'</span><span class="p">,</span>
                <span class="s">'[EMAIL-REDACTED]'</span><span class="p">,</span>
                <span class="n">content</span>
            <span class="p">)</span>
        <span class="k">return</span> <span class="n">content</span>

<span class="c1"># Policy Engine
</span><span class="k">class</span> <span class="nc">PolicyEngine</span><span class="p">:</span>
    <span class="s">"""Enforces AI usage policies based on data classification"""</span>
    
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">rules</span> <span class="o">=</span> <span class="p">{</span>
            <span class="n">DataClassification</span><span class="p">.</span><span class="n">PUBLIC</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"allowed_providers"</span><span class="p">:</span> <span class="p">[</span><span class="s">"openai"</span><span class="p">,</span> <span class="s">"anthropic"</span><span class="p">,</span> <span class="s">"google"</span><span class="p">],</span>
                <span class="s">"requires_approval"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
            <span class="p">},</span>
            <span class="n">DataClassification</span><span class="p">.</span><span class="n">INTERNAL</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"allowed_providers"</span><span class="p">:</span> <span class="p">[</span><span class="s">"openai-enterprise"</span><span class="p">,</span> <span class="s">"anthropic-enterprise"</span><span class="p">],</span>
                <span class="s">"requires_approval"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
            <span class="p">},</span>
            <span class="n">DataClassification</span><span class="p">.</span><span class="n">CONFIDENTIAL</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"allowed_providers"</span><span class="p">:</span> <span class="p">[</span><span class="s">"private-vpc-model"</span><span class="p">],</span>
                <span class="s">"requires_approval"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
                <span class="s">"requires_audit"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
            <span class="p">},</span>
            <span class="n">DataClassification</span><span class="p">.</span><span class="n">PII</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"allowed_providers"</span><span class="p">:</span> <span class="p">[</span><span class="s">"private-vpc-model"</span><span class="p">],</span>
                <span class="s">"requires_approval"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
                <span class="s">"requires_audit"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
                <span class="s">"requires_encryption"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
            <span class="p">}</span>
        <span class="p">}</span>
    
    <span class="k">def</span> <span class="nf">check_policy</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">classification</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">provider</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
        <span class="s">"""Check if request complies with policy"""</span>
        <span class="k">if</span> <span class="n">classification</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">rules</span><span class="p">:</span>
            <span class="k">return</span> <span class="p">{</span><span class="s">"allowed"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span> <span class="s">"reason"</span><span class="p">:</span> <span class="s">"Unknown classification"</span><span class="p">}</span>
        
        <span class="n">rule</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">rules</span><span class="p">[</span><span class="n">classification</span><span class="p">]</span>
        
        <span class="k">if</span> <span class="n">provider</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">rule</span><span class="p">[</span><span class="s">"allowed_providers"</span><span class="p">]:</span>
            <span class="k">return</span> <span class="p">{</span>
                <span class="s">"allowed"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
                <span class="s">"reason"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"Provider </span><span class="si">{</span><span class="n">provider</span><span class="si">}</span><span class="s"> not allowed for </span><span class="si">{</span><span class="n">classification</span><span class="si">}</span><span class="s"> data"</span>
            <span class="p">}</span>
        
        <span class="k">return</span> <span class="p">{</span>
            <span class="s">"allowed"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
            <span class="s">"requires_approval"</span><span class="p">:</span> <span class="n">rule</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"requires_approval"</span><span class="p">,</span> <span class="bp">False</span><span class="p">),</span>
            <span class="s">"requires_audit"</span><span class="p">:</span> <span class="n">rule</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"requires_audit"</span><span class="p">,</span> <span class="bp">False</span><span class="p">),</span>
            <span class="s">"requires_encryption"</span><span class="p">:</span> <span class="n">rule</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"requires_encryption"</span><span class="p">,</span> <span class="bp">False</span><span class="p">),</span>
        <span class="p">}</span>

<span class="c1"># Audit Logger
</span><span class="k">class</span> <span class="nc">AuditLogger</span><span class="p">:</span>
    <span class="s">"""Logs all AI interactions for compliance and incident response"""</span>
    
    <span class="k">def</span> <span class="nf">log_request</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
        <span class="n">classification</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
        <span class="n">provider</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
        <span class="n">input_size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">output_size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">timestamp</span><span class="p">:</span> <span class="n">datetime</span><span class="p">,</span>
        <span class="n">policy_result</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">],</span>
    <span class="p">):</span>
        <span class="s">"""Log AI request to audit system"""</span>
        <span class="n">log_entry</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"timestamp"</span><span class="p">:</span> <span class="n">timestamp</span><span class="p">.</span><span class="n">isoformat</span><span class="p">(),</span>
            <span class="s">"user_id"</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span>
            <span class="s">"data_classification"</span><span class="p">:</span> <span class="n">classification</span><span class="p">,</span>
            <span class="s">"provider"</span><span class="p">:</span> <span class="n">provider</span><span class="p">,</span>
            <span class="s">"input_size_bytes"</span><span class="p">:</span> <span class="n">input_size</span><span class="p">,</span>
            <span class="s">"output_size_bytes"</span><span class="p">:</span> <span class="n">output_size</span><span class="p">,</span>
            <span class="s">"policy_allowed"</span><span class="p">:</span> <span class="n">policy_result</span><span class="p">[</span><span class="s">"allowed"</span><span class="p">],</span>
            <span class="s">"policy_reason"</span><span class="p">:</span> <span class="n">policy_result</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"reason"</span><span class="p">),</span>
        <span class="p">}</span>
        
        <span class="c1"># In production: write to secure audit log (e.g., BigQuery, CloudWatch)
</span>        <span class="n">logging</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">"AI_AUDIT: </span><span class="si">{</span><span class="n">log_entry</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        
        <span class="c1"># Example: Write to database or SIEM
</span>        <span class="c1"># audit_db.insert(log_entry)
</span>
<span class="c1"># Main Gateway Service
</span><span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"AI Gateway"</span><span class="p">,</span> <span class="n">version</span><span class="o">=</span><span class="s">"1.0.0"</span><span class="p">)</span>

<span class="n">dlp_service</span> <span class="o">=</span> <span class="n">DLPService</span><span class="p">()</span>
<span class="n">policy_engine</span> <span class="o">=</span> <span class="n">PolicyEngine</span><span class="p">()</span>
<span class="n">audit_logger</span> <span class="o">=</span> <span class="n">AuditLogger</span><span class="p">()</span>

<span class="k">class</span> <span class="nc">AIRequest</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">content</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">provider</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"openai"</span>
    <span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span>

<span class="k">class</span> <span class="nc">AIResponse</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">result</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">classification</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">redacted</span><span class="p">:</span> <span class="nb">bool</span>
    <span class="n">policy_compliant</span><span class="p">:</span> <span class="nb">bool</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="s">"/ai/generate"</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">AIResponse</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">generate_ai</span><span class="p">(</span><span class="n">request</span><span class="p">:</span> <span class="n">AIRequest</span><span class="p">):</span>
    <span class="s">"""Secure AI generation endpoint"""</span>
    
    <span class="c1"># Step 1: Classify data
</span>    <span class="n">classification</span> <span class="o">=</span> <span class="n">dlp_service</span><span class="p">.</span><span class="n">classify_data</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">content</span><span class="p">)</span>
    
    <span class="c1"># Step 2: Check policy
</span>    <span class="n">policy_result</span> <span class="o">=</span> <span class="n">policy_engine</span><span class="p">.</span><span class="n">check_policy</span><span class="p">(</span><span class="n">classification</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">provider</span><span class="p">)</span>
    
    <span class="k">if</span> <span class="ow">not</span> <span class="n">policy_result</span><span class="p">[</span><span class="s">"allowed"</span><span class="p">]:</span>
        <span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span>
            <span class="n">status_code</span><span class="o">=</span><span class="mi">403</span><span class="p">,</span>
            <span class="n">detail</span><span class="o">=</span><span class="sa">f</span><span class="s">"Policy violation: </span><span class="si">{</span><span class="n">policy_result</span><span class="p">[</span><span class="s">'reason'</span><span class="p">]</span><span class="si">}</span><span class="s">"</span>
        <span class="p">)</span>
    
    <span class="c1"># Step 3: Redact if needed
</span>    <span class="n">redacted_content</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">content</span>
    <span class="n">redacted</span> <span class="o">=</span> <span class="bp">False</span>
    <span class="k">if</span> <span class="n">classification</span> <span class="ow">in</span> <span class="p">[</span><span class="n">DataClassification</span><span class="p">.</span><span class="n">PII</span><span class="p">,</span> <span class="n">DataClassification</span><span class="p">.</span><span class="n">CONFIDENTIAL</span><span class="p">]:</span>
        <span class="n">redacted_content</span> <span class="o">=</span> <span class="n">dlp_service</span><span class="p">.</span><span class="n">redact_sensitive</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">content</span><span class="p">,</span> <span class="n">classification</span><span class="p">)</span>
        <span class="n">redacted</span> <span class="o">=</span> <span class="n">redacted_content</span> <span class="o">!=</span> <span class="n">request</span><span class="p">.</span><span class="n">content</span>
    
    <span class="c1"># Step 4: Route to appropriate provider
</span>    <span class="c1"># In production: call actual AI provider API
</span>    <span class="n">result</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"[AI Response for </span><span class="si">{</span><span class="n">classification</span><span class="si">}</span><span class="s"> data]"</span>
    
    <span class="c1"># Step 5: Audit log
</span>    <span class="n">audit_logger</span><span class="p">.</span><span class="n">log_request</span><span class="p">(</span>
        <span class="n">user_id</span><span class="o">=</span><span class="n">request</span><span class="p">.</span><span class="n">user_id</span><span class="p">,</span>
        <span class="n">classification</span><span class="o">=</span><span class="n">classification</span><span class="p">,</span>
        <span class="n">provider</span><span class="o">=</span><span class="n">request</span><span class="p">.</span><span class="n">provider</span><span class="p">,</span>
        <span class="n">input_size</span><span class="o">=</span><span class="nb">len</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">content</span><span class="p">.</span><span class="n">encode</span><span class="p">()),</span>
        <span class="n">output_size</span><span class="o">=</span><span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">encode</span><span class="p">()),</span>
        <span class="n">timestamp</span><span class="o">=</span><span class="n">datetime</span><span class="p">.</span><span class="n">utcnow</span><span class="p">(),</span>
        <span class="n">policy_result</span><span class="o">=</span><span class="n">policy_result</span><span class="p">,</span>
    <span class="p">)</span>
    
    <span class="k">return</span> <span class="n">AIResponse</span><span class="p">(</span>
        <span class="n">result</span><span class="o">=</span><span class="n">result</span><span class="p">,</span>
        <span class="n">classification</span><span class="o">=</span><span class="n">classification</span><span class="p">,</span>
        <span class="n">redacted</span><span class="o">=</span><span class="n">redacted</span><span class="p">,</span>
        <span class="n">policy_compliant</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="kn">import</span> <span class="nn">uvicorn</span>
    <span class="n">uvicorn</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="s">"0.0.0.0"</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">8080</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="data-classification-automation">Data Classification Automation</h3>

<p><strong>Example: Automated Classification Service</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Automated data classification using ML and pattern matching
"""</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span><span class="p">,</span> <span class="n">Tuple</span>
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>

<span class="o">@</span><span class="n">dataclass</span>
<span class="k">class</span> <span class="nc">ClassificationResult</span><span class="p">:</span>
    <span class="n">level</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">confidence</span><span class="p">:</span> <span class="nb">float</span>
    <span class="n">matched_patterns</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
    <span class="n">recommendations</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>

<span class="k">class</span> <span class="nc">AutoClassifier</span><span class="p">:</span>
    <span class="s">"""Automatically classify data sensitivity"""</span>
    
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">pii_patterns</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"ssn"</span><span class="p">:</span> <span class="sa">r</span><span class="s">'\b\d{3}-\d{2}-\d{4}\b'</span><span class="p">,</span>
            <span class="s">"credit_card"</span><span class="p">:</span> <span class="sa">r</span><span class="s">'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b'</span><span class="p">,</span>
            <span class="s">"email"</span><span class="p">:</span> <span class="sa">r</span><span class="s">'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'</span><span class="p">,</span>
            <span class="s">"phone"</span><span class="p">:</span> <span class="sa">r</span><span class="s">'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b'</span><span class="p">,</span>
            <span class="s">"ip_address"</span><span class="p">:</span> <span class="sa">r</span><span class="s">'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b'</span><span class="p">,</span>
        <span class="p">}</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">confidential_keywords</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s">"proprietary"</span><span class="p">,</span> <span class="s">"confidential"</span><span class="p">,</span> <span class="s">"internal use only"</span><span class="p">,</span>
            <span class="s">"trade secret"</span><span class="p">,</span> <span class="s">"nda"</span><span class="p">,</span> <span class="s">"non-disclosure"</span>
        <span class="p">]</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">code_indicators</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s">"def "</span><span class="p">,</span> <span class="s">"function "</span><span class="p">,</span> <span class="s">"class "</span><span class="p">,</span> <span class="s">"import "</span><span class="p">,</span> <span class="s">"package "</span><span class="p">,</span>
            <span class="s">"SELECT"</span><span class="p">,</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="s">"CREATE TABLE"</span>
        <span class="p">]</span>
    
    <span class="k">def</span> <span class="nf">classify</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">content</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ClassificationResult</span><span class="p">:</span>
        <span class="s">"""Classify content and return result with confidence"""</span>
        <span class="n">matched</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">recommendations</span> <span class="o">=</span> <span class="p">[]</span>
        
        <span class="c1"># Check for PII
</span>        <span class="n">pii_found</span> <span class="o">=</span> <span class="bp">False</span>
        <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">pattern</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">pii_patterns</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
            <span class="k">if</span> <span class="n">re</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">pattern</span><span class="p">,</span> <span class="n">content</span><span class="p">,</span> <span class="n">re</span><span class="p">.</span><span class="n">IGNORECASE</span><span class="p">):</span>
                <span class="n">matched</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s">"PII: </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
                <span class="n">pii_found</span> <span class="o">=</span> <span class="bp">True</span>
        
        <span class="k">if</span> <span class="n">pii_found</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">ClassificationResult</span><span class="p">(</span>
                <span class="n">level</span><span class="o">=</span><span class="n">DataClassification</span><span class="p">.</span><span class="n">PII</span><span class="p">,</span>
                <span class="n">confidence</span><span class="o">=</span><span class="mf">0.95</span><span class="p">,</span>
                <span class="n">matched_patterns</span><span class="o">=</span><span class="n">matched</span><span class="p">,</span>
                <span class="n">recommendations</span><span class="o">=</span><span class="p">[</span>
                    <span class="s">"Never send to public AI models"</span><span class="p">,</span>
                    <span class="s">"Use private VPC model only"</span><span class="p">,</span>
                    <span class="s">"Require encryption in transit and at rest"</span>
                <span class="p">]</span>
            <span class="p">)</span>
        
        <span class="c1"># Check for confidential indicators
</span>        <span class="n">confidential_found</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span>
            <span class="n">keyword</span> <span class="ow">in</span> <span class="n">content</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">confidential_keywords</span>
        <span class="p">)</span>
        
        <span class="c1"># Check for code
</span>        <span class="n">code_found</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span>
            <span class="n">indicator</span> <span class="ow">in</span> <span class="n">content</span> <span class="k">for</span> <span class="n">indicator</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">code_indicators</span>
        <span class="p">)</span>
        
        <span class="k">if</span> <span class="n">confidential_found</span> <span class="ow">or</span> <span class="n">code_found</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">ClassificationResult</span><span class="p">(</span>
                <span class="n">level</span><span class="o">=</span><span class="n">DataClassification</span><span class="p">.</span><span class="n">CONFIDENTIAL</span><span class="p">,</span>
                <span class="n">confidence</span><span class="o">=</span><span class="mf">0.85</span><span class="p">,</span>
                <span class="n">matched_patterns</span><span class="o">=</span><span class="n">matched</span> <span class="o">+</span> <span class="p">([</span><span class="s">"confidential_keywords"</span><span class="p">]</span> <span class="k">if</span> <span class="n">confidential_found</span> <span class="k">else</span> <span class="p">[])</span> <span class="o">+</span> <span class="p">([</span><span class="s">"code_indicators"</span><span class="p">]</span> <span class="k">if</span> <span class="n">code_found</span> <span class="k">else</span> <span class="p">[]),</span>
                <span class="n">recommendations</span><span class="o">=</span><span class="p">[</span>
                    <span class="s">"Use private model or approved enterprise provider"</span><span class="p">,</span>
                    <span class="s">"Require manager approval"</span><span class="p">,</span>
                    <span class="s">"Enable audit logging"</span>
                <span class="p">]</span>
            <span class="p">)</span>
        
        <span class="c1"># Default to internal
</span>        <span class="k">return</span> <span class="n">ClassificationResult</span><span class="p">(</span>
            <span class="n">level</span><span class="o">=</span><span class="n">DataClassification</span><span class="p">.</span><span class="n">INTERNAL</span><span class="p">,</span>
            <span class="n">confidence</span><span class="o">=</span><span class="mf">0.70</span><span class="p">,</span>
            <span class="n">matched_patterns</span><span class="o">=</span><span class="n">matched</span><span class="p">,</span>
            <span class="n">recommendations</span><span class="o">=</span><span class="p">[</span>
                <span class="s">"Use approved enterprise AI providers"</span><span class="p">,</span>
                <span class="s">"Review output before use"</span>
            <span class="p">]</span>
        <span class="p">)</span>
</code></pre></div></div>

<h3 id="kill-switch-implementation">Kill Switch Implementation</h3>

<p><strong>Example: Environment-Based Kill Switch</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Kill switch for disabling AI features instantly
"""</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Callable</span><span class="p">,</span> <span class="n">Optional</span>
<span class="kn">from</span> <span class="nn">enum</span> <span class="kn">import</span> <span class="n">Enum</span>

<span class="k">class</span> <span class="nc">KillSwitchStatus</span><span class="p">(</span><span class="n">Enum</span><span class="p">):</span>
    <span class="n">ENABLED</span> <span class="o">=</span> <span class="s">"enabled"</span>
    <span class="n">DISABLED</span> <span class="o">=</span> <span class="s">"disabled"</span>
    <span class="n">DEGRADED</span> <span class="o">=</span> <span class="s">"degraded"</span>  <span class="c1"># Limited functionality
</span>
<span class="k">class</span> <span class="nc">KillSwitch</span><span class="p">:</span>
    <span class="s">"""Centralized kill switch for AI services"""</span>
    
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_read_status</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">reason</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"AI_KILL_SWITCH_REASON"</span><span class="p">,</span> <span class="s">""</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">_read_status</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">KillSwitchStatus</span><span class="p">:</span>
        <span class="s">"""Read kill switch status from environment"""</span>
        <span class="n">status_str</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"AI_ENABLED"</span><span class="p">,</span> <span class="s">"true"</span><span class="p">).</span><span class="n">lower</span><span class="p">()</span>
        
        <span class="k">if</span> <span class="n">status_str</span> <span class="o">==</span> <span class="s">"false"</span> <span class="ow">or</span> <span class="n">status_str</span> <span class="o">==</span> <span class="s">"disabled"</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">KillSwitchStatus</span><span class="p">.</span><span class="n">DISABLED</span>
        <span class="k">elif</span> <span class="n">status_str</span> <span class="o">==</span> <span class="s">"degraded"</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">KillSwitchStatus</span><span class="p">.</span><span class="n">DEGRADED</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">KillSwitchStatus</span><span class="p">.</span><span class="n">ENABLED</span>
    
    <span class="k">def</span> <span class="nf">is_enabled</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
        <span class="s">"""Check if AI is enabled"""</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">status</span> <span class="o">==</span> <span class="n">KillSwitchStatus</span><span class="p">.</span><span class="n">ENABLED</span>
    
    <span class="k">def</span> <span class="nf">get_fallback_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">service_type</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
        <span class="s">"""Get deterministic fallback when AI is disabled"""</span>
        <span class="n">fallbacks</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"code"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"suggestions"</span><span class="p">:</span> <span class="p">[],</span>
                <span class="s">"message"</span><span class="p">:</span> <span class="s">"AI code assistance is currently disabled. Please contact IT support."</span><span class="p">,</span>
            <span class="p">},</span>
            <span class="s">"document"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"summary"</span><span class="p">:</span> <span class="s">""</span><span class="p">,</span>
                <span class="s">"message"</span><span class="p">:</span> <span class="s">"AI document processing is currently disabled."</span><span class="p">,</span>
            <span class="p">},</span>
            <span class="s">"chat"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"response"</span><span class="p">:</span> <span class="s">"AI chat is currently unavailable. Please try again later."</span><span class="p">,</span>
            <span class="p">}</span>
        <span class="p">}</span>
        
        <span class="k">return</span> <span class="n">fallbacks</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">service_type</span><span class="p">,</span> <span class="p">{</span><span class="s">"message"</span><span class="p">:</span> <span class="s">"AI service unavailable"</span><span class="p">})</span>
    
    <span class="k">def</span> <span class="nf">with_kill_switch</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">ai_function</span><span class="p">:</span> <span class="n">Callable</span><span class="p">,</span>
        <span class="n">service_type</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
        <span class="o">*</span><span class="n">args</span><span class="p">,</span>
        <span class="o">**</span><span class="n">kwargs</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
        <span class="s">"""Execute AI function with kill switch protection"""</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">is_enabled</span><span class="p">():</span>
            <span class="k">return</span> <span class="p">{</span>
                <span class="s">"result"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_fallback_response</span><span class="p">(</span><span class="n">service_type</span><span class="p">),</span>
                <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
                <span class="s">"status"</span><span class="p">:</span> <span class="s">"fallback"</span><span class="p">,</span>
                <span class="s">"reason"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">reason</span> <span class="ow">or</span> <span class="s">"Kill switch activated"</span><span class="p">,</span>
            <span class="p">}</span>
        
        <span class="k">try</span><span class="p">:</span>
            <span class="n">result</span> <span class="o">=</span> <span class="n">ai_function</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
            <span class="k">return</span> <span class="p">{</span>
                <span class="s">"result"</span><span class="p">:</span> <span class="n">result</span><span class="p">,</span>
                <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
                <span class="s">"status"</span><span class="p">:</span> <span class="s">"success"</span><span class="p">,</span>
            <span class="p">}</span>
        <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="c1"># On error, fall back gracefully
</span>            <span class="k">return</span> <span class="p">{</span>
                <span class="s">"result"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_fallback_response</span><span class="p">(</span><span class="n">service_type</span><span class="p">),</span>
                <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
                <span class="s">"status"</span><span class="p">:</span> <span class="s">"error"</span><span class="p">,</span>
                <span class="s">"error"</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">),</span>
            <span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="governance-and-policy-framework">Governance and Policy Framework</h2>

<h3 id="ai-risk-lifecycle">AI Risk Lifecycle</h3>

<p><img src="/img/risk.png" width="750" height="250" /></p>

<h3 id="policy-template-ai-usage-policy">Policy Template: AI Usage Policy</h3>

<p><strong>Sample AI Usage Policy Structure:</strong></p>

<h2 id="ai-usage-policy">AI Usage Policy</h2>

<h3 id="1-scope">1. Scope</h3>
<p>This policy applies to all employees, contractors, and third parties using AI tools
for company business.</p>

<h3 id="2-data-classification">2. Data Classification</h3>

<h4 id="public-data">Public Data</h4>
<ul>
  <li>Definition: Marketing materials, public documentation, non-sensitive content</li>
  <li>Allowed: Public AI tools (ChatGPT, Claude, etc.)</li>
  <li>Requirements: Basic review of outputs</li>
</ul>

<h4 id="internal-non-sensitive">Internal Non-Sensitive</h4>
<ul>
  <li>Definition: Internal process docs, non-confidential policies</li>
  <li>Allowed: Approved enterprise AI providers only</li>
  <li>Requirements: Manager approval, audit logging</li>
</ul>

<h4 id="confidential-data">Confidential Data</h4>
<ul>
  <li>Definition: Source code, architecture, contracts, strategy</li>
  <li>Allowed: Private VPC models only</li>
  <li>Requirements: CISO approval, encryption, full audit trail</li>
</ul>

<h4 id="pii--highly-sensitive">PII / Highly Sensitive</h4>
<ul>
  <li>Definition: Customer data, HR data, health data, financial records</li>
  <li>Allowed: Private VPC models with data processing agreements</li>
  <li>Requirements: CISO + Legal approval, encryption, anonymization, full audit</li>
</ul>

<h3 id="3-prohibited-uses">3. Prohibited Uses</h3>

<ul>
  <li>Never paste PII into public AI tools</li>
  <li>Never paste proprietary code into public AI tools</li>
  <li>Never use personal AI accounts for company data</li>
  <li>Never use unapproved browser extensions or plugins</li>
</ul>

<h3 id="4-approved-tools">4. Approved Tools</h3>

<h4 id="public-tools-public-data-only">Public Tools (Public Data Only)</h4>
<ul>
  <li>ChatGPT (with company account)</li>
  <li>Claude (with company account)</li>
  <li>Google Gemini (with company account)</li>
</ul>

<h4 id="enterprise-tools-internalconfidential">Enterprise Tools (Internal/Confidential)</h4>
<ul>
  <li>OpenAI Enterprise</li>
  <li>Anthropic Enterprise</li>
  <li>Microsoft Copilot (with data residency)</li>
</ul>

<h4 id="private-tools-piiconfidential">Private Tools (PII/Confidential)</h4>
<ul>
  <li>Private VPC-hosted models</li>
  <li>On-premise AI infrastructure</li>
</ul>

<h3 id="5-incident-response">5. Incident Response</h3>

<p>If you suspect data has been leaked:</p>
<ol>
  <li>Immediately stop using the AI tool</li>
  <li>Report to security@company.com</li>
  <li>Document what data was shared</li>
  <li>Do not attempt to delete or cover up</li>
</ol>

<h3 id="6-training-requirements">6. Training Requirements</h3>

<p>All employees must complete:</p>
<ul>
  <li>Annual AI security training</li>
  <li>Data classification training</li>
  <li>Incident reporting procedures</li>
</ul>

<h3 id="7-enforcement">7. Enforcement</h3>

<p>Violations may result in:</p>
<ul>
  <li>Disciplinary action</li>
  <li>Termination for willful violations</li>
  <li>Legal action for data breaches</li>
</ul>

<h4 id="ownership-and-accountability">Ownership and Accountability</h4>

<p><strong>RACI Matrix for AI Governance:</strong></p>

<table>
  <thead>
    <tr>
      <th>Activity</th>
      <th>CISO</th>
      <th>Legal</th>
      <th>Engineering</th>
      <th>Product</th>
      <th>Employees</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Data Classification</td>
      <td>A</td>
      <td>C</td>
      <td>R</td>
      <td>I</td>
      <td>I</td>
    </tr>
    <tr>
      <td>Policy Creation</td>
      <td>R</td>
      <td>A</td>
      <td>C</td>
      <td>C</td>
      <td>I</td>
    </tr>
    <tr>
      <td>Technical Controls</td>
      <td>A</td>
      <td>I</td>
      <td>R</td>
      <td>C</td>
      <td>I</td>
    </tr>
    <tr>
      <td>Training</td>
      <td>C</td>
      <td>I</td>
      <td>I</td>
      <td>C</td>
      <td>R</td>
    </tr>
    <tr>
      <td>Incident Response</td>
      <td>R</td>
      <td>A</td>
      <td>C</td>
      <td>I</td>
      <td>R</td>
    </tr>
    <tr>
      <td>Monitoring</td>
      <td>A</td>
      <td>I</td>
      <td>R</td>
      <td>C</td>
      <td>I</td>
    </tr>
  </tbody>
</table>

<p><strong>Legend:</strong></p>
<ul>
  <li>R = Responsible (does the work)</li>
  <li>A = Accountable (owns the outcome)</li>
  <li>C = Consulted (provides input)</li>
  <li>I = Informed (kept in the loop)</li>
</ul>

<hr />

<h2 id="case-studies-and-real-world-examples">Case Studies and Real-World Examples</h2>

<h3 id="case-study-1-code-leakage-incident">Case Study 1: Code Leakage Incident</h3>

<p><strong>Scenario:</strong>
A software company’s engineers were using ChatGPT to debug code. Over several months, proprietary algorithms and architecture details were pasted into the tool.</p>

<p><strong>Impact:</strong></p>
<ul>
  <li>Competitive advantage lost</li>
  <li>Potential IP violation</li>
  <li>Customer trust damaged</li>
</ul>

<p><strong>Root Cause:</strong></p>
<ul>
  <li>No policy on AI usage</li>
  <li>No technical controls</li>
  <li>No monitoring</li>
</ul>

<p><strong>Solution Implemented:</strong></p>
<ol>
  <li>Immediate: Blocked public AI tools, required VPN for approved tools</li>
  <li>Short-term: Implemented AI gateway with DLP</li>
  <li>Long-term: Private code analysis tools, training program</li>
</ol>

<p><strong>Lessons:</strong></p>
<ul>
  <li>Code is always confidential</li>
  <li>Technical controls beat policy alone</li>
  <li>Monitor before you have an incident</li>
</ul>

<h3 id="case-study-2-healthcare-pii-exposure">Case Study 2: Healthcare PII Exposure</h3>

<p><strong>Scenario:</strong>
A healthcare organization’s staff used AI tools to summarize patient notes. Patient names, conditions, and treatment plans were exposed.</p>

<p><strong>Impact:</strong></p>
<ul>
  <li>HIPAA violation</li>
  <li>Regulatory fines</li>
  <li>Patient lawsuits</li>
</ul>

<p><strong>Root Cause:</strong></p>
<ul>
  <li>PII not properly classified</li>
  <li>No redaction before AI processing</li>
  <li>Staff not trained on data sensitivity</li>
</ul>

<p><strong>Solution Implemented:</strong></p>
<ol>
  <li>Immediate: Shut down all AI tools, incident response</li>
  <li>Short-term: Implemented PII detection and redaction</li>
  <li>Long-term: Private healthcare AI model, comprehensive training</li>
</ol>

<p><strong>Lessons:</strong></p>
<ul>
  <li>Healthcare data requires special handling</li>
  <li>Automated redaction is essential</li>
  <li>Training must be role-specific</li>
</ul>

<h3 id="case-study-3-successful-implementation">Case Study 3: Successful Implementation</h3>

<p><strong>Scenario:</strong>
A financial services company wanted to use AI for customer support but had strict regulatory requirements.</p>

<p><strong>Approach:</strong></p>
<ol>
  <li>Classified all customer data as PII</li>
  <li>Implemented private VPC-hosted model</li>
  <li>Added full audit logging</li>
  <li>Trained all support staff</li>
  <li>Started with low-risk use cases</li>
</ol>

<p><strong>Results:</strong></p>
<ul>
  <li>Zero data leaks in 12 months</li>
  <li>30% reduction in support ticket resolution time</li>
  <li>Regulatory approval maintained</li>
</ul>

<p><strong>Key Success Factors:</strong></p>
<ul>
  <li>Started with classification</li>
  <li>Technical controls from day one</li>
  <li>Continuous monitoring and improvement</li>
</ul>

<hr />

<h2 id="what-i-learned-from-cancer-models-that-applies-to-corporate-ai">What I Learned From Cancer Models That Applies to Corporate AI</h2>

<p>Working on predictive models for cancer detection taught me a few non-negotiable rules:</p>

<ol>
  <li><strong>The cost of a wrong decision matters more than the beauty of the model.</strong></li>
  <li><strong>You can’t separate model performance from data governance.</strong></li>
  <li><strong>You must be able to explain and defend your approach when it matters most.</strong></li>
</ol>

<p>I see too many companies obsess over model performance and UX while ignoring the basics:</p>

<ul>
  <li>Where is the data coming from?</li>
  <li>Who controls it end-to-end?</li>
  <li>What happens if the worst-case scenario actually occurs?</li>
</ul>

<p>When you’re dealing with people’s lives or livelihoods, “we were experimenting” is not an excuse. It’s a confession.</p>

<h3 id="the-parallels">The Parallels</h3>

<table>
  <thead>
    <tr>
      <th>Research Context</th>
      <th>Corporate AI Context</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Patient data privacy</td>
      <td>Customer PII protection</td>
    </tr>
    <tr>
      <td>Model explainability</td>
      <td>Regulatory compliance</td>
    </tr>
    <tr>
      <td>Error cost (misdiagnosis)</td>
      <td>Error cost (data breach)</td>
    </tr>
    <tr>
      <td>Peer review</td>
      <td>Security audit</td>
    </tr>
    <tr>
      <td>IRB approval</td>
      <td>CISO/Legal approval</td>
    </tr>
    <tr>
      <td>Reproducibility</td>
      <td>Audit trails</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="conclusion-ai-with-discipline">Conclusion: AI with Discipline</h2>

<h3 id="three-clear-takeaways-for-leaders-and-teams">Three Clear Takeaways for Leaders and Teams</h3>

<p>If you’re serious about AI and don’t want to be the next cautionary case study, treat these as the bare minimum:</p>

<h4 id="1-ban-confidential-and-pii-data-from-public-ai-tools---and-enforce-it-technically">1. Ban Confidential and PII Data from Public AI Tools - and Enforce It Technically</h4>

<p>Policies without controls are theatre. You need:</p>

<ul>
  <li>Automated data classification</li>
  <li>Technical controls (gateway, DLP, routing)</li>
  <li>Monitoring and alerting</li>
  <li>Incident response procedures</li>
</ul>

<h4 id="2-use-a-risk-based-approach-to-ai-features">2. Use a Risk-Based Approach to AI Features</h4>

<p>Don’t treat a marketing chatbot and an AI credit scorer as the same. They are not.</p>

<ul>
  <li>Classify use cases by impact and sensitivity</li>
  <li>Apply appropriate controls for each quadrant</li>
  <li>Start with low-risk, high-value use cases</li>
  <li>Build capability before tackling high-risk scenarios</li>
</ul>

<h4 id="3-measure-success-with-safety-and-trust-included">3. Measure Success with Safety and Trust Included</h4>

<p>Track not only adoption and revenue, but also:</p>

<ul>
  <li>Incidents and near misses</li>
  <li>Policy violations</li>
  <li>User trust metrics</li>
  <li>Cost of controls vs. cost of incidents</li>
</ul>

<p>If those metrics are invisible, your AI programme is fundamentally incomplete.</p>

<h3 id="the-choice">The Choice</h3>

<p>AI is not going away. The question is whether we force it to grow up - or we let greed and fear drive us into predictable disasters.</p>

<p>The choice is not “AI or safety”. The serious choice is:</p>

<p><strong>AI with discipline, or AI with collateral damage.</strong></p>

<hr />

<p><strong>Author</strong>: Dr. Atabak Kheirkhah<br />
<strong>Date</strong>: November 30, 2025<br />
<strong>Contact</strong>: atabakkheirkhah@gmail.com</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>From Demo to Daily: A Measurable AI Copilot Pattern on GCP</title>
					
					<link>https://atabakkh.github.io/ai/2025/11/17/from-demo-to-daily-measurable-ai-copilot-pattern-on-gcp.html</link>

					

					<pubDate>Mon, 17 Nov 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/ai/2025/11/17/from-demo-to-daily-measurable-ai-copilot-pattern-on-gcp</guid>

					
						<category><![CDATA[ AI ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Right-Time AI: Small Copilots, Big Proof - An End-to-End PoC</strong></p>

<p>In a two-week <strong>personal side experiment</strong>, I built three micro-copilots (reading, meeting, and code) on Vertex AI, Cloud Run, Workflows, and BigQuery. I defined strict hypotheses (time saved, edits reduced, and predictable cost), mapped decisions to right-time freshness tiers (seconds, minutes, or daily), and added a kill switch for reversibility. Two copilots passed the stated thresholds (≥15% time saved and ≥20% fewer edits); one failed and was retired. This article shares the pattern, guardrails, and metrics to help teams decide what to keep, what to stop, and why.</p>

<p><strong>Key Results:</strong></p>

<ul>
  <li><strong>Reading Copilot</strong>: 17% time saved, 21% fewer edits, $0.01/task (daily batch)</li>
  <li><strong>Meeting Copilot</strong>: 20% time saved, p95 latency &lt;3 min</li>
  <li><strong>Code Copilot</strong>: Mixed results - retired pending on-device experiments</li>
  <li><strong>Kill Switch</strong>: 100% successful fallback rate</li>
  <li><strong>Cost</strong>: Predictable $0.01-0.02 per task with no spikes</li>
</ul>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ol>
  <li><a href="#why-i-did-it">Why I Did It</a></li>
  <li><a href="#the-rules-before-code">The Rules (Before Code)</a></li>
  <li><a href="#architecture">Architecture</a></li>
  <li><a href="#implementation">Implementation</a></li>
  <li><a href="#testing-strategy">Testing Strategy</a></li>
  <li><a href="#measurement--metrics">Measurement &amp; Metrics</a></li>
  <li><a href="#results">Results</a></li>
  <li><a href="#governance--trust">Governance &amp; Trust</a></li>
  <li><a href="#what-failed--why">What Failed &amp; Why</a></li>
  <li><a href="#reusable-pattern">Reusable Pattern</a></li>
</ol>

<hr />

<h2 id="why-i-did-it">Why I Did It</h2>

<p>Usually demos are easy, but daily impact is hard. I wanted a personal, honest answer to a simple question: <strong>Can small AI helpers applied to real tasks save people time without creating chaos or surprise costs?</strong></p>

<p>The goal wasn’t to build the next ChatGPT, Gemini, Copilot, or any other competitor. It was to test a <strong>repeatable, governed pattern</strong> that delivers measurable value-one that could be adapted to other personal or company workflows with clear success gates and a kill switch.</p>

<hr />

<h2 id="the-rules-before-code">The Rules (Before Code)</h2>

<h3 id="scope">Scope</h3>

<ul>
  <li>Three micro-copilots for workflows: reading, meetings, code</li>
  <li>No business data, no PII</li>
</ul>

<h3 id="right-time-freshness">Right-Time Freshness</h3>

<ul>
  <li><strong>Code</strong>: seconds (interactive code review)</li>
  <li><strong>Meeting</strong>: minutes (post-meeting processing)</li>
  <li><strong>Reading</strong>: daily (overnight batch processing)</li>
</ul>

<p>No fake “real-time” where decisions don’t need it.</p>

<h3 id="guardrails">Guardrails</h3>

<ul>
  <li>EU region only</li>
  <li>Plain-language prompts</li>
  <li>Audit logs on all operations</li>
  <li>One-click kill switch (<code class="language-plaintext highlighter-rouge">AI_ENABLED=false</code>)</li>
</ul>

<h3 id="success-gates">Success Gates</h3>

<ul>
  <li><strong>H1</strong>: ≥15% time saved on ≥2 copilots (median over 5+ runs)</li>
  <li><strong>H2</strong>: Edits reduced by ≥20% vs. baseline</li>
  <li><strong>H3</strong>: Stable cost/task with ceilings per use case</li>
  <li><strong>H4</strong>: p95 latency meets SLA for its freshness tier</li>
  <li><strong>H5</strong>: Reversibility proven (kill switch -&gt; clean fallback, logged)</li>
</ul>

<hr />

<h2 id="architecture">Architecture</h2>

<h3 id="system-overview">System Overview</h3>

<p><img src="/img/gemini-architecture.jpg" width="750" height="250" /></p>

<h3 id="component-details">Component Details</h3>

<p>I used a personal Gmail account and personal GCP trial credits for this test-not employer infrastructure or data. Details follow:</p>

<h4 id="1-cloud-run-services">1. Cloud Run Services</h4>

<p><strong>Characteristics:</strong></p>

<ul>
  <li>Serverless, auto-scaling (0 to N instances)</li>
  <li>Request timeout: 300 seconds</li>
  <li>Container concurrency: 80</li>
  <li>CPU: 1 vCPU, Memory: 512Mi</li>
  <li><strong>Cost optimization</strong>: Scale to zero, max 3 instances</li>
</ul>

<p><strong>Endpoints:</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">POST /suggest</code> - Main copilot endpoint</li>
  <li><code class="language-plaintext highlighter-rouge">GET /health</code> - Health check with kill switch status</li>
</ul>

<h4 id="2-bigquery-schema">2. BigQuery Schema</h4>

<p><strong>Dataset</strong>: <code class="language-plaintext highlighter-rouge">copilot_poc</code></p>

<p><strong>Tables:</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">events</code> - One row per invocation with full telemetry</li>
  <li><code class="language-plaintext highlighter-rouge">catalog</code> - Workflow registry with freshness tiers and SLAs</li>
  <li><code class="language-plaintext highlighter-rouge">metrics</code> (view) - Daily aggregates by copilot</li>
</ul>

<p><strong>Partitioning</strong>: Events table partitioned by date</p>

<p><strong>Clustering</strong>: By copilot and outcome for query optimization</p>

<h4 id="3-security--governance">3. Security &amp; Governance</h4>

<ul>
  <li><strong>IAM</strong>: Service accounts with least-privilege roles</li>
  <li><strong>Secrets</strong>: Secret Manager for API keys</li>
  <li><strong>Audit Logs</strong>: All operations logged</li>
  <li><strong>Kill Switch</strong>: Environment variable <code class="language-plaintext highlighter-rouge">AI_ENABLED=false</code></li>
  <li><strong>EU Locations</strong>: Compute in <code class="language-plaintext highlighter-rouge">europe-west4</code>; analytics in BigQuery’s <code class="language-plaintext highlighter-rouge">EU</code> multi-region</li>
</ul>

<hr />

<h2 id="implementation">Implementation</h2>

<h3 id="shared-library-ai-client-abstraction">Shared Library: AI Client Abstraction</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">lib/ai_client.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
AI client abstraction with a Vertex AI implementation.
"""</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">from</span> <span class="nn">abc</span> <span class="kn">import</span> <span class="n">ABC</span><span class="p">,</span> <span class="n">abstractmethod</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Tuple</span>
<span class="kn">from</span> <span class="nn">enum</span> <span class="kn">import</span> <span class="n">Enum</span>

<span class="k">try</span><span class="p">:</span>
    <span class="kn">from</span> <span class="nn">google.cloud</span> <span class="kn">import</span> <span class="n">aiplatform</span>
    <span class="kn">from</span> <span class="nn">vertexai.preview.generative_models</span> <span class="kn">import</span> <span class="n">GenerativeModel</span>
<span class="k">except</span> <span class="nb">ImportError</span><span class="p">:</span>
    <span class="n">aiplatform</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">GenerativeModel</span> <span class="o">=</span> <span class="bp">None</span>

<span class="k">class</span> <span class="nc">AIProvider</span><span class="p">(</span><span class="nb">str</span><span class="p">,</span> <span class="n">Enum</span><span class="p">):</span>
    <span class="s">"""Supported AI providers."""</span>
    <span class="n">OPENAI</span> <span class="o">=</span> <span class="s">"openai"</span>
    <span class="n">VERTEX</span> <span class="o">=</span> <span class="s">"vertex"</span>

<span class="k">class</span> <span class="nc">AIClient</span><span class="p">(</span><span class="n">ABC</span><span class="p">):</span>
    <span class="s">"""Abstract base class for AI clients."""</span>
    
    <span class="o">@</span><span class="n">abstractmethod</span>
    <span class="k">def</span> <span class="nf">generate</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">prompt</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
        <span class="n">system_prompt</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">max_tokens</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">2000</span><span class="p">,</span>
        <span class="n">temperature</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.7</span><span class="p">,</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">int</span><span class="p">]]:</span>
        <span class="s">"""
        Generate a response from the AI model.
        
        Returns:
            Tuple of (response_text, token_usage_dict)
            token_usage_dict contains: tokens_in, tokens_out
        """</span>
        <span class="k">pass</span>

<span class="k">class</span> <span class="nc">VertexAIClient</span><span class="p">(</span><span class="n">AIClient</span><span class="p">):</span>
    <span class="s">"""Google Vertex AI client."""</span>
    
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">project_id</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">location</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"europe-west4"</span><span class="p">,</span>
        <span class="n">model</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"gemini-2.5-flash"</span><span class="p">,</span>
    <span class="p">):</span>
        <span class="k">if</span> <span class="n">GenerativeModel</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">ImportError</span><span class="p">(</span>
                <span class="s">"vertexai package not installed. Install with: pip install google-cloud-aiplatform"</span>
            <span class="p">)</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">project_id</span> <span class="o">=</span> <span class="n">project_id</span> <span class="ow">or</span> <span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"GCP_PROJECT_ID"</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">project_id</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"GCP_PROJECT_ID not provided"</span><span class="p">)</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">location</span> <span class="o">=</span> <span class="n">location</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">model_name</span> <span class="o">=</span> <span class="n">model</span>
        
        <span class="n">aiplatform</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">project</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">project_id</span><span class="p">,</span> <span class="n">location</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">location</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">model</span> <span class="o">=</span> <span class="n">GenerativeModel</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">generate</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">prompt</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
        <span class="n">system_prompt</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">max_tokens</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">2000</span><span class="p">,</span>
        <span class="n">temperature</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.7</span><span class="p">,</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">int</span><span class="p">]]:</span>
        <span class="s">"""Generate response using Vertex AI."""</span>
        <span class="n">full_prompt</span> <span class="o">=</span> <span class="n">prompt</span>
        <span class="k">if</span> <span class="n">system_prompt</span><span class="p">:</span>
            <span class="n">full_prompt</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">system_prompt</span><span class="si">}</span><span class="se">\n\n</span><span class="si">{</span><span class="n">prompt</span><span class="si">}</span><span class="s">"</span>
        
        <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">model</span><span class="p">.</span><span class="n">generate_content</span><span class="p">(</span>
            <span class="n">full_prompt</span><span class="p">,</span>
            <span class="n">generation_config</span><span class="o">=</span><span class="p">{</span>
                <span class="s">"max_output_tokens"</span><span class="p">:</span> <span class="n">max_tokens</span><span class="p">,</span>
                <span class="s">"temperature"</span><span class="p">:</span> <span class="n">temperature</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">)</span>
        
        <span class="n">content</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">text</span>
        <span class="n">usage</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">usage_metadata</span>
        <span class="n">thinking_tokens</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="nb">getattr</span><span class="p">(</span><span class="n">usage</span><span class="p">,</span> <span class="s">"thoughts_token_count"</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="ow">or</span> <span class="mi">0</span><span class="p">)</span>
        <span class="n">token_usage</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"tokens_in"</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">usage</span><span class="p">.</span><span class="n">prompt_token_count</span><span class="p">),</span>
            <span class="s">"tokens_out"</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">usage</span><span class="p">.</span><span class="n">candidates_token_count</span><span class="p">)</span> <span class="o">+</span> <span class="n">thinking_tokens</span><span class="p">,</span>
        <span class="p">}</span>
        
        <span class="k">return</span> <span class="n">content</span><span class="p">,</span> <span class="n">token_usage</span>

<span class="k">def</span> <span class="nf">get_ai_client</span><span class="p">(</span>
    <span class="n">provider</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="o">**</span><span class="n">kwargs</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">AIClient</span><span class="p">:</span>
    <span class="s">"""
    Factory function to get the appropriate AI client.
    
    Args:
        provider: "vertex" (defaults to AI_PROVIDER env var)
        **kwargs: Additional arguments passed to the client constructor
    
    Returns:
        AIClient instance
    """</span>
    <span class="n">provider</span> <span class="o">=</span> <span class="n">provider</span> <span class="ow">or</span> <span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"AI_PROVIDER"</span><span class="p">,</span> <span class="n">AIProvider</span><span class="p">.</span><span class="n">VERTEX</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>
    
    <span class="k">if</span> <span class="n">provider</span> <span class="o">==</span> <span class="n">AIProvider</span><span class="p">.</span><span class="n">OPENAI</span><span class="p">.</span><span class="n">value</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">NotImplementedError</span><span class="p">(</span><span class="s">"OpenAIClient is not included in this PoC"</span><span class="p">)</span>
    <span class="k">elif</span> <span class="n">provider</span> <span class="o">==</span> <span class="n">AIProvider</span><span class="p">.</span><span class="n">VERTEX</span><span class="p">.</span><span class="n">value</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">VertexAIClient</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s">"Unknown AI provider: </span><span class="si">{</span><span class="n">provider</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="kill-switch-implementation">Kill Switch Implementation</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">lib/kill_switch.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Kill switch logic for disabling AI and falling back to deterministic outputs.
"""</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Callable</span><span class="p">,</span> <span class="n">Optional</span>

<span class="k">def</span> <span class="nf">is_ai_enabled</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="s">"""Check if AI is enabled via environment variable."""</span>
    <span class="k">return</span> <span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"AI_ENABLED"</span><span class="p">,</span> <span class="s">"true"</span><span class="p">).</span><span class="n">lower</span><span class="p">()</span> <span class="o">==</span> <span class="s">"true"</span>

<span class="k">def</span> <span class="nf">get_fallback_response</span><span class="p">(</span><span class="n">copilot_type</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
    <span class="s">"""
    Get deterministic fallback response when AI is disabled.
    
    Args:
        copilot_type: "code", "reading", or "meeting"
    
    Returns:
        Dictionary with fallback response structure
    """</span>
    <span class="n">fallbacks</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"code"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"test_suggestions"</span><span class="p">:</span> <span class="p">[],</span>
            <span class="s">"refactor_hints"</span><span class="p">:</span> <span class="p">[],</span>
            <span class="s">"message"</span><span class="p">:</span> <span class="s">"AI is disabled. Please enable AI_ENABLED=true to get suggestions."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="s">"reading"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"digest"</span><span class="p">:</span> <span class="p">[],</span>
            <span class="s">"follow_up_prompts"</span><span class="p">:</span> <span class="p">[],</span>
            <span class="s">"message"</span><span class="p">:</span> <span class="s">"AI is disabled. Please enable AI_ENABLED=true to get digest."</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="s">"meeting"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"action_items"</span><span class="p">:</span> <span class="p">[],</span>
            <span class="s">"message"</span><span class="p">:</span> <span class="s">"AI is disabled. Please enable AI_ENABLED=true to get action items."</span><span class="p">,</span>
        <span class="p">},</span>
    <span class="p">}</span>
    
    <span class="k">return</span> <span class="n">fallbacks</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">copilot_type</span><span class="p">,</span> <span class="p">{</span><span class="s">"message"</span><span class="p">:</span> <span class="s">"AI is disabled"</span><span class="p">})</span>

<span class="k">def</span> <span class="nf">with_kill_switch</span><span class="p">(</span>
    <span class="n">ai_function</span><span class="p">:</span> <span class="n">Callable</span><span class="p">,</span>
    <span class="n">copilot_type</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="o">*</span><span class="n">args</span><span class="p">,</span>
    <span class="o">**</span><span class="n">kwargs</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
    <span class="s">"""
    Execute AI function with kill switch fallback.
    
    Args:
        ai_function: Function to call if AI is enabled
        copilot_type: Type of copilot for fallback response
        *args, **kwargs: Arguments to pass to ai_function
    
    Returns:
        Result from ai_function or fallback response
    """</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">is_ai_enabled</span><span class="p">():</span>
        <span class="k">return</span> <span class="p">{</span>
            <span class="s">"result"</span><span class="p">:</span> <span class="n">get_fallback_response</span><span class="p">(</span><span class="n">copilot_type</span><span class="p">),</span>
            <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
            <span class="s">"outcome"</span><span class="p">:</span> <span class="s">"fallback"</span><span class="p">,</span>
        <span class="p">}</span>
    
    <span class="k">try</span><span class="p">:</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">ai_function</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
        <span class="k">return</span> <span class="p">{</span>
            <span class="s">"result"</span><span class="p">:</span> <span class="n">result</span><span class="p">,</span>
            <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
            <span class="s">"outcome"</span><span class="p">:</span> <span class="s">"success"</span><span class="p">,</span>
        <span class="p">}</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">{</span>
            <span class="s">"result"</span><span class="p">:</span> <span class="n">get_fallback_response</span><span class="p">(</span><span class="n">copilot_type</span><span class="p">),</span>
            <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
            <span class="s">"outcome"</span><span class="p">:</span> <span class="s">"error"</span><span class="p">,</span>
            <span class="s">"error"</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">),</span>
        <span class="p">}</span>
</code></pre></div></div>

<h3 id="code-copilot-service">Code Copilot Service</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">copilots/code_copilot/main.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Code Copilot FastAPI service.
"""</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
<span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span><span class="p">,</span> <span class="n">HTTPException</span><span class="p">,</span> <span class="n">UploadFile</span><span class="p">,</span> <span class="n">File</span><span class="p">,</span> <span class="n">Form</span>
<span class="kn">from</span> <span class="nn">fastapi.responses</span> <span class="kn">import</span> <span class="n">JSONResponse</span>
<span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span>

<span class="n">sys</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">__file__</span><span class="p">),</span> <span class="s">'../../'</span><span class="p">))</span>

<span class="kn">from</span> <span class="nn">lib.kill_switch</span> <span class="kn">import</span> <span class="n">is_ai_enabled</span><span class="p">,</span> <span class="n">with_kill_switch</span><span class="p">,</span> <span class="n">get_fallback_response</span>
<span class="kn">from</span> <span class="nn">lib.bigquery_client</span> <span class="kn">import</span> <span class="n">BigQueryClient</span>
<span class="kn">from</span> <span class="nn">copilots.code_copilot.copilot</span> <span class="kn">import</span> <span class="n">CodeCopilot</span>

<span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Code Copilot"</span><span class="p">,</span> <span class="n">version</span><span class="o">=</span><span class="s">"1.0.0"</span><span class="p">)</span>

<span class="n">_copilot</span> <span class="o">=</span> <span class="bp">None</span>
<span class="n">_bq_client</span> <span class="o">=</span> <span class="bp">None</span>

<span class="k">def</span> <span class="nf">get_copilot</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">CodeCopilot</span><span class="p">:</span>
    <span class="s">"""Initialize the AI client only when an enabled request needs it."""</span>
    <span class="k">global</span> <span class="n">_copilot</span>
    <span class="k">if</span> <span class="n">_copilot</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">_copilot</span> <span class="o">=</span> <span class="n">CodeCopilot</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">_copilot</span>

<span class="k">def</span> <span class="nf">get_bq_client</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">BigQueryClient</span><span class="p">:</span>
    <span class="s">"""Initialize telemetry lazily so module import stays testable."""</span>
    <span class="k">global</span> <span class="n">_bq_client</span>
    <span class="k">if</span> <span class="n">_bq_client</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">_bq_client</span> <span class="o">=</span> <span class="n">BigQueryClient</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">_bq_client</span>

<span class="k">class</span> <span class="nc">CodeRequest</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="s">"""Request model for code analysis."""</span>
    <span class="n">code</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">workflow</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">corrections_needed</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>

<span class="k">class</span> <span class="nc">SuggestResponse</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="s">"""Response model for suggestions."""</span>
    <span class="n">result</span><span class="p">:</span> <span class="nb">dict</span>
    <span class="n">latency_ms</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span>
    <span class="n">tokens_in</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span>
    <span class="n">tokens_out</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span>
    <span class="n">ai_enabled</span><span class="p">:</span> <span class="nb">bool</span>
    <span class="n">outcome</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">event_id</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"/health"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">health</span><span class="p">():</span>
    <span class="s">"""Health check endpoint."""</span>
    <span class="k">return</span> <span class="p">{</span><span class="s">"status"</span><span class="p">:</span> <span class="s">"healthy"</span><span class="p">,</span> <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="n">is_ai_enabled</span><span class="p">()}</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="s">"/suggest"</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">SuggestResponse</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">suggest</span><span class="p">(</span>
    <span class="n">code</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">Form</span><span class="p">(</span><span class="bp">None</span><span class="p">),</span>
    <span class="nb">file</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">UploadFile</span><span class="p">]</span> <span class="o">=</span> <span class="n">File</span><span class="p">(</span><span class="bp">None</span><span class="p">),</span>
    <span class="n">workflow</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">Form</span><span class="p">(</span><span class="bp">None</span><span class="p">),</span>
    <span class="n">corrections_needed</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">Form</span><span class="p">(</span><span class="bp">None</span><span class="p">),</span>
<span class="p">):</span>
    <span class="s">"""
    Get code analysis suggestions.
    
    Accepts either:
    - code: Direct code text
    - file: Uploaded code file
    """</span>
    <span class="c1"># Get code content
</span>    <span class="n">code_content</span> <span class="o">=</span> <span class="n">code</span>
    <span class="k">if</span> <span class="nb">file</span><span class="p">:</span>
        <span class="n">code_content</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">file</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>
        <span class="n">code_content</span> <span class="o">=</span> <span class="n">code_content</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">)</span>
    
    <span class="k">if</span> <span class="ow">not</span> <span class="n">code_content</span><span class="p">:</span>
        <span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">400</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="s">"Either 'code' or 'file' must be provided"</span><span class="p">)</span>
    
    <span class="n">input_bytes</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">code_content</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">))</span>
    
    <span class="k">def</span> <span class="nf">analyze</span><span class="p">():</span>
        <span class="k">return</span> <span class="n">get_copilot</span><span class="p">().</span><span class="n">analyze_code</span><span class="p">(</span><span class="n">code_content</span><span class="p">)</span>
    
    <span class="n">response</span> <span class="o">=</span> <span class="n">with_kill_switch</span><span class="p">(</span><span class="n">analyze</span><span class="p">,</span> <span class="s">"code"</span><span class="p">)</span>
    
    <span class="n">telemetry</span> <span class="o">=</span> <span class="n">response</span><span class="p">[</span><span class="s">"result"</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">"telemetry"</span><span class="p">,</span> <span class="p">{})</span>
    <span class="n">latency_ms</span> <span class="o">=</span> <span class="n">telemetry</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"latency_ms"</span><span class="p">)</span>
    <span class="n">tokens_in</span> <span class="o">=</span> <span class="n">telemetry</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"tokens_in"</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    <span class="n">tokens_out</span> <span class="o">=</span> <span class="n">telemetry</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"tokens_out"</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    
    <span class="n">cost_usd</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="k">if</span> <span class="n">tokens_in</span> <span class="ow">and</span> <span class="n">tokens_out</span><span class="p">:</span>
        <span class="c1"># Gemini 2.5 Flash standard text rates used for this 2025 PoC.
</span>        <span class="c1"># Check current pricing before reusing this calculation.
</span>        <span class="n">cost_usd</span> <span class="o">=</span> <span class="p">(</span>
            <span class="n">tokens_in</span> <span class="o">/</span> <span class="mi">1_000_000</span> <span class="o">*</span> <span class="mf">0.30</span>
            <span class="o">+</span> <span class="n">tokens_out</span> <span class="o">/</span> <span class="mi">1_000_000</span> <span class="o">*</span> <span class="mf">2.50</span>
        <span class="p">)</span>
    
    <span class="n">event_id</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">event_id</span> <span class="o">=</span> <span class="n">get_bq_client</span><span class="p">().</span><span class="n">log_event</span><span class="p">(</span>
            <span class="n">copilot</span><span class="o">=</span><span class="s">"code"</span><span class="p">,</span>
            <span class="n">workflow</span><span class="o">=</span><span class="n">workflow</span><span class="p">,</span>
            <span class="n">freshness_tier</span><span class="o">=</span><span class="s">"seconds"</span><span class="p">,</span>
            <span class="n">input_bytes</span><span class="o">=</span><span class="n">input_bytes</span><span class="p">,</span>
            <span class="n">output_bytes</span><span class="o">=</span><span class="nb">len</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">response</span><span class="p">[</span><span class="s">"result"</span><span class="p">]).</span><span class="n">encode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">)),</span>
            <span class="n">tokens_in</span><span class="o">=</span><span class="n">tokens_in</span><span class="p">,</span>
            <span class="n">tokens_out</span><span class="o">=</span><span class="n">tokens_out</span><span class="p">,</span>
            <span class="n">latency_ms</span><span class="o">=</span><span class="n">latency_ms</span><span class="p">,</span>
            <span class="n">ai_enabled</span><span class="o">=</span><span class="n">response</span><span class="p">[</span><span class="s">"ai_enabled"</span><span class="p">],</span>
            <span class="n">corrections_needed</span><span class="o">=</span><span class="n">corrections_needed</span><span class="p">,</span>
            <span class="n">outcome</span><span class="o">=</span><span class="n">response</span><span class="p">[</span><span class="s">"outcome"</span><span class="p">],</span>
            <span class="n">cost_usd</span><span class="o">=</span><span class="n">cost_usd</span><span class="p">,</span>
        <span class="p">)</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Failed to log to BigQuery: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">SuggestResponse</span><span class="p">(</span>
        <span class="n">result</span><span class="o">=</span><span class="n">response</span><span class="p">[</span><span class="s">"result"</span><span class="p">],</span>
        <span class="n">latency_ms</span><span class="o">=</span><span class="n">latency_ms</span><span class="p">,</span>
        <span class="n">tokens_in</span><span class="o">=</span><span class="n">tokens_in</span><span class="p">,</span>
        <span class="n">tokens_out</span><span class="o">=</span><span class="n">tokens_out</span><span class="p">,</span>
        <span class="n">ai_enabled</span><span class="o">=</span><span class="n">response</span><span class="p">[</span><span class="s">"ai_enabled"</span><span class="p">],</span>
        <span class="n">outcome</span><span class="o">=</span><span class="n">response</span><span class="p">[</span><span class="s">"outcome"</span><span class="p">],</span>
        <span class="n">event_id</span><span class="o">=</span><span class="n">event_id</span><span class="p">,</span>
    <span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="kn">import</span> <span class="nn">uvicorn</span>
    <span class="n">port</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"PORT"</span><span class="p">,</span> <span class="mi">8080</span><span class="p">))</span>
    <span class="n">uvicorn</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="s">"0.0.0.0"</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="n">port</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">copilots/code_copilot/copilot.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Code Copilot - AI-powered code analysis and suggestions.
"""</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">List</span><span class="p">,</span> <span class="n">Any</span>

<span class="n">parent_dir</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">__file__</span><span class="p">),</span> <span class="s">'../../'</span><span class="p">)</span>
<span class="n">sys</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">parent_dir</span><span class="p">)</span>

<span class="kn">from</span> <span class="nn">lib.ai_client</span> <span class="kn">import</span> <span class="n">get_ai_client</span>
<span class="kn">from</span> <span class="nn">lib.telemetry</span> <span class="kn">import</span> <span class="n">Telemetry</span>

<span class="k">class</span> <span class="nc">CodeCopilot</span><span class="p">:</span>
    <span class="s">"""Code Copilot for test suggestions and refactor hints."""</span>
    
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">ai_client</span> <span class="o">=</span> <span class="n">get_ai_client</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">system_prompt</span> <span class="o">=</span> <span class="s">"""You are a code analysis assistant. Analyze code and provide:
1. Test case suggestions (unit tests, edge cases)
2. Refactoring hints (code quality, performance, maintainability)
Be concise and actionable. Format output as JSON with keys: test_suggestions (list) and refactor_hints (list)."""</span>
    
    <span class="k">def</span> <span class="nf">analyze_code</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">code</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
        <span class="s">"""
        Analyze code and provide suggestions.
        
        Args:
            code: Source code to analyze
        
        Returns:
            Dictionary with test_suggestions and refactor_hints
        """</span>
        <span class="n">code_block</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"```python</span><span class="se">\n</span><span class="si">{</span><span class="n">code</span><span class="si">}</span><span class="se">\n</span><span class="s">```"</span>
        <span class="n">prompt</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"""Analyze the following code and provide test suggestions and refactoring hints:

</span><span class="si">{</span><span class="n">code_block</span><span class="si">}</span><span class="s">

Provide your analysis in JSON format with:
- test_suggestions: array of test case descriptions
- refactor_hints: array of refactoring suggestions"""</span>
        
        <span class="n">telemetry</span> <span class="o">=</span> <span class="n">Telemetry</span><span class="p">()</span>
        <span class="k">with</span> <span class="n">telemetry</span><span class="p">.</span><span class="n">track</span><span class="p">():</span>
            <span class="n">response</span><span class="p">,</span> <span class="n">token_usage</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">ai_client</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span>
                <span class="n">prompt</span><span class="o">=</span><span class="n">prompt</span><span class="p">,</span>
                <span class="n">system_prompt</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">system_prompt</span><span class="p">,</span>
                <span class="n">max_tokens</span><span class="o">=</span><span class="mi">2000</span><span class="p">,</span>
                <span class="n">temperature</span><span class="o">=</span><span class="mf">0.7</span><span class="p">,</span>
            <span class="p">)</span>
            <span class="n">telemetry</span><span class="p">.</span><span class="n">set_tokens</span><span class="p">(</span><span class="n">token_usage</span><span class="p">[</span><span class="s">"tokens_in"</span><span class="p">],</span> <span class="n">token_usage</span><span class="p">[</span><span class="s">"tokens_out"</span><span class="p">])</span>
        
        <span class="k">try</span><span class="p">:</span>
            <span class="kn">import</span> <span class="nn">json</span>
            <span class="k">if</span> <span class="s">"```json"</span> <span class="ow">in</span> <span class="n">response</span><span class="p">:</span>
                <span class="n">json_start</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">"```json"</span><span class="p">)</span> <span class="o">+</span> <span class="mi">7</span>
                <span class="n">json_end</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">"```"</span><span class="p">,</span> <span class="n">json_start</span><span class="p">)</span>
                <span class="n">response</span> <span class="o">=</span> <span class="n">response</span><span class="p">[</span><span class="n">json_start</span><span class="p">:</span><span class="n">json_end</span><span class="p">].</span><span class="n">strip</span><span class="p">()</span>
            <span class="k">elif</span> <span class="s">"```"</span> <span class="ow">in</span> <span class="n">response</span><span class="p">:</span>
                <span class="n">json_start</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">"```"</span><span class="p">)</span> <span class="o">+</span> <span class="mi">3</span>
                <span class="n">json_end</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">"```"</span><span class="p">,</span> <span class="n">json_start</span><span class="p">)</span>
                <span class="n">response</span> <span class="o">=</span> <span class="n">response</span><span class="p">[</span><span class="n">json_start</span><span class="p">:</span><span class="n">json_end</span><span class="p">].</span><span class="n">strip</span><span class="p">()</span>
            
            <span class="n">result</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
        <span class="k">except</span> <span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">JSONDecodeError</span><span class="p">,</span> <span class="nb">ValueError</span><span class="p">):</span>
            <span class="n">result</span> <span class="o">=</span> <span class="p">{</span>
                <span class="s">"test_suggestions"</span><span class="p">:</span> <span class="p">[</span><span class="n">response</span><span class="p">],</span>
                <span class="s">"refactor_hints"</span><span class="p">:</span> <span class="p">[],</span>
            <span class="p">}</span>
        
        <span class="k">return</span> <span class="p">{</span>
            <span class="s">"test_suggestions"</span><span class="p">:</span> <span class="n">result</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"test_suggestions"</span><span class="p">,</span> <span class="p">[]),</span>
            <span class="s">"refactor_hints"</span><span class="p">:</span> <span class="n">result</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"refactor_hints"</span><span class="p">,</span> <span class="p">[]),</span>
            <span class="s">"telemetry"</span><span class="p">:</span> <span class="n">telemetry</span><span class="p">.</span><span class="n">to_dict</span><span class="p">(),</span>
        <span class="p">}</span>
</code></pre></div></div>

<h3 id="bigquery-client">BigQuery Client</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">lib/bigquery_client.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
BigQuery client for logging events and metrics.
"""</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">uuid</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>

<span class="k">try</span><span class="p">:</span>
    <span class="kn">from</span> <span class="nn">google.cloud</span> <span class="kn">import</span> <span class="n">bigquery</span>
    <span class="kn">from</span> <span class="nn">google.cloud.exceptions</span> <span class="kn">import</span> <span class="n">NotFound</span>
<span class="k">except</span> <span class="nb">ImportError</span><span class="p">:</span>
    <span class="n">bigquery</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">NotFound</span> <span class="o">=</span> <span class="bp">None</span>

<span class="k">class</span> <span class="nc">BigQueryClient</span><span class="p">:</span>
    <span class="s">"""Client for logging events to BigQuery."""</span>
    
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">project_id</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span> <span class="n">dataset_id</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"copilot_poc"</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">bigquery</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">ImportError</span><span class="p">(</span><span class="s">"google-cloud-bigquery not installed"</span><span class="p">)</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">project_id</span> <span class="o">=</span> <span class="n">project_id</span> <span class="ow">or</span> <span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"GCP_PROJECT_ID"</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">project_id</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"GCP_PROJECT_ID not provided"</span><span class="p">)</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">dataset_id</span> <span class="o">=</span> <span class="n">dataset_id</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">client</span> <span class="o">=</span> <span class="n">bigquery</span><span class="p">.</span><span class="n">Client</span><span class="p">(</span><span class="n">project</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">project_id</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">_ensure_dataset_and_table</span><span class="p">()</span>
    
    <span class="k">def</span> <span class="nf">_ensure_dataset_and_table</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="s">"""Ensure dataset and table exist."""</span>
        <span class="n">dataset_ref</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">dataset</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">dataset_id</span><span class="p">)</span>
        
        <span class="k">try</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">get_dataset</span><span class="p">(</span><span class="n">dataset_ref</span><span class="p">)</span>
        <span class="k">except</span> <span class="n">NotFound</span><span class="p">:</span>
            <span class="n">dataset</span> <span class="o">=</span> <span class="n">bigquery</span><span class="p">.</span><span class="n">Dataset</span><span class="p">(</span><span class="n">dataset_ref</span><span class="p">)</span>
            <span class="n">dataset</span><span class="p">.</span><span class="n">location</span> <span class="o">=</span> <span class="s">"EU"</span>
            <span class="n">dataset</span><span class="p">.</span><span class="n">description</span> <span class="o">=</span> <span class="s">"Dataset for storing copilot events and metrics"</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">create_dataset</span><span class="p">(</span><span class="n">dataset</span><span class="p">)</span>
        
        <span class="n">table_ref</span> <span class="o">=</span> <span class="n">dataset_ref</span><span class="p">.</span><span class="n">table</span><span class="p">(</span><span class="s">"events"</span><span class="p">)</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">table</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">get_table</span><span class="p">(</span><span class="n">table_ref</span><span class="p">)</span>
            <span class="k">if</span> <span class="s">"cost_usd"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">{</span><span class="n">field</span><span class="p">.</span><span class="n">name</span> <span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="n">table</span><span class="p">.</span><span class="n">schema</span><span class="p">}:</span>
                <span class="n">table</span><span class="p">.</span><span class="n">schema</span> <span class="o">=</span> <span class="p">[</span>
                    <span class="o">*</span><span class="n">table</span><span class="p">.</span><span class="n">schema</span><span class="p">,</span>
                    <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"cost_usd"</span><span class="p">,</span> <span class="s">"NUMERIC"</span><span class="p">),</span>
                <span class="p">]</span>
                <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">update_table</span><span class="p">(</span><span class="n">table</span><span class="p">,</span> <span class="p">[</span><span class="s">"schema"</span><span class="p">])</span>
        <span class="k">except</span> <span class="n">NotFound</span><span class="p">:</span>
            <span class="n">schema</span> <span class="o">=</span> <span class="p">[</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"event_id"</span><span class="p">,</span> <span class="s">"STRING"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"REQUIRED"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"ts"</span><span class="p">,</span> <span class="s">"TIMESTAMP"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"REQUIRED"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"copilot"</span><span class="p">,</span> <span class="s">"STRING"</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"REQUIRED"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"workflow"</span><span class="p">,</span> <span class="s">"STRING"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"freshness_tier"</span><span class="p">,</span> <span class="s">"STRING"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"input_bytes"</span><span class="p">,</span> <span class="s">"INT64"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"output_bytes"</span><span class="p">,</span> <span class="s">"INT64"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"tokens_in"</span><span class="p">,</span> <span class="s">"INT64"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"tokens_out"</span><span class="p">,</span> <span class="s">"INT64"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"latency_ms"</span><span class="p">,</span> <span class="s">"INT64"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"ai_enabled"</span><span class="p">,</span> <span class="s">"BOOL"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"corrections_needed"</span><span class="p">,</span> <span class="s">"INT64"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"outcome"</span><span class="p">,</span> <span class="s">"STRING"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"cost_usd"</span><span class="p">,</span> <span class="s">"NUMERIC"</span><span class="p">),</span>
                <span class="n">bigquery</span><span class="p">.</span><span class="n">SchemaField</span><span class="p">(</span><span class="s">"notes"</span><span class="p">,</span> <span class="s">"STRING"</span><span class="p">),</span>
            <span class="p">]</span>
            <span class="n">table</span> <span class="o">=</span> <span class="n">bigquery</span><span class="p">.</span><span class="n">Table</span><span class="p">(</span><span class="n">table_ref</span><span class="p">,</span> <span class="n">schema</span><span class="o">=</span><span class="n">schema</span><span class="p">)</span>
            <span class="n">table</span><span class="p">.</span><span class="n">time_partitioning</span> <span class="o">=</span> <span class="n">bigquery</span><span class="p">.</span><span class="n">TimePartitioning</span><span class="p">(</span><span class="n">field</span><span class="o">=</span><span class="s">"ts"</span><span class="p">)</span>
            <span class="n">table</span><span class="p">.</span><span class="n">clustering_fields</span> <span class="o">=</span> <span class="p">[</span><span class="s">"copilot"</span><span class="p">,</span> <span class="s">"outcome"</span><span class="p">]</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">create_table</span><span class="p">(</span><span class="n">table</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">log_event</span><span class="p">(</span>
        <span class="bp">self</span><span class="p">,</span>
        <span class="n">copilot</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
        <span class="n">workflow</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">freshness_tier</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"seconds"</span><span class="p">,</span>
        <span class="n">input_bytes</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">output_bytes</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">tokens_in</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">tokens_out</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">latency_ms</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">ai_enabled</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">True</span><span class="p">,</span>
        <span class="n">corrections_needed</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">outcome</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"success"</span><span class="p">,</span>
        <span class="n">cost_usd</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">float</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
        <span class="n">notes</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="s">"""
        Log an event to BigQuery.
        
        Returns:
            event_id: Unique identifier for the logged event
        """</span>
        <span class="n">event_id</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">uuid</span><span class="p">.</span><span class="n">uuid4</span><span class="p">())</span>
        
        <span class="n">row</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">"event_id"</span><span class="p">:</span> <span class="n">event_id</span><span class="p">,</span>
            <span class="s">"ts"</span><span class="p">:</span> <span class="n">datetime</span><span class="p">.</span><span class="n">utcnow</span><span class="p">().</span><span class="n">isoformat</span><span class="p">(),</span>
            <span class="s">"copilot"</span><span class="p">:</span> <span class="n">copilot</span><span class="p">,</span>
            <span class="s">"workflow"</span><span class="p">:</span> <span class="n">workflow</span><span class="p">,</span>
            <span class="s">"freshness_tier"</span><span class="p">:</span> <span class="n">freshness_tier</span><span class="p">,</span>
            <span class="s">"input_bytes"</span><span class="p">:</span> <span class="n">input_bytes</span><span class="p">,</span>
            <span class="s">"output_bytes"</span><span class="p">:</span> <span class="n">output_bytes</span><span class="p">,</span>
            <span class="s">"tokens_in"</span><span class="p">:</span> <span class="n">tokens_in</span><span class="p">,</span>
            <span class="s">"tokens_out"</span><span class="p">:</span> <span class="n">tokens_out</span><span class="p">,</span>
            <span class="s">"latency_ms"</span><span class="p">:</span> <span class="n">latency_ms</span><span class="p">,</span>
            <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="n">ai_enabled</span><span class="p">,</span>
            <span class="s">"corrections_needed"</span><span class="p">:</span> <span class="n">corrections_needed</span><span class="p">,</span>
            <span class="s">"outcome"</span><span class="p">:</span> <span class="n">outcome</span><span class="p">,</span>
            <span class="s">"cost_usd"</span><span class="p">:</span> <span class="n">cost_usd</span><span class="p">,</span>
            <span class="s">"notes"</span><span class="p">:</span> <span class="n">notes</span><span class="p">,</span>
        <span class="p">}</span>
        
        <span class="n">table_ref</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">dataset</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">dataset_id</span><span class="p">).</span><span class="n">table</span><span class="p">(</span><span class="s">"events"</span><span class="p">)</span>
        <span class="n">errors</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">insert_rows_json</span><span class="p">(</span><span class="n">table_ref</span><span class="p">,</span> <span class="p">[</span><span class="n">row</span><span class="p">])</span>
        
        <span class="k">if</span> <span class="n">errors</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="sa">f</span><span class="s">"Failed to insert row: </span><span class="si">{</span><span class="n">errors</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        
        <span class="k">return</span> <span class="n">event_id</span>
</code></pre></div></div>

<h3 id="telemetry-library">Telemetry Library</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">lib/telemetry.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Telemetry tracking for latency and token usage.
"""</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">Optional</span>

<span class="k">class</span> <span class="nc">Telemetry</span><span class="p">:</span>
    <span class="s">"""Track latency and token usage for AI operations."""</span>
    
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">start_time</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">float</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">end_time</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">float</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">tokens_in</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">tokens_out</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span>
    
    <span class="k">def</span> <span class="nf">__enter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
        <span class="k">return</span> <span class="bp">self</span>
    
    <span class="k">def</span> <span class="nf">__exit__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">exc_type</span><span class="p">,</span> <span class="n">exc_val</span><span class="p">,</span> <span class="n">exc_tb</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">end_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
    
    <span class="k">def</span> <span class="nf">track</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="s">"""Context manager for tracking latency."""</span>
        <span class="k">return</span> <span class="bp">self</span>
    
    <span class="k">def</span> <span class="nf">set_tokens</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tokens_in</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">tokens_out</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
        <span class="s">"""Set token usage."""</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">tokens_in</span> <span class="o">=</span> <span class="n">tokens_in</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">tokens_out</span> <span class="o">=</span> <span class="n">tokens_out</span>
    
    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">latency_ms</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
        <span class="s">"""Get latency in milliseconds."""</span>
        <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">start_time</span> <span class="ow">and</span> <span class="bp">self</span><span class="p">.</span><span class="n">end_time</span><span class="p">:</span>
            <span class="k">return</span> <span class="nb">int</span><span class="p">((</span><span class="bp">self</span><span class="p">.</span><span class="n">end_time</span> <span class="o">-</span> <span class="bp">self</span><span class="p">.</span><span class="n">start_time</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">None</span>
    
    <span class="k">def</span> <span class="nf">to_dict</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">any</span><span class="p">]:</span>
        <span class="s">"""Convert to dictionary."""</span>
        <span class="k">return</span> <span class="p">{</span>
            <span class="s">"latency_ms"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">latency_ms</span><span class="p">,</span>
            <span class="s">"tokens_in"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">tokens_in</span><span class="p">,</span>
            <span class="s">"tokens_out"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">tokens_out</span><span class="p">,</span>
        <span class="p">}</span>
</code></pre></div></div>

<h3 id="terraform-infrastructure">Terraform Infrastructure</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">infrastructure/modules/cloud_run/main.tf</code></p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"project_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"GCP Project ID"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"region"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"GCP Region"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">"europe-west4"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"service_name"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Cloud Run service name"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"image"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Container image URL"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"service_account_email"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Service account email"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"environment_variables"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Environment variables"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">map</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"min_instances"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Minimum number of instances"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">number</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="mi">0</span>  <span class="c1"># Scale to zero for cost optimization</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"max_instances"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Maximum number of instances"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">number</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="mi">3</span>  <span class="c1"># Limit max instances for cost control</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"google_cloud_run_service"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">service_name</span>
  <span class="nx">location</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">region</span>
  <span class="nx">project</span>  <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">project_id</span>
  <span class="nx">template</span> <span class="p">{</span>
    <span class="nx">spec</span> <span class="p">{</span>
      <span class="nx">service_account_name</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">service_account_email</span>
      
      <span class="nx">containers</span> <span class="p">{</span>
        <span class="nx">image</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">image</span>
        
        <span class="nx">resources</span> <span class="p">{</span>
          <span class="nx">limits</span> <span class="p">=</span> <span class="p">{</span>
            <span class="nx">cpu</span>    <span class="p">=</span> <span class="s2">"1"</span>
            <span class="nx">memory</span> <span class="p">=</span> <span class="s2">"512Mi"</span>
          <span class="p">}</span>
        <span class="p">}</span>
        
        <span class="nx">dynamic</span> <span class="s2">"env"</span> <span class="p">{</span>
          <span class="nx">for_each</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">environment_variables</span>
          <span class="nx">content</span> <span class="p">{</span>
            <span class="nx">name</span>  <span class="p">=</span> <span class="nx">env</span><span class="p">.</span><span class="nx">key</span>
            <span class="nx">value</span> <span class="p">=</span> <span class="nx">env</span><span class="p">.</span><span class="nx">value</span>
          <span class="p">}</span>
        <span class="p">}</span>
      <span class="p">}</span>
      
      <span class="nx">container_concurrency</span> <span class="p">=</span> <span class="mi">80</span>
      <span class="nx">timeout_seconds</span>       <span class="p">=</span> <span class="mi">300</span>
    <span class="p">}</span>
    
    <span class="nx">metadata</span> <span class="p">{</span>
      <span class="nx">annotations</span> <span class="p">=</span> <span class="p">{</span>
        <span class="s2">"autoscaling.knative.dev/minScale"</span> <span class="p">=</span> <span class="nx">tostring</span><span class="p">(</span><span class="kd">var</span><span class="p">.</span><span class="nx">min_instances</span><span class="p">)</span>
        <span class="s2">"autoscaling.knative.dev/maxScale"</span> <span class="p">=</span> <span class="nx">tostring</span><span class="p">(</span><span class="kd">var</span><span class="p">.</span><span class="nx">max_instances</span><span class="p">)</span>
      <span class="p">}</span>
      
      <span class="nx">labels</span> <span class="p">=</span> <span class="p">{</span>
        <span class="nx">environment</span> <span class="p">=</span> <span class="s2">"dev"</span>
        <span class="nx">service</span>     <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">service_name</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="nx">traffic</span> <span class="p">{</span>
    <span class="nx">percent</span>         <span class="p">=</span> <span class="mi">100</span>
    <span class="nx">latest_revision</span> <span class="p">=</span> <span class="kc">true</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Private access only (bearer token authentication)</span>
<span class="k">resource</span> <span class="s2">"google_cloud_run_service_iam_member"</span> <span class="s2">"authenticated_access"</span> <span class="p">{</span>
  <span class="nx">service</span>  <span class="p">=</span> <span class="nx">google_cloud_run_service</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span> <span class="p">=</span> <span class="nx">google_cloud_run_service</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">role</span>     <span class="p">=</span> <span class="s2">"roles/run.invoker"</span>
  <span class="nx">member</span>   <span class="p">=</span> <span class="s2">"serviceAccount:</span><span class="k">${</span><span class="kd">var</span><span class="p">.</span><span class="nx">service_account_email</span><span class="k">}</span><span class="s2">"</span>
<span class="p">}</span>

<span class="k">output</span> <span class="s2">"service_url"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Cloud Run service URL"</span>
  <span class="nx">value</span>       <span class="p">=</span> <span class="nx">google_cloud_run_service</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">status</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">url</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="bigquery-schema">BigQuery Schema</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">scripts/setup_bigquery.sql</code></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- BigQuery Setup Script for Copilot PoC</span>
<span class="c1">-- Dataset: copilot_poc</span>

<span class="c1">-- Create dataset</span>
<span class="c1">-- If copilot_poc already exists outside the EU, copy it to a new EU dataset;</span>
<span class="c1">-- CREATE SCHEMA IF NOT EXISTS does not change an existing dataset's location.</span>
<span class="k">CREATE</span> <span class="k">SCHEMA</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="nv">`copilot_poc`</span>
<span class="k">OPTIONS</span><span class="p">(</span>
  <span class="n">description</span><span class="o">=</span><span class="nv">"Dataset for storing copilot events and metrics"</span><span class="p">,</span>
  <span class="k">location</span><span class="o">=</span><span class="nv">"EU"</span>
<span class="p">);</span>

<span class="c1">-- Events table</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="nv">`copilot_poc.events`</span> <span class="p">(</span>
  <span class="n">event_id</span> <span class="n">STRING</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">ts</span> <span class="nb">TIMESTAMP</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">copilot</span> <span class="n">STRING</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">workflow</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">freshness_tier</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">input_bytes</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">output_bytes</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">tokens_in</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">tokens_out</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">latency_ms</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">ai_enabled</span> <span class="nb">BOOL</span><span class="p">,</span>
  <span class="n">corrections_needed</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">outcome</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">cost_usd</span> <span class="nb">NUMERIC</span><span class="p">,</span>
  <span class="n">notes</span> <span class="n">STRING</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">copilot</span><span class="p">,</span> <span class="n">outcome</span>
<span class="k">OPTIONS</span><span class="p">(</span>
  <span class="n">description</span><span class="o">=</span><span class="nv">"One row per copilot invocation with full telemetry"</span>
<span class="p">);</span>

<span class="c1">-- Migration for datasets created by an earlier version of this PoC</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="nv">`copilot_poc.events`</span>
<span class="k">ADD</span> <span class="k">COLUMN</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="n">cost_usd</span> <span class="nb">NUMERIC</span><span class="p">;</span>

<span class="c1">-- Paired human task measurements used for time-saved and edit-reduction gates</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="nv">`copilot_poc.experiment_trials`</span> <span class="p">(</span>
  <span class="n">trial_id</span> <span class="n">STRING</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">measured_at</span> <span class="nb">TIMESTAMP</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">copilot</span> <span class="n">STRING</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">baseline_task_ms</span> <span class="n">INT64</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">assisted_task_ms</span> <span class="n">INT64</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">baseline_corrections</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">assisted_corrections</span> <span class="n">INT64</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">measured_at</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">copilot</span><span class="p">;</span>

<span class="c1">-- Catalog table</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="nv">`copilot_poc.catalog`</span> <span class="p">(</span>
  <span class="n">workflow</span> <span class="n">STRING</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">copilot</span> <span class="n">STRING</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="k">owner</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">freshness_tier</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">sla_ms</span> <span class="n">INT64</span>
<span class="p">)</span>
<span class="k">OPTIONS</span><span class="p">(</span>
  <span class="n">description</span><span class="o">=</span><span class="nv">"Workflow registry with freshness tiers and SLAs"</span>
<span class="p">);</span>

<span class="c1">-- Upsert catalog entries</span>
<span class="n">MERGE</span> <span class="nv">`copilot_poc.catalog`</span> <span class="k">AS</span> <span class="n">target</span>
<span class="k">USING</span> <span class="k">UNNEST</span><span class="p">([</span>
  <span class="n">STRUCT</span><span class="p">(</span><span class="s1">'code-copilot-workflow'</span> <span class="k">AS</span> <span class="n">workflow</span><span class="p">,</span> <span class="s1">'code'</span> <span class="k">AS</span> <span class="n">copilot</span><span class="p">,</span> <span class="s1">'dev-team'</span> <span class="k">AS</span> <span class="k">owner</span><span class="p">,</span> <span class="s1">'seconds'</span> <span class="k">AS</span> <span class="n">freshness_tier</span><span class="p">,</span> <span class="mi">2000</span> <span class="k">AS</span> <span class="n">sla_ms</span><span class="p">),</span>
  <span class="n">STRUCT</span><span class="p">(</span><span class="s1">'reading-copilot-nightly'</span> <span class="k">AS</span> <span class="n">workflow</span><span class="p">,</span> <span class="s1">'reading'</span> <span class="k">AS</span> <span class="n">copilot</span><span class="p">,</span> <span class="s1">'dev-team'</span> <span class="k">AS</span> <span class="k">owner</span><span class="p">,</span> <span class="s1">'daily'</span> <span class="k">AS</span> <span class="n">freshness_tier</span><span class="p">,</span> <span class="mi">300000</span> <span class="k">AS</span> <span class="n">sla_ms</span><span class="p">),</span>
  <span class="n">STRUCT</span><span class="p">(</span><span class="s1">'meeting-copilot-workflow'</span> <span class="k">AS</span> <span class="n">workflow</span><span class="p">,</span> <span class="s1">'meeting'</span> <span class="k">AS</span> <span class="n">copilot</span><span class="p">,</span> <span class="s1">'dev-team'</span> <span class="k">AS</span> <span class="k">owner</span><span class="p">,</span> <span class="s1">'minutes'</span> <span class="k">AS</span> <span class="n">freshness_tier</span><span class="p">,</span> <span class="mi">180000</span> <span class="k">AS</span> <span class="n">sla_ms</span><span class="p">)</span>
<span class="p">])</span> <span class="k">AS</span> <span class="k">source</span>
<span class="k">ON</span> <span class="n">target</span><span class="p">.</span><span class="n">workflow</span> <span class="o">=</span> <span class="k">source</span><span class="p">.</span><span class="n">workflow</span>
<span class="k">WHEN</span> <span class="n">MATCHED</span> <span class="k">THEN</span>
  <span class="k">UPDATE</span> <span class="k">SET</span>
    <span class="n">copilot</span> <span class="o">=</span> <span class="k">source</span><span class="p">.</span><span class="n">copilot</span><span class="p">,</span>
    <span class="k">owner</span> <span class="o">=</span> <span class="k">source</span><span class="p">.</span><span class="k">owner</span><span class="p">,</span>
    <span class="n">freshness_tier</span> <span class="o">=</span> <span class="k">source</span><span class="p">.</span><span class="n">freshness_tier</span><span class="p">,</span>
    <span class="n">sla_ms</span> <span class="o">=</span> <span class="k">source</span><span class="p">.</span><span class="n">sla_ms</span>
<span class="k">WHEN</span> <span class="k">NOT</span> <span class="n">MATCHED</span> <span class="k">THEN</span>
  <span class="k">INSERT</span> <span class="p">(</span><span class="n">workflow</span><span class="p">,</span> <span class="n">copilot</span><span class="p">,</span> <span class="k">owner</span><span class="p">,</span> <span class="n">freshness_tier</span><span class="p">,</span> <span class="n">sla_ms</span><span class="p">)</span>
  <span class="k">VALUES</span> <span class="p">(</span><span class="k">source</span><span class="p">.</span><span class="n">workflow</span><span class="p">,</span> <span class="k">source</span><span class="p">.</span><span class="n">copilot</span><span class="p">,</span> <span class="k">source</span><span class="p">.</span><span class="k">owner</span><span class="p">,</span> <span class="k">source</span><span class="p">.</span><span class="n">freshness_tier</span><span class="p">,</span> <span class="k">source</span><span class="p">.</span><span class="n">sla_ms</span><span class="p">);</span>

<span class="c1">-- Metrics view (daily aggregates)</span>
<span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="nv">`copilot_poc.metrics`</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="k">as</span> <span class="nb">date</span><span class="p">,</span>
  <span class="n">copilot</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">as</span> <span class="n">total_events</span><span class="p">,</span>
  <span class="k">AVG</span><span class="p">(</span><span class="n">latency_ms</span><span class="p">)</span> <span class="k">as</span> <span class="n">avg_latency_ms</span><span class="p">,</span>
  <span class="n">APPROX_QUANTILES</span><span class="p">(</span><span class="n">latency_ms</span><span class="p">,</span> <span class="mi">100</span><span class="p">)[</span><span class="k">OFFSET</span><span class="p">(</span><span class="mi">95</span><span class="p">)]</span> <span class="k">as</span> <span class="n">p95_latency_ms</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">tokens_in</span><span class="p">)</span> <span class="k">as</span> <span class="n">total_tokens_in</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">tokens_out</span><span class="p">)</span> <span class="k">as</span> <span class="n">total_tokens_out</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">cost_usd</span><span class="p">)</span> <span class="k">as</span> <span class="n">total_cost_usd</span><span class="p">,</span>
  <span class="k">AVG</span><span class="p">(</span><span class="n">corrections_needed</span><span class="p">)</span> <span class="k">as</span> <span class="n">avg_corrections</span><span class="p">,</span>
  <span class="n">COUNTIF</span><span class="p">(</span><span class="n">outcome</span> <span class="o">=</span> <span class="s1">'success'</span><span class="p">)</span> <span class="k">as</span> <span class="n">success_count</span><span class="p">,</span>
  <span class="n">COUNTIF</span><span class="p">(</span><span class="n">outcome</span> <span class="o">=</span> <span class="s1">'fallback'</span><span class="p">)</span> <span class="k">as</span> <span class="n">fallback_count</span><span class="p">,</span>
  <span class="n">COUNTIF</span><span class="p">(</span><span class="n">outcome</span> <span class="o">=</span> <span class="s1">'error'</span><span class="p">)</span> <span class="k">as</span> <span class="n">error_count</span><span class="p">,</span>
  <span class="n">COUNTIF</span><span class="p">(</span><span class="n">outcome</span> <span class="o">=</span> <span class="s1">'success'</span><span class="p">)</span> <span class="o">/</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">as</span> <span class="n">success_rate</span>
<span class="k">FROM</span>
  <span class="nv">`copilot_poc.events`</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="nb">date</span><span class="p">,</span> <span class="n">copilot</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="nb">date</span> <span class="k">DESC</span><span class="p">,</span> <span class="n">copilot</span><span class="p">;</span>
</code></pre></div></div>

<hr />

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

<h3 id="unit-tests">Unit Tests</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">tests/test_kill_switch.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Unit tests for kill switch functionality.
"""</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">pytest</span>
<span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">patch</span><span class="p">,</span> <span class="n">MagicMock</span>
<span class="kn">from</span> <span class="nn">lib.kill_switch</span> <span class="kn">import</span> <span class="n">is_ai_enabled</span><span class="p">,</span> <span class="n">get_fallback_response</span><span class="p">,</span> <span class="n">with_kill_switch</span>

<span class="k">def</span> <span class="nf">test_is_ai_enabled_true</span><span class="p">():</span>
    <span class="s">"""Test AI enabled when environment variable is true."""</span>
    <span class="k">with</span> <span class="n">patch</span><span class="p">.</span><span class="nb">dict</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">,</span> <span class="p">{</span><span class="s">"AI_ENABLED"</span><span class="p">:</span> <span class="s">"true"</span><span class="p">}):</span>
        <span class="k">assert</span> <span class="n">is_ai_enabled</span><span class="p">()</span> <span class="ow">is</span> <span class="bp">True</span>

<span class="k">def</span> <span class="nf">test_is_ai_enabled_false</span><span class="p">():</span>
    <span class="s">"""Test AI disabled when environment variable is false."""</span>
    <span class="k">with</span> <span class="n">patch</span><span class="p">.</span><span class="nb">dict</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">,</span> <span class="p">{</span><span class="s">"AI_ENABLED"</span><span class="p">:</span> <span class="s">"false"</span><span class="p">}):</span>
        <span class="k">assert</span> <span class="n">is_ai_enabled</span><span class="p">()</span> <span class="ow">is</span> <span class="bp">False</span>

<span class="k">def</span> <span class="nf">test_is_ai_enabled_default</span><span class="p">():</span>
    <span class="s">"""Test AI enabled by default when environment variable is not set."""</span>
    <span class="k">with</span> <span class="n">patch</span><span class="p">.</span><span class="nb">dict</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">,</span> <span class="p">{},</span> <span class="n">clear</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span>
        <span class="k">assert</span> <span class="n">is_ai_enabled</span><span class="p">()</span> <span class="ow">is</span> <span class="bp">True</span>

<span class="k">def</span> <span class="nf">test_get_fallback_response_code</span><span class="p">():</span>
    <span class="s">"""Test fallback response for code copilot."""</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">get_fallback_response</span><span class="p">(</span><span class="s">"code"</span><span class="p">)</span>
    <span class="k">assert</span> <span class="s">"test_suggestions"</span> <span class="ow">in</span> <span class="n">response</span>
    <span class="k">assert</span> <span class="s">"refactor_hints"</span> <span class="ow">in</span> <span class="n">response</span>
    <span class="k">assert</span> <span class="s">"message"</span> <span class="ow">in</span> <span class="n">response</span>
    <span class="k">assert</span> <span class="n">response</span><span class="p">[</span><span class="s">"test_suggestions"</span><span class="p">]</span> <span class="o">==</span> <span class="p">[]</span>
    <span class="k">assert</span> <span class="n">response</span><span class="p">[</span><span class="s">"refactor_hints"</span><span class="p">]</span> <span class="o">==</span> <span class="p">[]</span>

<span class="k">def</span> <span class="nf">test_get_fallback_response_reading</span><span class="p">():</span>
    <span class="s">"""Test fallback response for reading copilot."""</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">get_fallback_response</span><span class="p">(</span><span class="s">"reading"</span><span class="p">)</span>
    <span class="k">assert</span> <span class="s">"digest"</span> <span class="ow">in</span> <span class="n">response</span>
    <span class="k">assert</span> <span class="s">"follow_up_prompts"</span> <span class="ow">in</span> <span class="n">response</span>
    <span class="k">assert</span> <span class="s">"message"</span> <span class="ow">in</span> <span class="n">response</span>

<span class="k">def</span> <span class="nf">test_with_kill_switch_disabled</span><span class="p">():</span>
    <span class="s">"""Test kill switch when AI is disabled."""</span>
    <span class="k">with</span> <span class="n">patch</span><span class="p">.</span><span class="nb">dict</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">,</span> <span class="p">{</span><span class="s">"AI_ENABLED"</span><span class="p">:</span> <span class="s">"false"</span><span class="p">}):</span>
        <span class="n">mock_function</span> <span class="o">=</span> <span class="n">MagicMock</span><span class="p">()</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">with_kill_switch</span><span class="p">(</span><span class="n">mock_function</span><span class="p">,</span> <span class="s">"code"</span><span class="p">)</span>
        
        <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"ai_enabled"</span><span class="p">]</span> <span class="ow">is</span> <span class="bp">False</span>
        <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"outcome"</span><span class="p">]</span> <span class="o">==</span> <span class="s">"fallback"</span>
        <span class="k">assert</span> <span class="s">"result"</span> <span class="ow">in</span> <span class="n">result</span>
        <span class="n">mock_function</span><span class="p">.</span><span class="n">assert_not_called</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">test_with_kill_switch_enabled_success</span><span class="p">():</span>
    <span class="s">"""Test kill switch when AI is enabled and succeeds."""</span>
    <span class="k">with</span> <span class="n">patch</span><span class="p">.</span><span class="nb">dict</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">,</span> <span class="p">{</span><span class="s">"AI_ENABLED"</span><span class="p">:</span> <span class="s">"true"</span><span class="p">}):</span>
        <span class="n">mock_function</span> <span class="o">=</span> <span class="n">MagicMock</span><span class="p">(</span><span class="n">return_value</span><span class="o">=</span><span class="p">{</span><span class="s">"test"</span><span class="p">:</span> <span class="s">"result"</span><span class="p">})</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">with_kill_switch</span><span class="p">(</span><span class="n">mock_function</span><span class="p">,</span> <span class="s">"code"</span><span class="p">)</span>
        
        <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"ai_enabled"</span><span class="p">]</span> <span class="ow">is</span> <span class="bp">True</span>
        <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"outcome"</span><span class="p">]</span> <span class="o">==</span> <span class="s">"success"</span>
        <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"result"</span><span class="p">]</span> <span class="o">==</span> <span class="p">{</span><span class="s">"test"</span><span class="p">:</span> <span class="s">"result"</span><span class="p">}</span>
        <span class="n">mock_function</span><span class="p">.</span><span class="n">assert_called_once</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">test_with_kill_switch_enabled_error</span><span class="p">():</span>
    <span class="s">"""Test kill switch when AI is enabled but fails."""</span>
    <span class="k">with</span> <span class="n">patch</span><span class="p">.</span><span class="nb">dict</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">,</span> <span class="p">{</span><span class="s">"AI_ENABLED"</span><span class="p">:</span> <span class="s">"true"</span><span class="p">}):</span>
        <span class="n">mock_function</span> <span class="o">=</span> <span class="n">MagicMock</span><span class="p">(</span><span class="n">side_effect</span><span class="o">=</span><span class="nb">Exception</span><span class="p">(</span><span class="s">"AI error"</span><span class="p">))</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">with_kill_switch</span><span class="p">(</span><span class="n">mock_function</span><span class="p">,</span> <span class="s">"code"</span><span class="p">)</span>
        
        <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"ai_enabled"</span><span class="p">]</span> <span class="ow">is</span> <span class="bp">True</span>
        <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"outcome"</span><span class="p">]</span> <span class="o">==</span> <span class="s">"error"</span>
        <span class="k">assert</span> <span class="s">"error"</span> <span class="ow">in</span> <span class="n">result</span>
        <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"error"</span><span class="p">]</span> <span class="o">==</span> <span class="s">"AI error"</span>
</code></pre></div></div>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">tests/test_ai_client.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Unit tests for AI client abstraction.
"""</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">pytest</span>
<span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">patch</span><span class="p">,</span> <span class="n">MagicMock</span>
<span class="kn">from</span> <span class="nn">lib.ai_client</span> <span class="kn">import</span> <span class="n">get_ai_client</span><span class="p">,</span> <span class="n">VertexAIClient</span><span class="p">,</span> <span class="n">AIProvider</span>

<span class="o">@</span><span class="n">patch</span><span class="p">(</span><span class="s">'lib.ai_client.aiplatform'</span><span class="p">)</span>
<span class="o">@</span><span class="n">patch</span><span class="p">(</span><span class="s">'lib.ai_client.GenerativeModel'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_vertex_ai_client_init</span><span class="p">(</span><span class="n">mock_model_class</span><span class="p">,</span> <span class="n">mock_aiplatform</span><span class="p">):</span>
    <span class="s">"""Test Vertex AI client initialization."""</span>
    <span class="n">mock_model</span> <span class="o">=</span> <span class="n">MagicMock</span><span class="p">()</span>
    <span class="n">mock_model_class</span><span class="p">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="n">mock_model</span>
    
    <span class="k">with</span> <span class="n">patch</span><span class="p">.</span><span class="nb">dict</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">,</span> <span class="p">{</span><span class="s">"GCP_PROJECT_ID"</span><span class="p">:</span> <span class="s">"test-project"</span><span class="p">}):</span>
        <span class="n">client</span> <span class="o">=</span> <span class="n">VertexAIClient</span><span class="p">(</span>
            <span class="n">project_id</span><span class="o">=</span><span class="s">"test-project"</span><span class="p">,</span>
            <span class="n">location</span><span class="o">=</span><span class="s">"europe-west4"</span><span class="p">,</span>
            <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.5-flash"</span>
        <span class="p">)</span>
        
        <span class="k">assert</span> <span class="n">client</span><span class="p">.</span><span class="n">project_id</span> <span class="o">==</span> <span class="s">"test-project"</span>
        <span class="k">assert</span> <span class="n">client</span><span class="p">.</span><span class="n">location</span> <span class="o">==</span> <span class="s">"europe-west4"</span>
        <span class="k">assert</span> <span class="n">client</span><span class="p">.</span><span class="n">model_name</span> <span class="o">==</span> <span class="s">"gemini-2.5-flash"</span>
        <span class="n">mock_aiplatform</span><span class="p">.</span><span class="n">init</span><span class="p">.</span><span class="n">assert_called_once</span><span class="p">()</span>
        <span class="n">mock_model_class</span><span class="p">.</span><span class="n">assert_called_once_with</span><span class="p">(</span><span class="s">"gemini-2.5-flash"</span><span class="p">)</span>

<span class="o">@</span><span class="n">patch</span><span class="p">(</span><span class="s">'lib.ai_client.aiplatform'</span><span class="p">)</span>
<span class="o">@</span><span class="n">patch</span><span class="p">(</span><span class="s">'lib.ai_client.GenerativeModel'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_vertex_ai_client_generate</span><span class="p">(</span><span class="n">mock_model_class</span><span class="p">,</span> <span class="n">mock_aiplatform</span><span class="p">):</span>
    <span class="s">"""Test Vertex AI client generate method."""</span>
    <span class="n">mock_model</span> <span class="o">=</span> <span class="n">MagicMock</span><span class="p">()</span>
    <span class="n">mock_response</span> <span class="o">=</span> <span class="n">MagicMock</span><span class="p">()</span>
    <span class="n">mock_response</span><span class="p">.</span><span class="n">text</span> <span class="o">=</span> <span class="s">"Test response"</span>
    <span class="n">mock_response</span><span class="p">.</span><span class="n">usage_metadata</span><span class="p">.</span><span class="n">prompt_token_count</span> <span class="o">=</span> <span class="mi">20</span>
    <span class="n">mock_response</span><span class="p">.</span><span class="n">usage_metadata</span><span class="p">.</span><span class="n">candidates_token_count</span> <span class="o">=</span> <span class="mi">10</span>
    <span class="n">mock_response</span><span class="p">.</span><span class="n">usage_metadata</span><span class="p">.</span><span class="n">thoughts_token_count</span> <span class="o">=</span> <span class="mi">5</span>
    <span class="n">mock_model</span><span class="p">.</span><span class="n">generate_content</span><span class="p">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="n">mock_response</span>
    <span class="n">mock_model_class</span><span class="p">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="n">mock_model</span>
    
    <span class="k">with</span> <span class="n">patch</span><span class="p">.</span><span class="nb">dict</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">,</span> <span class="p">{</span><span class="s">"GCP_PROJECT_ID"</span><span class="p">:</span> <span class="s">"test-project"</span><span class="p">}):</span>
        <span class="n">client</span> <span class="o">=</span> <span class="n">VertexAIClient</span><span class="p">()</span>
        <span class="n">response</span><span class="p">,</span> <span class="n">token_usage</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span>
            <span class="n">prompt</span><span class="o">=</span><span class="s">"Test prompt"</span><span class="p">,</span>
            <span class="n">system_prompt</span><span class="o">=</span><span class="s">"System prompt"</span><span class="p">,</span>
            <span class="n">max_tokens</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span>
            <span class="n">temperature</span><span class="o">=</span><span class="mf">0.7</span>
        <span class="p">)</span>
        
        <span class="k">assert</span> <span class="n">response</span> <span class="o">==</span> <span class="s">"Test response"</span>
        <span class="k">assert</span> <span class="n">token_usage</span> <span class="o">==</span> <span class="p">{</span><span class="s">"tokens_in"</span><span class="p">:</span> <span class="mi">20</span><span class="p">,</span> <span class="s">"tokens_out"</span><span class="p">:</span> <span class="mi">15</span><span class="p">}</span>
        <span class="n">mock_model</span><span class="p">.</span><span class="n">generate_content</span><span class="p">.</span><span class="n">assert_called_once</span><span class="p">()</span>
</code></pre></div></div>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">tests/test_code_copilot.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Unit tests for code copilot.
"""</span>
<span class="kn">import</span> <span class="nn">pytest</span>
<span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">patch</span><span class="p">,</span> <span class="n">MagicMock</span>
<span class="kn">from</span> <span class="nn">copilots.code_copilot.copilot</span> <span class="kn">import</span> <span class="n">CodeCopilot</span>

<span class="o">@</span><span class="n">patch</span><span class="p">(</span><span class="s">'copilots.code_copilot.copilot.get_ai_client'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_code_copilot_analyze_code</span><span class="p">(</span><span class="n">mock_get_client</span><span class="p">):</span>
    <span class="s">"""Test code copilot analyze_code method."""</span>
    <span class="n">mock_client</span> <span class="o">=</span> <span class="n">MagicMock</span><span class="p">()</span>
    <span class="n">mock_client</span><span class="p">.</span><span class="n">generate</span><span class="p">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="p">(</span>
        <span class="s">'{"test_suggestions": ["Test 1", "Test 2"], "refactor_hints": ["Hint 1"]}'</span><span class="p">,</span>
        <span class="p">{</span><span class="s">"tokens_in"</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span> <span class="s">"tokens_out"</span><span class="p">:</span> <span class="mi">50</span><span class="p">}</span>
    <span class="p">)</span>
    <span class="n">mock_get_client</span><span class="p">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="n">mock_client</span>
    
    <span class="n">copilot</span> <span class="o">=</span> <span class="n">CodeCopilot</span><span class="p">()</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">copilot</span><span class="p">.</span><span class="n">analyze_code</span><span class="p">(</span><span class="s">"def test(): pass"</span><span class="p">)</span>
    
    <span class="k">assert</span> <span class="s">"test_suggestions"</span> <span class="ow">in</span> <span class="n">result</span>
    <span class="k">assert</span> <span class="s">"refactor_hints"</span> <span class="ow">in</span> <span class="n">result</span>
    <span class="k">assert</span> <span class="s">"telemetry"</span> <span class="ow">in</span> <span class="n">result</span>
    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="p">[</span><span class="s">"test_suggestions"</span><span class="p">])</span> <span class="o">==</span> <span class="mi">2</span>
    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="p">[</span><span class="s">"refactor_hints"</span><span class="p">])</span> <span class="o">==</span> <span class="mi">1</span>
    <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"telemetry"</span><span class="p">][</span><span class="s">"tokens_in"</span><span class="p">]</span> <span class="o">==</span> <span class="mi">100</span>
    <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"telemetry"</span><span class="p">][</span><span class="s">"tokens_out"</span><span class="p">]</span> <span class="o">==</span> <span class="mi">50</span>
</code></pre></div></div>

<h3 id="integration-tests">Integration Tests</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">tests/integration/test_copilot_integration.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Integration tests for copilot services.
"""</span>
<span class="kn">import</span> <span class="nn">pytest</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">patch</span>
<span class="kn">from</span> <span class="nn">fastapi.testclient</span> <span class="kn">import</span> <span class="n">TestClient</span>
<span class="kn">from</span> <span class="nn">copilots.code_copilot.main</span> <span class="kn">import</span> <span class="n">app</span>

<span class="o">@</span><span class="n">pytest</span><span class="p">.</span><span class="n">fixture</span>
<span class="k">def</span> <span class="nf">client</span><span class="p">():</span>
    <span class="s">"""Create test client."""</span>
    <span class="k">return</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">test_health_endpoint</span><span class="p">(</span><span class="n">client</span><span class="p">):</span>
    <span class="s">"""Test health endpoint."""</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"/health"</span><span class="p">)</span>
    <span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
    <span class="n">data</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()</span>
    <span class="k">assert</span> <span class="s">"status"</span> <span class="ow">in</span> <span class="n">data</span>
    <span class="k">assert</span> <span class="s">"ai_enabled"</span> <span class="ow">in</span> <span class="n">data</span>

<span class="o">@</span><span class="n">patch</span><span class="p">(</span><span class="s">'copilots.code_copilot.main.get_bq_client'</span><span class="p">)</span>
<span class="o">@</span><span class="n">patch</span><span class="p">.</span><span class="nb">dict</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">,</span> <span class="p">{</span><span class="s">"AI_ENABLED"</span><span class="p">:</span> <span class="s">"false"</span><span class="p">})</span>
<span class="k">def</span> <span class="nf">test_suggest_endpoint_kill_switch</span><span class="p">(</span><span class="n">mock_get_bq_client</span><span class="p">,</span> <span class="n">client</span><span class="p">):</span>
    <span class="s">"""Test suggest endpoint with kill switch disabled."""</span>
    <span class="n">mock_get_bq_client</span><span class="p">.</span><span class="n">return_value</span><span class="p">.</span><span class="n">log_event</span><span class="p">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="s">"test-event"</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span>
        <span class="s">"/suggest"</span><span class="p">,</span>
        <span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="s">"code"</span><span class="p">:</span> <span class="s">"def test(): pass"</span><span class="p">}</span>
    <span class="p">)</span>
    <span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
    <span class="n">data</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()</span>
    <span class="k">assert</span> <span class="n">data</span><span class="p">[</span><span class="s">"ai_enabled"</span><span class="p">]</span> <span class="ow">is</span> <span class="bp">False</span>
    <span class="k">assert</span> <span class="n">data</span><span class="p">[</span><span class="s">"outcome"</span><span class="p">]</span> <span class="o">==</span> <span class="s">"fallback"</span>
</code></pre></div></div>

<h3 id="performance-tests">Performance Tests</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">tests/performance/test_latency.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Performance tests for latency requirements.
"""</span>
<span class="kn">import</span> <span class="nn">pytest</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">from</span> <span class="nn">copilots.code_copilot.copilot</span> <span class="kn">import</span> <span class="n">CodeCopilot</span>

<span class="k">def</span> <span class="nf">test_code_copilot_latency_seconds_tier</span><span class="p">():</span>
    <span class="s">"""Test code copilot meets seconds-tier latency requirement."""</span>
    <span class="n">copilot</span> <span class="o">=</span> <span class="n">CodeCopilot</span><span class="p">()</span>
    <span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">copilot</span><span class="p">.</span><span class="n">analyze_code</span><span class="p">(</span><span class="s">"def test(): pass"</span><span class="p">)</span>
    <span class="n">elapsed_ms</span> <span class="o">=</span> <span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1000</span>
    
    <span class="c1"># Seconds tier: p95 &lt; 2000ms
</span>    <span class="k">assert</span> <span class="n">elapsed_ms</span> <span class="o">&lt;</span> <span class="mi">2000</span><span class="p">,</span> <span class="sa">f</span><span class="s">"Latency </span><span class="si">{</span><span class="n">elapsed_ms</span><span class="si">}</span><span class="s">ms exceeds 2000ms SLA"</span>
    <span class="k">assert</span> <span class="s">"telemetry"</span> <span class="ow">in</span> <span class="n">result</span>
    <span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"telemetry"</span><span class="p">][</span><span class="s">"latency_ms"</span><span class="p">]</span> <span class="o">&lt;</span> <span class="mi">2000</span>
</code></pre></div></div>

<h3 id="end-to-end-tests">End-to-End Tests</h3>

<p><strong>File</strong>: <code class="language-plaintext highlighter-rouge">tests/e2e/test_full_workflow.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
End-to-end tests for full copilot workflow.
"""</span>
<span class="kn">import</span> <span class="nn">pytest</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">import</span> <span class="nn">os</span>

<span class="o">@</span><span class="n">pytest</span><span class="p">.</span><span class="n">mark</span><span class="p">.</span><span class="n">e2e</span>
<span class="k">def</span> <span class="nf">test_code_copilot_e2e</span><span class="p">():</span>
    <span class="s">"""Test full code copilot workflow end-to-end."""</span>
    <span class="n">service_url</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"CODE_COPILOT_URL"</span><span class="p">,</span> <span class="s">"http://localhost:8080"</span><span class="p">)</span>
    
    <span class="c1"># Test health check
</span>    <span class="n">health_response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">service_url</span><span class="si">}</span><span class="s">/health"</span><span class="p">)</span>
    <span class="k">assert</span> <span class="n">health_response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
    
    <span class="c1"># Test code analysis
</span>    <span class="n">code</span> <span class="o">=</span> <span class="s">"""
def calculate_total(items):
    total = 0
    for item in items:
        total += item.price
    return total
"""</span>
    <span class="n">suggest_response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">post</span><span class="p">(</span>
        <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">service_url</span><span class="si">}</span><span class="s">/suggest"</span><span class="p">,</span>
        <span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="s">"code"</span><span class="p">:</span> <span class="n">code</span><span class="p">}</span>
    <span class="p">)</span>
    <span class="k">assert</span> <span class="n">suggest_response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
    <span class="n">data</span> <span class="o">=</span> <span class="n">suggest_response</span><span class="p">.</span><span class="n">json</span><span class="p">()</span>
    <span class="k">assert</span> <span class="s">"result"</span> <span class="ow">in</span> <span class="n">data</span>
    <span class="k">assert</span> <span class="s">"event_id"</span> <span class="ow">in</span> <span class="n">data</span>
</code></pre></div></div>

<hr />

<h2 id="measurement--metrics">Measurement &amp; Metrics</h2>

<p>The time-saving and edit-reduction gates use paired manual task runs stored in <code class="language-plaintext highlighter-rouge">experiment_trials</code>. Model inference latency in <code class="language-plaintext highlighter-rouge">events</code> is a service metric; it is not treated as human time saved.</p>

<h3 id="bigquery-queries">BigQuery Queries</h3>

<p><strong>Daily Metrics:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="nv">`copilot_poc.metrics`</span>
<span class="k">WHERE</span> <span class="nb">date</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="nb">date</span> <span class="k">DESC</span><span class="p">,</span> <span class="n">copilot</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Time Saved Calculation:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">paired_trials</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">copilot</span><span class="p">,</span>
    <span class="mi">100</span> <span class="o">*</span> <span class="n">SAFE_DIVIDE</span><span class="p">(</span>
      <span class="n">baseline_task_ms</span> <span class="o">-</span> <span class="n">assisted_task_ms</span><span class="p">,</span>
      <span class="n">baseline_task_ms</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="n">time_saved_percent</span>
  <span class="k">FROM</span> <span class="nv">`copilot_poc.experiment_trials`</span>
  <span class="k">WHERE</span> <span class="n">baseline_task_ms</span> <span class="o">&gt;</span> <span class="mi">0</span>
    <span class="k">AND</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">measured_at</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">copilot</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">paired_runs</span><span class="p">,</span>
  <span class="n">APPROX_QUANTILES</span><span class="p">(</span><span class="n">time_saved_percent</span><span class="p">,</span> <span class="mi">100</span><span class="p">)[</span><span class="k">OFFSET</span><span class="p">(</span><span class="mi">50</span><span class="p">)]</span>
    <span class="k">AS</span> <span class="n">median_time_saved_percent</span>
<span class="k">FROM</span> <span class="n">paired_trials</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">copilot</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Cost per 1K Predictions:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">copilot</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">as</span> <span class="n">total_predictions</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">cost_usd</span><span class="p">)</span> <span class="k">as</span> <span class="n">total_cost_usd</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">cost_usd</span><span class="p">)</span> <span class="o">/</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1000</span> <span class="k">as</span> <span class="n">cost_per_1k_predictions_usd</span>
<span class="k">FROM</span>
  <span class="nv">`copilot_poc.events`</span>
<span class="k">WHERE</span>
  <span class="n">outcome</span> <span class="o">=</span> <span class="s1">'success'</span>
  <span class="k">AND</span> <span class="n">cost_usd</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">NULL</span>
  <span class="k">AND</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="n">copilot</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Kill Switch Effectiveness:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="k">as</span> <span class="nb">date</span><span class="p">,</span>
  <span class="n">copilot</span><span class="p">,</span>
  <span class="n">COUNTIF</span><span class="p">(</span><span class="n">outcome</span> <span class="o">=</span> <span class="s1">'fallback'</span><span class="p">)</span> <span class="k">as</span> <span class="n">fallback_count</span><span class="p">,</span>
  <span class="n">COUNTIF</span><span class="p">(</span><span class="n">outcome</span> <span class="o">=</span> <span class="s1">'success'</span><span class="p">)</span> <span class="k">as</span> <span class="n">success_count</span><span class="p">,</span>
  <span class="n">COUNTIF</span><span class="p">(</span><span class="n">outcome</span> <span class="o">=</span> <span class="s1">'fallback'</span><span class="p">)</span> <span class="o">/</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span> <span class="k">as</span> <span class="n">fallback_rate</span>
<span class="k">FROM</span>
  <span class="nv">`copilot_poc.events`</span>
<span class="k">WHERE</span>
  <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="nb">date</span><span class="p">,</span> <span class="n">copilot</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="nb">date</span> <span class="k">DESC</span><span class="p">,</span> <span class="n">copilot</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Quality Metrics (Edits Reduced):</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">paired_trials</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">copilot</span><span class="p">,</span>
    <span class="mi">100</span> <span class="o">*</span> <span class="n">SAFE_DIVIDE</span><span class="p">(</span>
      <span class="n">baseline_corrections</span> <span class="o">-</span> <span class="n">assisted_corrections</span><span class="p">,</span>
      <span class="n">baseline_corrections</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="n">edits_reduced_percent</span>
  <span class="k">FROM</span> <span class="nv">`copilot_poc.experiment_trials`</span>
  <span class="k">WHERE</span> <span class="n">baseline_corrections</span> <span class="o">&gt;</span> <span class="mi">0</span>
    <span class="k">AND</span> <span class="n">assisted_corrections</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">NULL</span>
    <span class="k">AND</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">measured_at</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">copilot</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">paired_runs</span><span class="p">,</span>
  <span class="n">APPROX_QUANTILES</span><span class="p">(</span><span class="n">edits_reduced_percent</span><span class="p">,</span> <span class="mi">100</span><span class="p">)[</span><span class="k">OFFSET</span><span class="p">(</span><span class="mi">50</span><span class="p">)]</span>
    <span class="k">AS</span> <span class="n">median_edits_reduced_percent</span>
<span class="k">FROM</span> <span class="n">paired_trials</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">copilot</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="results">Results</h2>

<h3 id="reading-copilot">Reading Copilot</h3>

<p><strong>Hypothesis</strong>: H1 (time saved), H2 (edits reduced), H3 (cost)</p>

<p><strong>Results:</strong></p>

<ul>
  <li><strong>Time saved</strong>: 17% (median over 5 runs)</li>
  <li><strong>Edits reduced</strong>: 21% vs. baseline</li>
  <li><strong>Cost</strong>: $0.01/task (daily batch)</li>
  <li><strong>p95 latency</strong>: 45 seconds (within daily tier SLA)</li>
  <li><strong>Status</strong>: <strong>PASSED</strong> - Kept</li>
</ul>

<p><strong>Analysis:</strong></p>

<p>Daily batch processing eliminated real-time costs while meeting the “fresh enough” requirement. The 10-bullet digest format reduced cognitive load and editing time.</p>

<h3 id="meeting-copilot">Meeting Copilot</h3>

<p><strong>Hypothesis</strong>: H1 (time saved), H2 (edits reduced), H4 (latency)</p>

<p><strong>Results:</strong></p>

<ul>
  <li><strong>Time saved</strong>: 20% (median over 5 runs)</li>
  <li><strong>Edits reduced</strong>: 22% vs. baseline</li>
  <li><strong>p95 latency</strong>: 2.5 minutes (within minutes tier SLA)</li>
  <li><strong>Cost</strong>: $0.02/task</li>
  <li><strong>Status</strong>: <strong>PASSED</strong> - Kept</li>
</ul>

<p><strong>Analysis:</strong></p>

<p>Structured action items with owners and deadlines reduced follow-up time. The minutes-tier freshness met the use case without requiring real-time processing.</p>

<h3 id="code-copilot">Code Copilot</h3>

<p><strong>Hypothesis</strong>: H1 (time saved), H2 (edits reduced), H4 (latency)</p>

<p><strong>Results:</strong></p>

<ul>
  <li><strong>Time saved</strong>: 8% (median over 5 runs) - <strong>Below 15% threshold</strong></li>
  <li><strong>Edits reduced</strong>: 12% vs. baseline - <strong>Below 20% threshold</strong></li>
  <li><strong>p95 latency</strong>: 1.8 seconds (within seconds tier SLA)</li>
  <li><strong>Cost</strong>: $0.005/task</li>
  <li><strong>Status</strong>: <strong>FAILED</strong> - Retired</li>
</ul>

<p><strong>Analysis:</strong></p>

<p>While latency was acceptable, the time saved and edit reduction didn’t meet the success gates. The copilot was helpful but not transformative enough to justify keeping. Future experiments with on-device models or better prompts may improve results.</p>

<h3 id="kill-switch">Kill Switch</h3>

<p><strong>Hypothesis</strong>: H5 (reversibility)</p>

<p><strong>Results:</strong></p>

<ul>
  <li><strong>Fallback success rate</strong>: 100% (10/10 tests)</li>
  <li><strong>Response time</strong>: &lt;50ms (deterministic fallback)</li>
  <li><strong>Logging</strong>: All fallback events logged to BigQuery</li>
  <li><strong>Status</strong>: <strong>PASSED</strong></li>
</ul>

<p><strong>Analysis:</strong></p>

<p>The kill switch provided clean reversibility with zero AI API calls when disabled. All fallback events were logged, enabling audit trails.</p>

<hr />

<h2 id="governance--trust">Governance &amp; Trust</h2>

<h3 id="model-card-ai-copilots-poc">Model Card: AI Copilots PoC</h3>

<h4 id="model-details">Model Details</h4>

<ul>
  <li><strong>Provider</strong>: Google Vertex AI (Gemini 2.5 Flash)</li>
  <li><strong>Locations</strong>: <code class="language-plaintext highlighter-rouge">europe-west4</code> for compute; <code class="language-plaintext highlighter-rouge">EU</code> multi-region for BigQuery</li>
  <li><strong>Use Case</strong>: Personal workflow assistance (reading, meetings, code)</li>
</ul>

<h4 id="intended-use">Intended Use</h4>

<ul>
  <li><strong>Reading Copilot</strong>: Generate 10-bullet digests from documents</li>
  <li><strong>Meeting Copilot</strong>: Extract action items with owners and deadlines</li>
  <li><strong>Code Copilot</strong>: Suggest test cases and refactoring hints</li>
</ul>

<h4 id="limitations">Limitations</h4>

<ul>
  <li>No business data or PII processing</li>
  <li>Personal use only</li>
  <li>Outputs require human review</li>
  <li>No guarantees on accuracy or completeness</li>
</ul>

<h4 id="failure-modes">Failure Modes</h4>

<ul>
  <li>API outages -&gt; Error fallback; operator can activate the kill switch</li>
  <li>Cost spikes -&gt; Budget alert; operator can activate the kill switch</li>
  <li>Quality regression -&gt; Manual review and kill switch if needed</li>
</ul>

<h4 id="bias--safety">Bias &amp; Safety</h4>

<ul>
  <li>Prompts designed to avoid absolute claims</li>
  <li>Citations encouraged when possible</li>
  <li>Human-in-the-loop for all outputs</li>
</ul>

<h3 id="bias-checklist">Bias Checklist</h3>

<h4 id="pre-deployment">Pre-Deployment</h4>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Prompts reviewed for bias</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Output formats structured to reduce interpretation errors</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Kill switch tested and documented</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Audit logs enabled</li>
</ul>

<h4 id="during-operation">During Operation</h4>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Monitor for quality regression</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Track corrections_needed metric</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Review fallback events</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Cost monitoring alerts</li>
</ul>

<h4 id="post-deployment">Post-Deployment</h4>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Review outcomes weekly</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Compare against baseline metrics</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Document failures and learnings</li>
</ul>

<hr />

<h2 id="what-failed--why">What Failed &amp; Why</h2>

<h3 id="code-copilot-retirement">Code Copilot Retirement</h3>

<p><strong>Why it failed:</strong></p>

<ul>
  <li>Time saved (8%) below 15% threshold</li>
  <li>Edits reduced (12%) below 20% threshold</li>
  <li>Helpful but not transformative</li>
</ul>

<p><strong>Lessons:</strong></p>

<ul>
  <li>Not all tasks need generative AI</li>
  <li>Right-time beats real-time (reading copilot succeeded with daily batch)</li>
  <li>Small scope wins (easier to measure and decide)</li>
</ul>

<p><strong>Next steps:</strong></p>

<ul>
  <li>Experiment with on-device models for lower latency/privacy</li>
  <li>Try different prompts or fine-tuning</li>
  <li>Consider rule-based alternatives for code analysis</li>
</ul>

<hr />

<h2 id="reusable-pattern">Reusable Pattern</h2>

<h3 id="one-page-playbook">One-Page Playbook</h3>

<p><strong>1. Start Tiny</strong></p>

<ul>
  <li>One copilot, one endpoint, one metric table</li>
  <li>Measure baseline honestly (5+ runs)</li>
</ul>

<p><strong>2. Map Freshness to Decisions</strong></p>

<ul>
  <li>Seconds: User waiting (interactive)</li>
  <li>Minutes: Acceptable delay (post-processing)</li>
  <li>Daily: No user waiting (batch)</li>
</ul>

<p><strong>3. Add Kill Switch</strong></p>

<ul>
  <li>Environment variable: <code class="language-plaintext highlighter-rouge">AI_ENABLED=false</code></li>
  <li>Deterministic fallback responses</li>
  <li>Log all fallback events</li>
</ul>

<p><strong>4. Measure Strictly</strong></p>

<ul>
  <li>Time saved (median over 5+ runs)</li>
  <li>Edits reduced vs. baseline</li>
  <li>Cost per task</li>
  <li>p95 latency within SLA</li>
</ul>

<p><strong>5. Keep Only What Beats Control</strong></p>

<ul>
  <li>≥15% time saved</li>
  <li>≥20% fewer edits</li>
  <li>Stable cost</li>
  <li>Stop the rest</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>This PoC proved that <strong>small, measurable AI helpers can deliver real value</strong> when:</p>

<ul>
  <li>Freshness is mapped to decisions (right-time, not real-time)</li>
  <li>Success gates are strict (≥15% time saved, ≥20% fewer edits)</li>
  <li>A kill switch provides reversibility</li>
  <li>Cost is predictable and monitored</li>
</ul>

<p><strong>Two copilots passed, one failed and was retired.</strong> The pattern is portable, governed, and repeatable - ready to template for other workflows.</p>

<hr />

<p><strong>Author</strong>: Dr. Atabak Kheirkhah<br />
<strong>Date</strong>: November 17, 2025<br />
<strong>Contact</strong>: atabakkheirkhah@gmail.com</p>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Before AI, the Data Platform Has to Hold</title>
					
					<link>https://atabakkh.github.io/cloud/2025/10/08/before-ai-fix-your-data-platform.html</link>

					

					<pubDate>Wed, 08 Oct 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2025/10/08/before-ai-fix-your-data-platform</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>Most AI projects fail before the model is the problem. They fail because the platform underneath cannot answer freshness, ownership, lineage, or trust. This is the scorecard I keep coming back to when serious AI or agent work is on the table.</em></p>

<p><strong>Context:</strong> By autumn 2025 enterprise AI had moved from “build a demo” toward “run in production”. Agents and RAG stacks were no longer exotic. In programs I touched, the bottleneck was rarely the model. It was whether the platform could feed, govern, and measure AI reliably.</p>

<p>When nobody could say when a table was last updated, who owned it, or whether yesterday’s numbers reconciled with finance, adding a vector database or an agent framework did not fix that. It amplified the mess.</p>

<p>Notes from migration work, batch scoring pipelines, and personal lab experiments on public cloud - not from any employer context.</p>

<hr />

<h2 id="the-pattern-i-kept-seeing">The pattern I kept seeing</h2>

<ol>
  <li>Leadership asks for a copilot, recommender, or agent that “uses our data”</li>
  <li>Team spins up Vertex AI, OpenAI, or an internal gateway quickly</li>
  <li>Demo works on a curated sample</li>
  <li>Production breaks on freshness, permissions, definitions, lineage, cost, or silent pipeline failures from three weeks ago</li>
</ol>

<p>The model was never the first failure. The platform was. AI exposes weak data platforms faster than BI ever did.</p>

<hr />

<h2 id="what-ai-ready-meant-in-practice">What “AI-ready” meant in practice</h2>

<p><img src="/img/diagrams/data-platform-layers.svg" alt="AI-ready data platform layers" width="750" /></p>

<p>Not “we bought a lakehouse” or “we deployed an LLM”.</p>

<p>It meant the platform could consistently provide:</p>

<table>
  <thead>
    <tr>
      <th>Capability</th>
      <th>Why it mattered</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Trusted inputs</td>
      <td>Models amplify bad data</td>
    </tr>
    <tr>
      <td>Known freshness</td>
      <td>Retrieval and features need “as of when?”</td>
    </tr>
    <tr>
      <td>Clear ownership</td>
      <td>Someone must fix breaks</td>
    </tr>
    <tr>
      <td>Lineage and audit</td>
      <td>Incidents and compliance need provenance</td>
    </tr>
    <tr>
      <td>Predictable cost</td>
      <td>AI multiplies reads and enrichment</td>
    </tr>
    <tr>
      <td>Safe access boundaries</td>
      <td>Agents need least privilege, not warehouse admin</td>
    </tr>
  </tbody>
</table>

<p>If you cannot do this for a batch report, you will not do it for production AI.</p>

<hr />

<h2 id="five-foundations-that-had-to-move-first">Five foundations that had to move first</h2>

<p><strong>1. Layered architecture.</strong> Landing -&gt; curated -&gt; serving -&gt; feature/context. Red flag: every AI project rebuilds “customer 360” from scratch. Publish serving tables and contracts for entities AI will touch.</p>

<p><strong>2. Quality with teeth.</strong> Freshness SLAs, reconciliation, schema drift detection, quarantine - not a dashboard that says 98% complete while the missing 2% is revenue or consent.</p>

<p><strong>3. Orchestration you can explain.</strong> One source of truth per domain, explicit dependencies, idempotent partitions, run metadata. Honest freshness tiers: seconds / minutes / daily - not fake real-time everywhere.</p>

<p><strong>4. Ownership, lineage, access.</strong> Business owner, technical owner, access class. Least privilege for humans, services, and agents. An agent with broad warehouse access and no approval flow is an exfiltration pattern waiting for a prompt.</p>

<p><strong>5. Cost as design.</strong> Partition for real filters. Materialize heavy joins. Budgets and kill switches. AI makes expensive mistakes show up weekly, not quarterly.</p>

<hr />

<h2 id="scorecard-i-still-use">Scorecard I still use</h2>

<p>Architecture: layered paths · serving matches access patterns · versioned feature/context<br />
Quality: freshness · reconciliation · drift/quarantine<br />
Operations: real dependency graph · idempotent partitions · runbooks with owners<br />
Governance: named owners · lineage to model input · least privilege<br />
Economics: cost-reviewed patterns · freshness tied to schedules · budgets and kill switches</p>

<p>12-15 -&gt; production-ready. 8-11 -&gt; bounded pilots. Below 8 -&gt; fix the platform first.</p>

<p>I kept a rough score sheet as YAML for intake reviews (personal notes / lab style):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">use_case</span><span class="pi">:</span> <span class="s">support_case_summary</span>
<span class="na">datasets</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">curated.cases</span>
    <span class="na">freshness_sla_min</span><span class="pi">:</span> <span class="m">60</span>
    <span class="na">owner_business</span><span class="pi">:</span> <span class="s">support-ops</span>
    <span class="na">owner_technical</span><span class="pi">:</span> <span class="s">data-platform</span>
    <span class="na">score_architecture</span><span class="pi">:</span> <span class="m">2</span>   <span class="c1"># 0-3</span>
    <span class="na">score_quality</span><span class="pi">:</span> <span class="m">2</span>
    <span class="na">score_ops</span><span class="pi">:</span> <span class="m">1</span>
    <span class="na">score_governance</span><span class="pi">:</span> <span class="m">2</span>
    <span class="na">score_economics</span><span class="pi">:</span> <span class="m">2</span>
<span class="na">total</span><span class="pi">:</span> <span class="m">9</span>   <span class="c1"># bounded pilot only until ops improves</span>
</code></pre></div></div>

<hr />

<h2 id="first-90-days-when-recovery-was-possible">First 90 days when recovery was possible</h2>

<p><strong>Weeks 1-4:</strong> Inventory AI-critical datasets; freshness and ownership metadata; draw the real pipeline graph; reconcile top revenue/compliance tables.</p>

<p><strong>Weeks 5-8:</strong> Kill duplicate pipelines; publish serving tables; drift checks; access classes.</p>

<p><strong>Weeks 9-12:</strong> Version feature tables; audit automated retrieval; cost ceilings; contracts for agent entities.</p>

<hr />

<h2 id="from-the-leadership-seat">From the leadership seat</h2>

<p>When leadership asked to “do AI” this quarter:</p>

<p>I would not approve model spend before a platform scorecard on the datasets in scope. Below 8 -&gt; bounded pilot only, with a named platform track in parallel.</p>

<p>I tried to split capacity: a real share of the initiative for ownership, contracts, freshness, access - packaged as part of the AI work so it did not get deprioritized as “platform”.</p>

<p>Kill criteria upfront - time saved, override rate, cost ceiling, reversibility.</p>

<p>One executive for definitions when marketing, finance, and product disagree on “customer” or “revenue”. That is not an LLM problem.</p>

<p>Protect the best engineer from hero mode. If one person is the only one who understands the graph, you are person-dependent, not AI-ready.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Production AI needs a platform that can answer: what data was used, how fresh, who owns it, what happens when it breaks, what it cost. Those answers had to come first.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Cloud Run: Concurrency, Min/Max Instances, and Cold Start Tuning</title>
					
					<link>https://atabakkh.github.io/devops/2025/08/23/cloud-run-scaling-concurrency.html</link>

					

					<pubDate>Sat, 23 Aug 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/devops/2025/08/23/cloud-run-scaling-concurrency</guid>

					
						<category><![CDATA[ DevOps ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Problem:</strong> Either you pay too much (min too high) or you get tail latency (min too low). Balance it.</p>

<p>Cloud Run’s serverless model is powerful, but misconfigured concurrency and instance settings can lead to either excessive costs or poor user experience. This guide walks through practical tuning strategies based on real production workloads.</p>

<hr />

<h2 id="understanding-cloud-runs-scaling-model">Understanding Cloud Run’s Scaling Model</h2>

<p>Cloud Run scales instances based on <strong>request concurrency</strong>-the number of simultaneous requests each container instance handles. Unlike traditional autoscaling that uses CPU/memory metrics, Cloud Run scales when:</p>

<ol>
  <li><strong>All instances are at capacity</strong> (concurrency limit reached)</li>
  <li><strong>New requests arrive</strong> (queue forms)</li>
  <li><strong>Traffic increases</strong> (more instances needed)</li>
</ol>

<p>The key insight: <strong>concurrency is your primary cost lever</strong>. Higher concurrency = fewer instances = lower cost, but only if your application can handle it without degrading latency.</p>

<hr />

<h2 id="settings-that-matter">Settings that Matter</h2>

<h3 id="1-concurrency-the-primary-cost-control">1. Concurrency: The Primary Cost Control</h3>

<p><strong>Concurrency</strong> determines how many requests each instance handles simultaneously. This is your biggest cost optimization opportunity.</p>

<p><strong>Starting Points:</strong></p>
<ul>
  <li><strong>I/O-bound services</strong> (APIs, web servers): Start with <strong>40-80</strong> concurrent requests</li>
  <li><strong>CPU-bound services</strong> (ML inference, data processing): Start with <strong>4-8</strong> concurrent requests</li>
  <li><strong>Mixed workloads</strong>: Start with <strong>20-40</strong> and tune based on metrics</li>
</ul>

<p><strong>Why it matters:</strong></p>
<ul>
  <li>Concurrency of 1 = every request gets its own instance (expensive, fast)</li>
  <li>Concurrency of 1000 = one instance handles everything (cheap, but may queue)</li>
</ul>

<p><strong>Real-world example:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># High-traffic API (I/O-bound, database queries)</span>
gcloud run deploy api <span class="se">\</span>
  <span class="nt">--concurrency</span><span class="o">=</span>80 <span class="se">\</span>
  <span class="nt">--cpu</span><span class="o">=</span>2 <span class="se">\</span>
  <span class="nt">--memory</span><span class="o">=</span>2Gi

<span class="c"># ML inference service (CPU-bound, model predictions)</span>
gcloud run deploy ml-service <span class="se">\</span>
  <span class="nt">--concurrency</span><span class="o">=</span>4 <span class="se">\</span>
  <span class="nt">--cpu</span><span class="o">=</span>4 <span class="se">\</span>
  <span class="nt">--memory</span><span class="o">=</span>8Gi
</code></pre></div></div>

<p><strong>Tuning strategy:</strong></p>
<ol>
  <li>Start conservative (lower concurrency)</li>
  <li>Monitor p95/p99 latency during peak traffic</li>
  <li>Gradually increase concurrency until latency degrades</li>
  <li>Back off 20% from the breaking point</li>
</ol>

<hr />

<h3 id="2-min-instances-eliminating-cold-starts">2. Min Instances: Eliminating Cold Starts</h3>

<p><strong>Min instances</strong> keep containers warm, eliminating cold start latency for critical paths.</p>

<p><strong>When to use:</strong></p>
<ul>
  <li><strong>User-facing APIs</strong> with strict latency SLAs (&lt; 200ms p95)</li>
  <li><strong>High-traffic endpoints</strong> that can’t tolerate cold starts</li>
  <li><strong>Business-critical services</strong> during peak hours</li>
</ul>

<p><strong>Cost trade-off:</strong></p>
<ul>
  <li>Min instances = 0: Pay only for requests, but cold starts on first request</li>
  <li>Min instances = 5: Always paying for 5 instances, but zero cold starts</li>
</ul>

<p><strong>Practical guidance:</strong></p>
<ul>
  <li><strong>Critical APIs</strong>: 1-3 min instances</li>
  <li><strong>Background jobs</strong>: 0 min instances (cold starts acceptable)</li>
  <li><strong>Peak hours only</strong>: Use Cloud Scheduler to adjust min instances by time</li>
</ul>

<p><strong>Example: Business hours only</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># During business hours (9 AM - 6 PM)</span>
gcloud run services update api <span class="se">\</span>
  <span class="nt">--min-instances</span><span class="o">=</span>3 <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>europe-west3

<span class="c"># After hours (via Cloud Scheduler)</span>
gcloud run services update api <span class="se">\</span>
  <span class="nt">--min-instances</span><span class="o">=</span>0 <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>europe-west3
</code></pre></div></div>

<p><strong>Monitoring cold starts:</strong></p>
<ul>
  <li>Track <code class="language-plaintext highlighter-rouge">run.googleapis.com/container/startup_latencies</code> metric</li>
  <li>Alert if cold start p95 &gt; 1 second</li>
  <li>Use Cloud Run’s “startup CPU boost” to reduce cold start time</li>
</ul>

<hr />

<h3 id="3-max-instances-cost-protection">3. Max Instances: Cost Protection</h3>

<p><strong>Max instances</strong> caps your maximum spend and protects downstream dependencies.</p>

<p><strong>Why it matters:</strong></p>
<ul>
  <li><strong>Cost control</strong>: Prevents runaway scaling during traffic spikes</li>
  <li><strong>Dependency protection</strong>: Protects databases, APIs from overload</li>
  <li><strong>Budget guardrails</strong>: Hard limit on concurrent instances</li>
</ul>

<p><strong>Setting max instances:</strong></p>
<ul>
  <li><strong>Start with 2-3x your peak traffic</strong> (measured in requests/second)</li>
  <li><strong>Consider downstream limits</strong>: Database connection pools, API rate limits</li>
  <li><strong>Add 50% buffer</strong> for traffic spikes</li>
</ul>

<p><strong>Example calculation:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Peak traffic: 1000 req/s
Concurrency: 40 req/instance
Required instances: 1000 / 40 = 25 instances
Max instances: 25 * 1.5 = 38 (round to 40)
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud run deploy api <span class="se">\</span>
  <span class="nt">--max-instances</span><span class="o">=</span>40 <span class="se">\</span>
  <span class="nt">--concurrency</span><span class="o">=</span>40
</code></pre></div></div>

<p><strong>What happens when max is reached:</strong></p>
<ul>
  <li>New requests return <strong>429 Too Many Requests</strong></li>
  <li>Consider implementing <strong>exponential backoff</strong> in clients</li>
  <li>Monitor <code class="language-plaintext highlighter-rouge">run.googleapis.com/request_count</code> to detect saturation</li>
</ul>

<hr />

<h3 id="4-startup-cpu-boost-faster-cold-starts">4. Startup CPU Boost: Faster Cold Starts</h3>

<p><strong>Startup CPU boost</strong> allocates extra CPU during container startup, reducing cold start time.</p>

<p><strong>When to enable:</strong></p>
<ul>
  <li>Cold starts &gt; 500ms</li>
  <li>Applications with heavy initialization (imports, model loading)</li>
  <li>Services with strict latency SLAs</li>
</ul>

<p><strong>Cost impact:</strong></p>
<ul>
  <li>Only charged during startup (typically 1-5 seconds)</li>
  <li>Minimal cost increase, significant latency improvement</li>
</ul>

<p><strong>Enable it:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud run deploy api <span class="se">\</span>
  <span class="nt">--cpu-boost</span> <span class="se">\</span>
  <span class="nt">--cpu</span><span class="o">=</span>2 <span class="se">\</span>
  <span class="nt">--memory</span><span class="o">=</span>2Gi
</code></pre></div></div>

<p><strong>Expected improvement:</strong></p>
<ul>
  <li><strong>Without boost</strong>: 2-5 second cold starts</li>
  <li><strong>With boost</strong>: 0.5-2 second cold starts</li>
  <li><strong>Warm instances</strong>: &lt; 100ms (no cold start)</li>
</ul>

<hr />

<h2 id="complete-deployment-example">Complete Deployment Example</h2>

<p>Here’s a production-ready configuration for a typical API service:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud run deploy api <span class="se">\</span>
  <span class="nt">--image</span><span class="o">=</span>gcr.io/my-project/api:latest <span class="se">\</span>
  <span class="nt">--concurrency</span><span class="o">=</span>40 <span class="se">\</span>
  <span class="nt">--min-instances</span><span class="o">=</span>2 <span class="se">\</span>
  <span class="nt">--max-instances</span><span class="o">=</span>50 <span class="se">\</span>
  <span class="nt">--cpu</span><span class="o">=</span>2 <span class="se">\</span>
  <span class="nt">--memory</span><span class="o">=</span>2Gi <span class="se">\</span>
  <span class="nt">--cpu-boost</span> <span class="se">\</span>
  <span class="nt">--timeout</span><span class="o">=</span>300 <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>europe-west3 <span class="se">\</span>
  <span class="nt">--allow-unauthenticated</span>
</code></pre></div></div>

<p><strong>Configuration rationale:</strong></p>
<ul>
  <li><strong>Concurrency 40</strong>: I/O-bound API, handles database queries efficiently</li>
  <li><strong>Min instances 2</strong>: Eliminates cold starts for critical user-facing API</li>
  <li><strong>Max instances 50</strong>: Handles 2000 req/s peak (50 * 40 = 2000)</li>
  <li><strong>CPU boost</strong>: Reduces cold start from 3s to 1s</li>
  <li><strong>2 CPU, 2Gi memory</strong>: Sufficient for 40 concurrent requests with headroom</li>
</ul>

<hr />

<h2 id="measuring-and-iterating">Measuring and Iterating</h2>

<h3 id="key-metrics-to-track">Key Metrics to Track</h3>

<ol>
  <li><strong>Request latency</strong> (p50, p95, p99)
    <ul>
      <li>Monitor: <code class="language-plaintext highlighter-rouge">run.googleapis.com/request_latencies</code></li>
      <li>Alert if p95 &gt; 250ms (adjust based on your SLA)</li>
    </ul>
  </li>
  <li><strong>Concurrency utilization</strong>
    <ul>
      <li>Monitor: <code class="language-plaintext highlighter-rouge">run.googleapis.com/request_count</code> / <code class="language-plaintext highlighter-rouge">run.googleapis.com/container_instance_count</code></li>
      <li>Target: 70-80% of concurrency limit (leaves headroom for spikes)</li>
    </ul>
  </li>
  <li><strong>Cold start frequency</strong>
    <ul>
      <li>Monitor: <code class="language-plaintext highlighter-rouge">run.googleapis.com/container/startup_latencies</code></li>
      <li>Track: % of requests hitting cold starts</li>
      <li>Goal: &lt; 1% of requests (with min instances)</li>
    </ul>
  </li>
  <li><strong>Instance count</strong>
    <ul>
      <li>Monitor: <code class="language-plaintext highlighter-rouge">run.googleapis.com/container_instance_count</code></li>
      <li>Correlate with traffic patterns and cost</li>
    </ul>
  </li>
  <li><strong>CPU and memory utilization</strong>
    <ul>
      <li>Monitor: <code class="language-plaintext highlighter-rouge">run.googleapis.com/container/cpu/utilizations</code></li>
      <li>Monitor: <code class="language-plaintext highlighter-rouge">run.googleapis.com/container/memory/utilizations</code></li>
      <li>Target: &lt; 70% average (allows burst capacity)</li>
    </ul>
  </li>
</ol>

<h3 id="iterative-tuning-process">Iterative Tuning Process</h3>

<p><strong>Week 1: Baseline</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Start conservative</span>
<span class="nt">--concurrency</span><span class="o">=</span>20 <span class="se">\</span>
<span class="nt">--min-instances</span><span class="o">=</span>0 <span class="se">\</span>
<span class="nt">--max-instances</span><span class="o">=</span>20
</code></pre></div></div>

<p><strong>Week 2: Optimize concurrency</strong></p>
<ul>
  <li>Increase concurrency by 10 each day</li>
  <li>Monitor latency degradation</li>
  <li>Stop when p95 latency increases &gt; 20%</li>
</ul>

<p><strong>Week 3: Add min instances</strong></p>
<ul>
  <li>Set min instances = 1-2 for critical paths</li>
  <li>Measure cold start reduction</li>
  <li>Validate cost increase is acceptable</li>
</ul>

<p><strong>Week 4: Fine-tune max instances</strong></p>
<ul>
  <li>Analyze peak traffic patterns</li>
  <li>Set max = 2x observed peak</li>
  <li>Add alerts for 429 responses</li>
</ul>

<hr />

<h2 id="common-pitfalls-and-solutions">Common Pitfalls and Solutions</h2>

<h3 id="pitfall-1-concurrency-too-high">Pitfall 1: Concurrency Too High</h3>

<p><strong>Symptoms:</strong></p>
<ul>
  <li>High p95/p99 latency</li>
  <li>Request timeouts</li>
  <li>Memory pressure</li>
</ul>

<p><strong>Solution:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Reduce concurrency</span>
gcloud run services update api <span class="se">\</span>
  <span class="nt">--concurrency</span><span class="o">=</span>20  <span class="c"># Down from 80</span>
</code></pre></div></div>

<h3 id="pitfall-2-min-instances-too-high">Pitfall 2: Min Instances Too High</h3>

<p><strong>Symptoms:</strong></p>
<ul>
  <li>High idle costs</li>
  <li>Paying for unused capacity</li>
</ul>

<p><strong>Solution:</strong></p>
<ul>
  <li>Use Cloud Scheduler to adjust min instances by time</li>
  <li>Set min = 0 for non-critical services</li>
  <li>Monitor actual traffic patterns</li>
</ul>

<h3 id="pitfall-3-max-instances-too-low">Pitfall 3: Max Instances Too Low</h3>

<p><strong>Symptoms:</strong></p>
<ul>
  <li>429 errors during traffic spikes</li>
  <li>Users experiencing failures</li>
</ul>

<p><strong>Solution:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Increase max instances</span>
gcloud run services update api <span class="se">\</span>
  <span class="nt">--max-instances</span><span class="o">=</span>100  <span class="c"># Up from 50</span>
</code></pre></div></div>

<h3 id="pitfall-4-ignoring-cold-starts">Pitfall 4: Ignoring Cold Starts</h3>

<p><strong>Symptoms:</strong></p>
<ul>
  <li>Sporadic high latency (first request after idle)</li>
  <li>User complaints about slow responses</li>
</ul>

<p><strong>Solution:</strong></p>
<ul>
  <li>Enable CPU boost</li>
  <li>Set min instances = 1-2</li>
  <li>Implement health checks to keep instances warm</li>
</ul>

<hr />

<h2 id="advanced-time-based-scaling">Advanced: Time-Based Scaling</h2>

<p>For services with predictable traffic patterns, adjust min instances by time of day:</p>

<p><strong>Cloud Scheduler jobs:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Morning ramp-up (8 AM)</span>
gcloud scheduler <span class="nb">jobs </span>create http scale-up-morning <span class="se">\</span>
  <span class="nt">--schedule</span><span class="o">=</span><span class="s2">"0 8 * * *"</span> <span class="se">\</span>
  <span class="nt">--uri</span><span class="o">=</span><span class="s2">"https://europe-west3-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/my-project/services/api"</span> <span class="se">\</span>
  <span class="nt">--http-method</span><span class="o">=</span>PATCH <span class="se">\</span>
  <span class="nt">--oauth-service-account-email</span><span class="o">=</span>cloud-run-scheduler@my-project.iam.gserviceaccount.com <span class="se">\</span>
  <span class="nt">--message-body</span><span class="o">=</span><span class="s1">'{"spec":{"template":{"spec":{"containerConcurrency":40,"minScale":5}}}}'</span>

<span class="c"># Evening scale-down (8 PM)</span>
gcloud scheduler <span class="nb">jobs </span>create http scale-down-evening <span class="se">\</span>
  <span class="nt">--schedule</span><span class="o">=</span><span class="s2">"0 20 * * *"</span> <span class="se">\</span>
  <span class="nt">--uri</span><span class="o">=</span><span class="s2">"https://europe-west3-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/my-project/services/api"</span> <span class="se">\</span>
  <span class="nt">--http-method</span><span class="o">=</span>PATCH <span class="se">\</span>
  <span class="nt">--oauth-service-account-email</span><span class="o">=</span>cloud-run-scheduler@my-project.iam.gserviceaccount.com <span class="se">\</span>
  <span class="nt">--message-body</span><span class="o">=</span><span class="s1">'{"spec":{"template":{"spec":{"containerConcurrency":40,"minScale":1}}}}'</span>
</code></pre></div></div>

<hr />

<h2 id="cost-optimization-checklist">Cost Optimization Checklist</h2>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Concurrency tuned</strong>: 70-80% utilization during peak</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Min instances optimized</strong>: Only where cold starts hurt</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Max instances set</strong>: Based on actual peak traffic + 50% buffer</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>CPU boost enabled</strong>: For services with &gt; 500ms cold starts</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Time-based scaling</strong>: Reduce min instances during off-hours</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Monitoring alerts</strong>: Set up for latency, saturation, cold starts</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Cost tracking</strong>: Monitor spend per service, per endpoint</li>
</ul>

<hr />

<h2 id="expected-outcomes">Expected Outcomes</h2>

<p>With proper tuning, you should see:</p>

<ul>
  <li><strong>20-40% cost reduction</strong> (from optimized concurrency)</li>
  <li><strong>&lt; 1% cold start rate</strong> (from min instances)</li>
  <li><strong>p95 latency &lt; 250ms</strong> (from proper concurrency)</li>
  <li><strong>Zero 429 errors</strong> (from appropriate max instances)</li>
  <li><strong>Predictable costs</strong> (from max instance caps)</li>
</ul>

<p><strong>Outcome:</strong> Lower cost with fewer tail-latency surprises-no code rewrite required.</p>

<hr />

<h2 id="next-steps">Next Steps</h2>

<p>Want the dashboard/runbook templates?</p>
<ul>
  <li><a href="mailto:atabakkheirkhah@gmail.com">Book a 20-min intro</a></li>
</ul>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Use p95 to Tune Autoscaling and Cloud Cost (No Rewrite)</title>
					
					<link>https://atabakkh.github.io/cloud/2025/08/15/p95-autoscaling-saves-cost.html</link>

					

					<pubDate>Fri, 15 Aug 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2025/08/15/p95-autoscaling-saves-cost</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p>Many teams scale only on <strong>average CPU</strong>. That is easy, but it may miss queues and tail latency. Use demand signals such as queue depth or request rate to scale, then use your <strong>p95 latency SLO</strong> to validate and tune the policy.</p>

<h2 id="why-cpu-avg-wastes-money">Why CPU avg wastes money</h2>
<ul>
  <li>CPU utilization is not a direct measure of user experience</li>
  <li>CPU alone does not capture request <strong>queuing</strong>, downstream waits, or tail latency</li>
  <li>Burst patterns can make a policy scale <strong>late</strong> and stay <strong>high</strong> for too long</li>
  <li>You pay more while still missing p95 targets</li>
</ul>

<h2 id="the-p95-aware-approach-6-steps">The p95-aware approach (6 steps)</h2>
<p>1) <strong>Define SLO</strong> (e.g., 95% of requests &lt; 250 ms)
2) Add instrumentation for <strong>p95 latency</strong> (per service/endpoint)
3) Configure autoscaling on an actionable demand metric such as <strong>request rate or queue depth</strong>
4) Add <strong>hysteresis</strong> and <strong>cool-down</strong> to avoid flapping
5) Protect queues with <strong>lag thresholds</strong> and exponential backoff
6) Set <strong>cost guardrails</strong> (budgets, quotas, anomaly detection)</p>

<h2 id="what-changes-in-practice">What changes in practice</h2>
<ul>
  <li>Scale <strong>before</strong> queue explosion; scale <strong>down</strong> when tails calm</li>
  <li>Fewer <strong>retry storms</strong> and less error amplification</li>
  <li>Better <strong>cost/perf ratio</strong> with the same infrastructure</li>
</ul>

<h2 id="quick-wins-1-2-weeks">Quick wins (1-2 weeks)</h2>
<ul>
  <li>Export p95 via OTel/Datadog; create a custom metric</li>
  <li>Tune HPA policies with demand metrics; keep p95 as the SLO guardrail</li>
  <li>Right-size instances; fix one noisy endpoint</li>
  <li>Add daily budget alerts + lifecycle rules for logs</li>
</ul>

<h3 id="example-kubernetes-hpa-request-rate-metric">Example: Kubernetes HPA (request-rate metric)</h3>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">autoscaling/v2</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">HorizontalPodAutoscaler</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">api-hpa</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span>
    <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">api</span>
  <span class="na">minReplicas</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">maxReplicas</span><span class="pi">:</span> <span class="m">20</span>
  <span class="na">behavior</span><span class="pi">:</span>
    <span class="na">scaleDown</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">stabilizationWindowSeconds</span><span class="pi">:</span> <span class="nv">300</span> <span class="pi">}</span>
  <span class="na">metrics</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Pods</span>
    <span class="na">pods</span><span class="pi">:</span>
      <span class="na">metric</span><span class="pi">:</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">http_requests_per_second</span>
      <span class="na">target</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">AverageValue</span>
        <span class="na">averageValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">50"</span>
</code></pre></div></div>

<h3 id="example-queue-scaling-with-keda-pubsubrabbitkafka">Example: Queue scaling with KEDA (Pub/Sub/Rabbit/Kafka)</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">keda.sh/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ScaledObject</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">worker-queue</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">worker</span>
  <span class="na">triggers</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">prometheus</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">serverAddress</span><span class="pi">:</span> <span class="s">http://prometheus:9090</span>
      <span class="na">metricName</span><span class="pi">:</span> <span class="s">queue_lag</span>
      <span class="na">threshold</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1000"</span>
      <span class="na">query</span><span class="pi">:</span> <span class="s">sum(queue_lag{consumer_group="worker-queue"})</span>
</code></pre></div></div>

<h2 id="what-to-measure">What to measure</h2>

<ul>
  <li>p95 during <strong>burst</strong> and <strong>steady</strong> windows</li>
  <li>Queue <strong>lag vs. consumer throughput</strong></li>
  <li><strong>Spend / 1000 requests</strong> (cost-to-serve)</li>
</ul>

<p><strong>Possible result:</strong> lower cost and fewer latency incidents without an application rewrite. The outcome depends on workload shape, metric delay, and the original scaling policy, so measure against a baseline rather than assuming a fixed saving.</p>

<hr />

<h2 id="illustrative-example-before-and-after">Illustrative Example: Before and After</h2>

<p><strong>Before (CPU-based scaling):</strong></p>
<ul>
  <li>Average CPU: 45%</li>
  <li>p95 latency: 450ms (SLO: 250ms)</li>
  <li>Instances: 10-25 (thrashing)</li>
  <li>Monthly cost: €2,400</li>
  <li>Incidents: 3 per month (latency spikes)</li>
</ul>

<p><strong>After (demand-based scaling, validated against p95):</strong></p>
<ul>
  <li>Average CPU: 60% (higher utilization)</li>
  <li>p95 latency: 180ms (within SLO)</li>
  <li>Instances: 5-12 (stable)</li>
  <li>Monthly cost: €1,680 (30% reduction)</li>
  <li>Incidents: 0 per month</li>
</ul>

<p><strong>Key insight:</strong> CPU doesn’t reflect user experience. p95 latency does.</p>

<p>These numbers illustrate how to compare policies; they are not a benchmark or a guaranteed outcome.</p>

<hr />

<h2 id="implementation-guide">Implementation Guide</h2>

<h3 id="step-1-instrument-p95-latency">Step 1: Instrument p95 Latency</h3>

<p><strong>For Kubernetes (Prometheus):</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Add to your deployment</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">api</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">prometheus.io/scrape</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">prometheus.io/port</span><span class="pi">:</span> <span class="s2">"</span><span class="s">9090"</span>
    <span class="na">prometheus.io/path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/metrics"</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">ports</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
    <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>In your application code (Go example):</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">metrics</span>

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

  <span class="s">"github.com/prometheus/client_golang/prometheus"</span>
  <span class="s">"github.com/prometheus/client_golang/prometheus/promauto"</span>
<span class="p">)</span>

<span class="k">var</span> <span class="n">httpRequestDuration</span> <span class="o">=</span> <span class="n">promauto</span><span class="o">.</span><span class="n">NewHistogramVec</span><span class="p">(</span>
  <span class="n">prometheus</span><span class="o">.</span><span class="n">HistogramOpts</span><span class="p">{</span>
    <span class="n">Name</span><span class="o">:</span>    <span class="s">"http_request_duration_seconds"</span><span class="p">,</span>
    <span class="n">Help</span><span class="o">:</span>    <span class="s">"HTTP request duration in seconds"</span><span class="p">,</span>
    <span class="n">Buckets</span><span class="o">:</span> <span class="p">[]</span><span class="kt">float64</span><span class="p">{</span><span class="m">0.01</span><span class="p">,</span> <span class="m">0.05</span><span class="p">,</span> <span class="m">0.1</span><span class="p">,</span> <span class="m">0.25</span><span class="p">,</span> <span class="m">0.5</span><span class="p">,</span> <span class="m">1.0</span><span class="p">,</span> <span class="m">2.5</span><span class="p">,</span> <span class="m">5.0</span><span class="p">},</span>
  <span class="p">},</span>
  <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"method"</span><span class="p">,</span> <span class="s">"endpoint"</span><span class="p">,</span> <span class="s">"status"</span><span class="p">},</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">handler</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="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">statusCode</span> <span class="o">:=</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span>

  <span class="c">// ... your handler logic ...</span>

  <span class="n">duration</span> <span class="o">:=</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="n">httpRequestDuration</span><span class="o">.</span><span class="n">WithLabelValues</span><span class="p">(</span>
    <span class="n">r</span><span class="o">.</span><span class="n">Method</span><span class="p">,</span>
    <span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span>
    <span class="n">strconv</span><span class="o">.</span><span class="n">Itoa</span><span class="p">(</span><span class="n">statusCode</span><span class="p">),</span>
  <span class="p">)</span><span class="o">.</span><span class="n">Observe</span><span class="p">(</span><span class="n">duration</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>For Cloud Run / Cloud Functions:</strong></p>
<ul>
  <li>Use OpenTelemetry or Cloud Monitoring client libraries</li>
  <li>Export custom metrics to Cloud Monitoring</li>
  <li>Create custom metrics from request logs</li>
</ul>

<h3 id="step-2-create-scaling-and-slo-metrics">Step 2: Create Scaling and SLO Metrics</h3>

<p><strong>Prometheus recording rule:</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">groups</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">slo</span>
  <span class="na">interval</span><span class="pi">:</span> <span class="s">30s</span>
  <span class="na">rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">record</span><span class="pi">:</span> <span class="s">http_requests_per_second</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">sum(rate(http_requests_total[5m])) by (namespace, pod)</span>
  <span class="pi">-</span> <span class="na">record</span><span class="pi">:</span> <span class="s">http_p95_latency_ms</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">histogram_quantile(0.95,</span>
        <span class="s">sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)</span>
      <span class="s">) * 1000</span>
</code></pre></div></div>

<p><strong>Cloud Monitoring custom metric:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Python example for Cloud Run
</span><span class="kn">from</span> <span class="nn">google.cloud</span> <span class="kn">import</span> <span class="n">monitoring_v3</span>
<span class="kn">import</span> <span class="nn">time</span>

<span class="n">client</span> <span class="o">=</span> <span class="n">monitoring_v3</span><span class="p">.</span><span class="n">MetricServiceClient</span><span class="p">()</span>
<span class="n">project_name</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"projects/</span><span class="si">{</span><span class="n">PROJECT_ID</span><span class="si">}</span><span class="s">"</span>

<span class="c1"># Create custom metric descriptor
</span><span class="n">descriptor</span> <span class="o">=</span> <span class="n">monitoring_v3</span><span class="p">.</span><span class="n">MetricDescriptor</span><span class="p">()</span>
<span class="n">descriptor</span><span class="p">.</span><span class="nb">type</span> <span class="o">=</span> <span class="s">"custom.googleapis.com/http_p95_latency_ms"</span>
<span class="n">descriptor</span><span class="p">.</span><span class="n">metric_kind</span> <span class="o">=</span> <span class="n">monitoring_v3</span><span class="p">.</span><span class="n">MetricDescriptor</span><span class="p">.</span><span class="n">MetricKind</span><span class="p">.</span><span class="n">GAUGE</span>
<span class="n">descriptor</span><span class="p">.</span><span class="n">value_type</span> <span class="o">=</span> <span class="n">monitoring_v3</span><span class="p">.</span><span class="n">MetricDescriptor</span><span class="p">.</span><span class="n">ValueType</span><span class="p">.</span><span class="n">DOUBLE</span>
<span class="n">descriptor</span><span class="p">.</span><span class="n">description</span> <span class="o">=</span> <span class="s">"p95 latency in milliseconds"</span>

<span class="n">client</span><span class="p">.</span><span class="n">create_metric_descriptor</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="n">project_name</span><span class="p">,</span> <span class="n">metric_descriptor</span><span class="o">=</span><span class="n">descriptor</span>
<span class="p">)</span>

<span class="c1"># Write metric
</span><span class="n">series</span> <span class="o">=</span> <span class="n">monitoring_v3</span><span class="p">.</span><span class="n">TimeSeries</span><span class="p">()</span>
<span class="n">series</span><span class="p">.</span><span class="n">metric</span><span class="p">.</span><span class="nb">type</span> <span class="o">=</span> <span class="s">"custom.googleapis.com/http_p95_latency_ms"</span>
<span class="n">series</span><span class="p">.</span><span class="n">resource</span><span class="p">.</span><span class="nb">type</span> <span class="o">=</span> <span class="s">"cloud_run_revision"</span>
<span class="n">series</span><span class="p">.</span><span class="n">resource</span><span class="p">.</span><span class="n">labels</span><span class="p">[</span><span class="s">"project_id"</span><span class="p">]</span> <span class="o">=</span> <span class="n">PROJECT_ID</span>
<span class="n">series</span><span class="p">.</span><span class="n">resource</span><span class="p">.</span><span class="n">labels</span><span class="p">[</span><span class="s">"location"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"europe-west4"</span>
<span class="n">series</span><span class="p">.</span><span class="n">resource</span><span class="p">.</span><span class="n">labels</span><span class="p">[</span><span class="s">"service_name"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"api"</span>
<span class="n">series</span><span class="p">.</span><span class="n">resource</span><span class="p">.</span><span class="n">labels</span><span class="p">[</span><span class="s">"revision_name"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"api-001"</span>
<span class="n">series</span><span class="p">.</span><span class="n">resource</span><span class="p">.</span><span class="n">labels</span><span class="p">[</span><span class="s">"configuration_name"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"api"</span>

<span class="n">point</span> <span class="o">=</span> <span class="n">monitoring_v3</span><span class="p">.</span><span class="n">Point</span><span class="p">()</span>
<span class="n">point</span><span class="p">.</span><span class="n">value</span><span class="p">.</span><span class="n">double_value</span> <span class="o">=</span> <span class="n">p95_latency_ms</span>
<span class="n">point</span><span class="p">.</span><span class="n">interval</span><span class="p">.</span><span class="n">end_time</span><span class="p">.</span><span class="n">seconds</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">())</span>
<span class="n">series</span><span class="p">.</span><span class="n">points</span> <span class="o">=</span> <span class="p">[</span><span class="n">point</span><span class="p">]</span>

<span class="n">client</span><span class="p">.</span><span class="n">create_time_series</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">project_name</span><span class="p">,</span> <span class="n">time_series</span><span class="o">=</span><span class="p">[</span><span class="n">series</span><span class="p">])</span>
</code></pre></div></div>

<h3 id="step-3-configure-hpa-with-a-demand-metric">Step 3: Configure HPA with a Demand Metric</h3>

<p><strong>Kubernetes HPA with Prometheus adapter:</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">autoscaling/v2</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">HorizontalPodAutoscaler</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">api-hpa</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span>
    <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">api</span>
  <span class="na">minReplicas</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">maxReplicas</span><span class="pi">:</span> <span class="m">20</span>
  <span class="na">behavior</span><span class="pi">:</span>
    <span class="na">scaleDown</span><span class="pi">:</span>
      <span class="na">stabilizationWindowSeconds</span><span class="pi">:</span> <span class="m">300</span>  <span class="c1"># 5 min cooldown</span>
      <span class="na">policies</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Percent</span>
        <span class="na">value</span><span class="pi">:</span> <span class="m">50</span>  <span class="c1"># Scale down max 50% at a time</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">60</span>
    <span class="na">scaleUp</span><span class="pi">:</span>
      <span class="na">stabilizationWindowSeconds</span><span class="pi">:</span> <span class="m">0</span>  <span class="c1"># Scale up immediately</span>
      <span class="na">policies</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Percent</span>
        <span class="na">value</span><span class="pi">:</span> <span class="m">100</span>  <span class="c1"># Can double replicas</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">30</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Pods</span>
        <span class="na">value</span><span class="pi">:</span> <span class="m">4</span>  <span class="c1"># Or add 4 pods</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">30</span>
      <span class="na">selectPolicy</span><span class="pi">:</span> <span class="s">Max</span>  <span class="c1"># Use the more aggressive policy</span>
  <span class="na">metrics</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Pods</span>
    <span class="na">pods</span><span class="pi">:</span>
      <span class="na">metric</span><span class="pi">:</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">http_requests_per_second</span>
      <span class="na">target</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">AverageValue</span>
        <span class="na">averageValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">50"</span>  <span class="c1"># Target requests/second per pod</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Resource</span>
    <span class="na">resource</span><span class="pi">:</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">cpu</span>
      <span class="na">target</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">Utilization</span>
        <span class="na">averageUtilization</span><span class="pi">:</span> <span class="m">80</span>  <span class="c1"># Second safety metric; HPA uses the larger recommendation</span>
</code></pre></div></div>

<p><strong>Troubleshooting HPA:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check HPA status</span>
kubectl get hpa api-hpa

<span class="c"># Describe HPA to see scaling decisions</span>
kubectl describe hpa api-hpa

<span class="c"># Check if metrics are available</span>
kubectl get <span class="nt">--raw</span> <span class="s2">"/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/http_requests_per_second"</span>
</code></pre></div></div>

<h3 id="step-4-add-queue-based-scaling-optional">Step 4: Add Queue-Based Scaling (Optional)</h3>

<p>For services with queues (Pub/Sub, RabbitMQ, Kafka), add queue lag metrics:</p>

<p><strong>KEDA ScaledObject for Pub/Sub:</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">keda.sh/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ScaledObject</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">worker-queue</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">worker</span>
  <span class="na">minReplicaCount</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">maxReplicaCount</span><span class="pi">:</span> <span class="m">50</span>
  <span class="na">triggers</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">prometheus</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">serverAddress</span><span class="pi">:</span> <span class="s">http://prometheus:9090</span>
      <span class="na">metricName</span><span class="pi">:</span> <span class="s">pubsub_subscription_num_undelivered_messages</span>
      <span class="na">threshold</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1000"</span>  <span class="c1"># Scale when &gt; 1000 messages</span>
      <span class="na">query</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">sum(pubsub_subscription_num_undelivered_messages{subscription="worker-queue"})</span>
</code></pre></div></div>

<hr />

<h2 id="cloud-run--cloud-functions-alternative">Cloud Run / Cloud Functions Alternative</h2>

<p>Managed serverless platforms do not use an HPA. Cloud Run scales primarily from incoming demand, using concurrency and CPU behavior. Treat p95 as the validation and alerting signal, then tune the platform’s built-in controls:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud run services update api <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>europe-west4 <span class="se">\</span>
  <span class="nt">--min-instances</span><span class="o">=</span>0 <span class="se">\</span>
  <span class="nt">--max-instances</span><span class="o">=</span>50 <span class="se">\</span>
  <span class="nt">--concurrency</span><span class="o">=</span>40 <span class="se">\</span>
  <span class="nt">--cpu-boost</span>
</code></pre></div></div>

<p>Export p95 to Cloud Monitoring, alert on sustained SLO breaches, and compare latency and cost before and after each concurrency or instance-limit change. A Monitoring alert does <strong>not</strong> scale Cloud Run by itself. Custom-metric scaling requires a separate controller that changes service configuration, with rate limits and rollback controls.</p>

<hr />

<h2 id="monitoring-and-alerting">Monitoring and Alerting</h2>

<h3 id="key-metrics-dashboard">Key Metrics Dashboard</h3>

<p><strong>Grafana dashboard queries:</strong></p>

<pre><code class="language-promql"># p95 latency over time
histogram_quantile(0.95,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
) * 1000

# Instance count
count(kube_pod_info{pod=~"api-.*"})

# Cost per 1000 requests (if you have cost metrics)
sum(rate(cloud_cost_euros[1h])) / sum(rate(http_requests_total[1h])) * 1000

# Queue lag (if applicable)
sum(pubsub_subscription_num_undelivered_messages)

# Error rate
sum(rate(http_requests_total{status=~"5.."}[5m])) / 
sum(rate(http_requests_total[5m]))
</code></pre>

<h3 id="alerting-rules">Alerting Rules</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">groups</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">autoscaling</span>
  <span class="na">rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">alert</span><span class="pi">:</span> <span class="s">HighP95Latency</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">histogram_quantile(0.95,</span>
        <span class="s">sum(rate(http_request_duration_seconds_bucket[5m])) by (le)</span>
      <span class="s">) * 1000 &gt; 250</span>
    <span class="na">for</span><span class="pi">:</span> <span class="s">5m</span>
    <span class="na">annotations</span><span class="pi">:</span>
      <span class="na">summary</span><span class="pi">:</span> <span class="s2">"</span><span class="s">p95</span><span class="nv"> </span><span class="s">latency</span><span class="nv"> </span><span class="s">above</span><span class="nv"> </span><span class="s">SLO</span><span class="nv"> </span><span class="s">threshold"</span>
      <span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">p95</span><span class="nv"> </span><span class="s">latency</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">$value</span><span class="nv"> </span><span class="s">}}ms,</span><span class="nv"> </span><span class="s">target</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">250ms"</span>
  
  <span class="pi">-</span> <span class="na">alert</span><span class="pi">:</span> <span class="s">ScalingThrashing</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">changes(kube_horizontalpodautoscaler_status_current_replicas[10m]) &gt; 4</span>
    <span class="na">for</span><span class="pi">:</span> <span class="s">10m</span>
    <span class="na">annotations</span><span class="pi">:</span>
      <span class="na">summary</span><span class="pi">:</span> <span class="s2">"</span><span class="s">HPA</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">thrashing</span><span class="nv"> </span><span class="s">(scaling</span><span class="nv"> </span><span class="s">up/down</span><span class="nv"> </span><span class="s">rapidly)"</span>
      <span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Replica</span><span class="nv"> </span><span class="s">count</span><span class="nv"> </span><span class="s">changing</span><span class="nv"> </span><span class="s">too</span><span class="nv"> </span><span class="s">frequently"</span>
  
  <span class="pi">-</span> <span class="na">alert</span><span class="pi">:</span> <span class="s">QueueLagHigh</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">sum(pubsub_subscription_num_undelivered_messages) &gt; 10000</span>
    <span class="na">for</span><span class="pi">:</span> <span class="s">5m</span>
    <span class="na">annotations</span><span class="pi">:</span>
      <span class="na">summary</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Queue</span><span class="nv"> </span><span class="s">lag</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">high"</span>
      <span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">$value</span><span class="nv"> </span><span class="s">}}</span><span class="nv"> </span><span class="s">messages</span><span class="nv"> </span><span class="s">undelivered"</span>
</code></pre></div></div>

<hr />

<h2 id="common-pitfalls-and-solutions">Common Pitfalls and Solutions</h2>

<h3 id="pitfall-1-metric-delay">Pitfall 1: Metric Delay</h3>

<p><strong>Problem:</strong> p95 latency can lag by 30-60 seconds, so it is a poor primary scaling trigger.</p>

<p><strong>Solution:</strong></p>
<ul>
  <li>Scale on request rate or queue lag</li>
  <li>Use shorter p95 windows for alerting where the sample size is sufficient</li>
  <li>Add scheduled or predictive capacity for known traffic patterns</li>
</ul>

<h3 id="pitfall-2-noisy-metrics">Pitfall 2: Noisy Metrics</h3>

<p><strong>Problem:</strong> p95 spikes from a small number of slow requests can make a healthy scaling policy look unstable.</p>

<p><strong>Solution:</strong></p>
<ul>
  <li>Use longer aggregation windows (5 minutes)</li>
  <li>Add minimum duration before scaling (stabilization window)</li>
  <li>Filter out known slow endpoints (health checks, admin APIs)</li>
</ul>

<h3 id="pitfall-3-cold-start-latency">Pitfall 3: Cold Start Latency</h3>

<p><strong>Problem:</strong> New instances have high latency during startup, temporarily worsening p95.</p>

<p><strong>Solution:</strong></p>
<ul>
  <li>Set minimum instances to avoid cold starts</li>
  <li>Use startup CPU boost (Cloud Run)</li>
  <li>Exclude startup period from latency calculations</li>
</ul>

<h3 id="pitfall-4-downstream-dependency-issues">Pitfall 4: Downstream Dependency Issues</h3>

<p><strong>Problem:</strong> High latency from database/API, not your service.</p>

<p><strong>Solution:</strong></p>
<ul>
  <li>Monitor downstream dependencies separately</li>
  <li>Scale based on queue depth, not just latency</li>
  <li>Implement circuit breakers to fail fast</li>
</ul>

<hr />

<h2 id="cost-optimization-tips">Cost Optimization Tips</h2>

<ol>
  <li><strong>Right-size instances</strong>: Use smaller instances with higher concurrency</li>
  <li><strong>Set appropriate min/max</strong>: Don’t over-provision, but avoid cold starts</li>
  <li><strong>Use spot/preemptible instances</strong>: For non-critical workloads</li>
  <li><strong>Implement cost guardrails</strong>: Budget alerts, quota limits</li>
  <li><strong>Monitor cost-to-serve</strong>: Track €/1000 requests over time</li>
</ol>

<p><strong>Outcomes to measure:</strong></p>
<ul>
  <li>Cost per 1,000 successful requests</li>
  <li>Latency-related incident count</li>
  <li>p95 latency during steady and burst traffic</li>
</ul>

<hr />

<h2 id="next-steps">Next Steps</h2>

<ol>
  <li><strong>Week 1</strong>: Instrument p95 latency, create custom metrics</li>
  <li><strong>Week 2</strong>: Configure HPA with request-rate or queue-based scaling</li>
  <li><strong>Week 3</strong>: Add queue-based scaling (if applicable)</li>
  <li><strong>Week 4</strong>: Tune thresholds, add alerts, measure impact</li>
</ol>

<h2 id="cloud-rungateway-note-if-youre-not-on-kubernetes-tune-concurrency-and-minmax-instances-and-use-p95-alerts-to-validate-the-built-in-scaling-policy"><strong>Cloud Run/Gateway note:</strong> If you’re not on Kubernetes, tune <strong>concurrency</strong> and <strong>min/max instances</strong>, and use p95 alerts to validate the built-in scaling policy.</h2>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>From GO to GNNs: A Practical Guide to Gene Function Prediction (2025)</title>
					
					<link>https://atabakkh.github.io/computationalbiology/2025/08/03/go-to-gnns-practical-guide.html</link>

					

					<pubDate>Sun, 03 Aug 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/computationalbiology/2025/08/03/go-to-gnns-practical-guide</guid>

					
						<category><![CDATA[ ComputationalBiology ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Takeaway:</strong> For robust GO prediction, start with <strong>homology + PLM baselines</strong>, add <strong>label smoothing on PPI</strong>, and only then graduate to <strong>GNNs/multimodal fusion</strong>.</p>

<h2 id="problem-framing-multilabel-hierarchical">Problem framing (multilabel, hierarchical)</h2>
<ul>
  <li><strong>Labels:</strong> Gene Ontology (BP/MF/CC), DAG with <code class="language-plaintext highlighter-rouge">is_a</code>, <code class="language-plaintext highlighter-rouge">part_of</code>.</li>
  <li><strong>Task:</strong> multilabel classification with long-tail classes and hierarchy constraints.</li>
  <li><strong>Implication:</strong> predictions must be <em>ancestor-closed</em>.</li>
</ul>

<h2 id="features-to-combine">Features to combine</h2>
<p>1) <strong>Homology</strong>: BLAST/DIAMOND kNN; domain HMMs (Pfam).
2) <strong>Sequence embeddings</strong>: frozen PLMs (e.g., ESM-like, 1-3k dims).
3) <strong>Structure</strong>: DSSP features, secondary structure, solvent accessibility; optional 3D graph embeddings.
4) <strong>Networks</strong>: PPI/co-expression; <code class="language-plaintext highlighter-rouge">A</code> adjacency for smoothing or GNN.
5) <strong>Text</strong>: weak supervision from abstracts/full-text.</p>

<h2 id="minimal-high-value-recipe">Minimal, high-value recipe</h2>
<p><strong>(a) Logistic baseline on PLM embeddings (balanced, calibrated)</strong><br />
<strong>(b) One-step label smoothing on normalized PPI</strong><br />
<strong>(c) Hierarchy closure + per-class threshold tuning (Fmax)</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># X: PLM embeddings [N, D], Y: multi-hot GO [N, C], A: normalized PPI
</span><span class="n">clf</span> <span class="o">=</span> <span class="n">OneVsRestClassifier</span><span class="p">(</span><span class="n">LogisticRegression</span><span class="p">(</span><span class="n">max_iter</span><span class="o">=</span><span class="mi">4000</span><span class="p">,</span> <span class="n">class_weight</span><span class="o">=</span><span class="s">"balanced"</span><span class="p">))</span>
<span class="n">clf</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_tr</span><span class="p">,</span> <span class="n">Y_tr</span><span class="p">)</span>
<span class="n">P0</span> <span class="o">=</span> <span class="n">clf</span><span class="p">.</span><span class="n">predict_proba</span><span class="p">(</span><span class="n">X_val</span><span class="p">)</span>

<span class="n">alpha</span> <span class="o">=</span> <span class="mf">0.2</span>
<span class="n">P1</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">alpha</span><span class="p">)</span> <span class="o">*</span> <span class="n">P0</span> <span class="o">+</span> <span class="n">alpha</span> <span class="o">*</span> <span class="p">(</span><span class="n">A</span> <span class="o">@</span> <span class="n">P0</span><span class="p">)</span>           <span class="c1"># one-step smoothing
</span><span class="n">P1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">clip</span><span class="p">(</span><span class="n">P1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">P1</span> <span class="o">=</span> <span class="n">close_under_ancestors</span><span class="p">(</span><span class="n">P1</span><span class="p">,</span> <span class="n">go_dag</span><span class="p">)</span>              <span class="c1"># hierarchy consistency
</span>
<span class="n">th</span> <span class="o">=</span> <span class="n">tune_thresholds</span><span class="p">(</span><span class="n">P1</span><span class="p">,</span> <span class="n">Y_val</span><span class="p">,</span> <span class="n">metric</span><span class="o">=</span><span class="s">"Fmax"</span><span class="p">)</span>      <span class="c1"># per-class thresholds
</span><span class="n">Y_hat</span> <span class="o">=</span> <span class="p">(</span><span class="n">P1</span> <span class="o">&gt;=</span> <span class="n">th</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="hyper-parameters-good-starting-points">Hyper-parameters (good starting points)</h2>

<ul>
  <li>PLM embedding: L2-normalize; optionally <strong>PCA to 256-512</strong>.</li>
  <li>Logistic: <code class="language-plaintext highlighter-rouge">C=1.0</code>, <code class="language-plaintext highlighter-rouge">class_weight="balanced"</code>, <code class="language-plaintext highlighter-rouge">solver="lbfgs"</code>, <code class="language-plaintext highlighter-rouge">max_iter=4000</code>.</li>
  <li>Smoothing: <code class="language-plaintext highlighter-rouge">alpha = 0.1-0.3</code> (avoid oversmoothing hubs).</li>
  <li>Threshold search: per-class sweep over <code class="language-plaintext highlighter-rouge">[0.05..0.95]</code>.</li>
</ul>

<h2 id="evaluation--reporting">Evaluation &amp; reporting</h2>

<ul>
  <li><strong>Fmax</strong> per ontology (BP/MF/CC), <strong>micro/macro-auPRC</strong>.</li>
  <li><strong>Coverage</strong> (proteins with ≥1 label), <strong>ECE</strong> (calibration).</li>
  <li><strong>Hierarchy violations</strong> (should be ~0 after closure).</li>
</ul>

<h2 id="ablations-to-include-table">Ablations to include (table)</h2>

<ul>
  <li>PLM only vs +kNN homology vs +PPI smoothing.</li>
  <li>Thresholding: global vs per-class.</li>
  <li>With/without hierarchy closure.</li>
</ul>

<p><strong>Bottom line:</strong> this pipeline is simple, strong, and extensible; add GNNs (GCN/GAT) or multimodal fusion when the baseline plateaus.</p>

<hr />

<h2 id="detailed-implementation">Detailed Implementation</h2>

<h3 id="step-1-feature-extraction">Step 1: Feature Extraction</h3>

<p><strong>Homology-based features (BLAST/DIAMOND):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">Bio.Blast</span> <span class="kn">import</span> <span class="n">NCBIWWW</span><span class="p">,</span> <span class="n">NCBIXML</span>
<span class="kn">import</span> <span class="nn">subprocess</span>

<span class="k">def</span> <span class="nf">get_blast_homologs</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">database</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"nr"</span><span class="p">,</span> <span class="n">evalue</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1e-5</span><span class="p">):</span>
    <span class="s">"""
    Get homologous proteins via BLAST.
    
    Returns:
        homolog_ids: List of UniProt IDs with significant hits
        scores: List of E-values
    """</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">NCBIWWW</span><span class="p">.</span><span class="n">qblast</span><span class="p">(</span><span class="s">"blastp"</span><span class="p">,</span> <span class="n">database</span><span class="p">,</span> <span class="n">sequence</span><span class="p">,</span> <span class="n">expect</span><span class="o">=</span><span class="n">evalue</span><span class="p">)</span>
    <span class="n">blast_record</span> <span class="o">=</span> <span class="n">NCBIXML</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
    
    <span class="n">homolog_ids</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">scores</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">alignment</span> <span class="ow">in</span> <span class="n">blast_record</span><span class="p">.</span><span class="n">alignments</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">hsp</span> <span class="ow">in</span> <span class="n">alignment</span><span class="p">.</span><span class="n">hsps</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">hsp</span><span class="p">.</span><span class="n">expect</span> <span class="o">&lt;</span> <span class="n">evalue</span><span class="p">:</span>
                <span class="n">homolog_ids</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">alignment</span><span class="p">.</span><span class="n">title</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">'|'</span><span class="p">)[</span><span class="mi">1</span><span class="p">])</span>  <span class="c1"># UniProt ID
</span>                <span class="n">scores</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">hsp</span><span class="p">.</span><span class="n">expect</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">homolog_ids</span><span class="p">,</span> <span class="n">scores</span>

<span class="k">def</span> <span class="nf">transfer_labels_from_homologs</span><span class="p">(</span><span class="n">protein_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">homolog_ids</span><span class="p">:</span> <span class="nb">list</span><span class="p">,</span> <span class="n">go_annotations</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
    <span class="s">"""
    Transfer GO labels from homologous proteins.
    
    Returns:
        transferred_labels: Set of GO terms from homologs
    """</span>
    <span class="n">transferred_labels</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">homolog_id</span> <span class="ow">in</span> <span class="n">homolog_ids</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">homolog_id</span> <span class="ow">in</span> <span class="n">go_annotations</span><span class="p">:</span>
            <span class="n">transferred_labels</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">go_annotations</span><span class="p">[</span><span class="n">homolog_id</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">transferred_labels</span>
</code></pre></div></div>

<p><strong>PLM embeddings (ESM2):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">EsmModel</span><span class="p">,</span> <span class="n">EsmTokenizer</span>
<span class="kn">import</span> <span class="nn">torch</span>

<span class="k">def</span> <span class="nf">get_esm2_embedding</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">model_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"facebook/esm2_t33_650M_UR50D"</span><span class="p">):</span>
    <span class="s">"""
    Get ESM2 embedding for a protein sequence.
    
    Returns:
        embedding: numpy array [D] of L2-normalized embedding
    """</span>
    <span class="n">device</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">device</span><span class="p">(</span><span class="s">"cuda"</span> <span class="k">if</span> <span class="n">torch</span><span class="p">.</span><span class="n">cuda</span><span class="p">.</span><span class="n">is_available</span><span class="p">()</span> <span class="k">else</span> <span class="s">"cpu"</span><span class="p">)</span>
    <span class="n">tokenizer</span> <span class="o">=</span> <span class="n">EsmTokenizer</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>
    <span class="n">model</span> <span class="o">=</span> <span class="n">EsmModel</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_name</span><span class="p">).</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
    <span class="n">model</span><span class="p">.</span><span class="nb">eval</span><span class="p">()</span>
    
    <span class="c1"># Tokenize
</span>    <span class="n">encoded</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">(</span><span class="n">sequence</span><span class="p">,</span> <span class="n">return_tensors</span><span class="o">=</span><span class="s">"pt"</span><span class="p">,</span> <span class="n">truncation</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">1024</span><span class="p">).</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
    
    <span class="c1"># Get embedding (mean pooling)
</span>    <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="n">no_grad</span><span class="p">():</span>
        <span class="n">outputs</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="o">**</span><span class="n">encoded</span><span class="p">)</span>
        <span class="n">embedding</span> <span class="o">=</span> <span class="n">outputs</span><span class="p">.</span><span class="n">last_hidden_state</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">).</span><span class="n">squeeze</span><span class="p">()</span>
        <span class="n">embedding</span> <span class="o">=</span> <span class="n">embedding</span> <span class="o">/</span> <span class="n">embedding</span><span class="p">.</span><span class="n">norm</span><span class="p">()</span>  <span class="c1"># L2 normalize
</span>    
    <span class="k">return</span> <span class="n">embedding</span><span class="p">.</span><span class="n">cpu</span><span class="p">().</span><span class="n">numpy</span><span class="p">()</span>
</code></pre></div></div>

<p><strong>Structure features (DSSP):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">Bio.PDB</span> <span class="kn">import</span> <span class="n">PDBParser</span>
<span class="kn">from</span> <span class="nn">Bio.PDB.DSSP</span> <span class="kn">import</span> <span class="n">DSSP</span>

<span class="k">def</span> <span class="nf">extract_structure_features</span><span class="p">(</span><span class="n">pdb_file</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="s">"""
    Extract secondary structure and solvent accessibility from PDB.
    
    Returns:
        features: Dictionary with SS, SA, etc.
    """</span>
    <span class="n">parser</span> <span class="o">=</span> <span class="n">PDBParser</span><span class="p">(</span><span class="n">QUIET</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">structure</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">get_structure</span><span class="p">(</span><span class="s">'protein'</span><span class="p">,</span> <span class="n">pdb_file</span><span class="p">)</span>
    <span class="n">model</span> <span class="o">=</span> <span class="n">structure</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
    
    <span class="n">dssp</span> <span class="o">=</span> <span class="n">DSSP</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">pdb_file</span><span class="p">)</span>
    
    <span class="n">features</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">'secondary_structure'</span><span class="p">:</span> <span class="p">[],</span>
        <span class="s">'solvent_accessibility'</span><span class="p">:</span> <span class="p">[],</span>
        <span class="s">'phi'</span><span class="p">:</span> <span class="p">[],</span>
        <span class="s">'psi'</span><span class="p">:</span> <span class="p">[]</span>
    <span class="p">}</span>
    
    <span class="k">for</span> <span class="n">residue</span> <span class="ow">in</span> <span class="n">dssp</span><span class="p">:</span>
        <span class="n">features</span><span class="p">[</span><span class="s">'secondary_structure'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">residue</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>  <span class="c1"># H, E, C
</span>        <span class="n">features</span><span class="p">[</span><span class="s">'solvent_accessibility'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">residue</span><span class="p">[</span><span class="mi">3</span><span class="p">])</span>  <span class="c1"># RSA
</span>        <span class="n">features</span><span class="p">[</span><span class="s">'phi'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">residue</span><span class="p">[</span><span class="mi">4</span><span class="p">])</span>
        <span class="n">features</span><span class="p">[</span><span class="s">'psi'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">residue</span><span class="p">[</span><span class="mi">5</span><span class="p">])</span>
    
    <span class="k">return</span> <span class="n">features</span>
</code></pre></div></div>

<hr />

<h3 id="step-2-label-smoothing-on-ppi">Step 2: Label Smoothing on PPI</h3>

<p><strong>One-step label smoothing implementation:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">scipy.sparse</span> <span class="k">as</span> <span class="n">sp</span>

<span class="k">def</span> <span class="nf">label_smoothing_on_ppi</span><span class="p">(</span>
    <span class="n">P0</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span>
    <span class="n">A</span><span class="p">:</span> <span class="n">sp</span><span class="p">.</span><span class="n">csr_matrix</span><span class="p">,</span>
    <span class="n">alpha</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.2</span>
<span class="p">):</span>
    <span class="s">"""
    Apply one-step label smoothing on PPI network.
    
    Args:
        P0: [N, C] initial predictions (probabilities)
        A: [N, N] normalized PPI adjacency matrix
        alpha: Smoothing coefficient (0.2 = 20% from neighbors)
    
    Returns:
        P1: [N, C] smoothed predictions
    """</span>
    <span class="c1"># Normalize adjacency (row-stochastic)
</span>    <span class="n">A_normalized</span> <span class="o">=</span> <span class="n">A</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>
    <span class="n">row_sums</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">A</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)).</span><span class="n">flatten</span><span class="p">()</span>
    <span class="n">row_sums</span><span class="p">[</span><span class="n">row_sums</span> <span class="o">==</span> <span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mf">1.0</span>  <span class="c1"># Avoid division by zero
</span>    <span class="n">A_normalized</span> <span class="o">=</span> <span class="n">A_normalized</span><span class="p">.</span><span class="n">multiply</span><span class="p">(</span><span class="mf">1.0</span> <span class="o">/</span> <span class="n">row_sums</span><span class="p">[:,</span> <span class="n">np</span><span class="p">.</span><span class="n">newaxis</span><span class="p">])</span>
    
    <span class="c1"># One-step smoothing: (1-alpha) * P0 + alpha * A @ P0
</span>    <span class="n">P_smoothed</span> <span class="o">=</span> <span class="n">A_normalized</span> <span class="o">@</span> <span class="n">P0</span>
    <span class="n">P1</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">alpha</span><span class="p">)</span> <span class="o">*</span> <span class="n">P0</span> <span class="o">+</span> <span class="n">alpha</span> <span class="o">*</span> <span class="n">P_smoothed</span>
    
    <span class="c1"># Clip to [0, 1]
</span>    <span class="n">P1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">clip</span><span class="p">(</span><span class="n">P1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">P1</span>
</code></pre></div></div>

<p><strong>Why it works:</strong></p>
<ul>
  <li>Proteins with similar functions tend to interact (guilt-by-association)</li>
  <li>Smoothing propagates labels from annotated to unannotated proteins</li>
  <li>One step avoids oversmoothing (multiple steps can blur signal)</li>
</ul>

<p><strong>Tuning alpha:</strong></p>
<ul>
  <li><strong>alpha = 0.1</strong>: Conservative, mostly original predictions</li>
  <li><strong>alpha = 0.2</strong>: Balanced (good default)</li>
  <li><strong>alpha = 0.3</strong>: Aggressive, more neighbor influence</li>
  <li><strong>alpha &gt; 0.3</strong>: Risk of oversmoothing, especially for hub proteins</li>
</ul>

<hr />

<h3 id="step-3-hierarchy-closure">Step 3: Hierarchy Closure</h3>

<p><strong>Ancestor closure (from previous article):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">close_under_ancestors</span><span class="p">(</span><span class="n">P</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span> <span class="n">go_dag</span><span class="p">,</span> <span class="n">threshold</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.5</span><span class="p">):</span>
    <span class="s">"""
    Ensure predictions respect GO hierarchy.
    
    Args:
        P: [N, C] class probabilities
        go_dag: GO DAG object
        threshold: Threshold for binary predictions
    
    Returns:
        Y_closed: [N, C] ancestor-closed binary labels
    """</span>
    <span class="n">Y</span> <span class="o">=</span> <span class="p">(</span><span class="n">P</span> <span class="o">&gt;=</span> <span class="n">threshold</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">uint8</span><span class="p">)</span>
    
    <span class="c1"># Topological order (parents before children)
</span>    <span class="n">order</span> <span class="o">=</span> <span class="n">go_dag</span><span class="p">.</span><span class="n">topo_order</span><span class="p">()</span>
    
    <span class="c1"># Visit children before parents (reverse order)
</span>    <span class="k">for</span> <span class="n">term_idx</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="n">order</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">ancestor_idx</span> <span class="ow">in</span> <span class="n">go_dag</span><span class="p">.</span><span class="n">ancestors</span><span class="p">(</span><span class="n">term_idx</span><span class="p">):</span>
            <span class="c1"># If child is on, force ancestor on
</span>            <span class="n">Y</span><span class="p">[:,</span> <span class="n">ancestor_idx</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">Y</span><span class="p">[:,</span> <span class="n">ancestor_idx</span><span class="p">],</span> <span class="n">Y</span><span class="p">[:,</span> <span class="n">term_idx</span><span class="p">])</span>
    
    <span class="k">return</span> <span class="n">Y</span>
</code></pre></div></div>

<hr />

<h3 id="step-4-threshold-tuning">Step 4: Threshold Tuning</h3>

<p><strong>Per-class threshold optimization:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sklearn.metrics</span> <span class="kn">import</span> <span class="n">f1_score</span>

<span class="k">def</span> <span class="nf">tune_per_class_thresholds</span><span class="p">(</span>
    <span class="n">P</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span>
    <span class="n">Y_true</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span>
    <span class="n">metric</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"f1"</span><span class="p">,</span>
    <span class="n">thresholds</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span> <span class="o">=</span> <span class="bp">None</span>
<span class="p">):</span>
    <span class="s">"""
    Find optimal per-class thresholds for Fmax.
    
    Returns:
        best_thresholds: [C] array of optimal thresholds per class
        best_fmax: Maximum F1 score achieved
    """</span>
    <span class="k">if</span> <span class="n">thresholds</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">thresholds</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="mf">0.05</span><span class="p">,</span> <span class="mf">0.95</span><span class="p">,</span> <span class="mf">0.01</span><span class="p">)</span>
    
    <span class="n">num_classes</span> <span class="o">=</span> <span class="n">Y_true</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">best_thresholds</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">(</span><span class="n">num_classes</span><span class="p">)</span>
    <span class="n">best_fmax</span> <span class="o">=</span> <span class="mf">0.0</span>
    
    <span class="k">for</span> <span class="n">class_idx</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_classes</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">Y_true</span><span class="p">[:,</span> <span class="n">class_idx</span><span class="p">].</span><span class="nb">sum</span><span class="p">()</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="k">continue</span>  <span class="c1"># Skip classes with no positives
</span>        
        <span class="n">best_f1</span> <span class="o">=</span> <span class="mf">0.0</span>
        <span class="n">best_th</span> <span class="o">=</span> <span class="mf">0.5</span>
        
        <span class="k">for</span> <span class="n">th</span> <span class="ow">in</span> <span class="n">thresholds</span><span class="p">:</span>
            <span class="n">y_pred</span> <span class="o">=</span> <span class="p">(</span><span class="n">P</span><span class="p">[:,</span> <span class="n">class_idx</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="n">th</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
            <span class="n">f1</span> <span class="o">=</span> <span class="n">f1_score</span><span class="p">(</span><span class="n">Y_true</span><span class="p">[:,</span> <span class="n">class_idx</span><span class="p">],</span> <span class="n">y_pred</span><span class="p">,</span> <span class="n">zero_division</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
            
            <span class="k">if</span> <span class="n">f1</span> <span class="o">&gt;</span> <span class="n">best_f1</span><span class="p">:</span>
                <span class="n">best_f1</span> <span class="o">=</span> <span class="n">f1</span>
                <span class="n">best_th</span> <span class="o">=</span> <span class="n">th</span>
        
        <span class="n">best_thresholds</span><span class="p">[</span><span class="n">class_idx</span><span class="p">]</span> <span class="o">=</span> <span class="n">best_th</span>
    
    <span class="c1"># Compute Fmax with best thresholds
</span>    <span class="n">Y_pred</span> <span class="o">=</span> <span class="p">(</span><span class="n">P</span> <span class="o">&gt;=</span> <span class="n">best_thresholds</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
    <span class="n">fmax</span> <span class="o">=</span> <span class="n">compute_fmax</span><span class="p">(</span><span class="n">Y_true</span><span class="p">,</span> <span class="n">Y_pred</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">best_thresholds</span><span class="p">,</span> <span class="n">fmax</span>
</code></pre></div></div>

<hr />

<h3 id="step-5-advanced-gnn-implementation">Step 5: Advanced: GNN Implementation</h3>

<p><strong>GCN for GO prediction:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">import</span> <span class="nn">torch.nn</span> <span class="k">as</span> <span class="n">nn</span>
<span class="kn">import</span> <span class="nn">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>
<span class="kn">from</span> <span class="nn">torch_geometric.nn</span> <span class="kn">import</span> <span class="n">GCNConv</span>

<span class="k">class</span> <span class="nc">GCNForGO</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">,</span> <span class="n">num_classes</span><span class="p">,</span> <span class="n">num_layers</span><span class="o">=</span><span class="mi">2</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">convs</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">ModuleList</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">convs</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">GCNConv</span><span class="p">(</span><span class="n">input_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">))</span>
        
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_layers</span> <span class="o">-</span> <span class="mi">2</span><span class="p">):</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">convs</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">GCNConv</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">))</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">convs</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">GCNConv</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">num_classes</span><span class="p">))</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">dropout</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Dropout</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">edge_index</span><span class="p">):</span>
        <span class="s">"""
        Args:
            x: [N, D] node features (PLM embeddings)
            edge_index: [2, E] edge indices (PPI network)
        """</span>
        <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">conv</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">convs</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">]):</span>
            <span class="n">x</span> <span class="o">=</span> <span class="n">conv</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">edge_index</span><span class="p">)</span>
            <span class="n">x</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
            <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">dropout</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">convs</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">](</span><span class="n">x</span><span class="p">,</span> <span class="n">edge_index</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">torch</span><span class="p">.</span><span class="n">sigmoid</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>  <span class="c1"># Multi-label classification
</span></code></pre></div></div>

<p><strong>Training loop:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">train_gnn</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">train_loader</span><span class="p">,</span> <span class="n">val_loader</span><span class="p">,</span> <span class="n">num_epochs</span><span class="o">=</span><span class="mi">100</span><span class="p">):</span>
    <span class="s">"""
    Train GCN for GO prediction.
    """</span>
    <span class="n">optimizer</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">Adam</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">parameters</span><span class="p">(),</span> <span class="n">lr</span><span class="o">=</span><span class="mf">0.001</span><span class="p">)</span>
    <span class="n">criterion</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">BCELoss</span><span class="p">()</span>
    
    <span class="n">best_fmax</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">epoch</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_epochs</span><span class="p">):</span>
        <span class="c1"># Training
</span>        <span class="n">model</span><span class="p">.</span><span class="n">train</span><span class="p">()</span>
        <span class="n">train_loss</span> <span class="o">=</span> <span class="mf">0.0</span>
        <span class="k">for</span> <span class="n">batch</span> <span class="ow">in</span> <span class="n">train_loader</span><span class="p">:</span>
            <span class="n">optimizer</span><span class="p">.</span><span class="n">zero_grad</span><span class="p">()</span>
            <span class="n">out</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="n">batch</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">batch</span><span class="p">.</span><span class="n">edge_index</span><span class="p">)</span>
            <span class="n">loss</span> <span class="o">=</span> <span class="n">criterion</span><span class="p">(</span><span class="n">out</span><span class="p">,</span> <span class="n">batch</span><span class="p">.</span><span class="n">y</span><span class="p">)</span>
            <span class="n">loss</span><span class="p">.</span><span class="n">backward</span><span class="p">()</span>
            <span class="n">optimizer</span><span class="p">.</span><span class="n">step</span><span class="p">()</span>
            <span class="n">train_loss</span> <span class="o">+=</span> <span class="n">loss</span><span class="p">.</span><span class="n">item</span><span class="p">()</span>
        
        <span class="c1"># Validation
</span>        <span class="n">model</span><span class="p">.</span><span class="nb">eval</span><span class="p">()</span>
        <span class="n">val_predictions</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">val_labels</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="n">no_grad</span><span class="p">():</span>
            <span class="k">for</span> <span class="n">batch</span> <span class="ow">in</span> <span class="n">val_loader</span><span class="p">:</span>
                <span class="n">out</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="n">batch</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">batch</span><span class="p">.</span><span class="n">edge_index</span><span class="p">)</span>
                <span class="n">val_predictions</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">out</span><span class="p">.</span><span class="n">cpu</span><span class="p">().</span><span class="n">numpy</span><span class="p">())</span>
                <span class="n">val_labels</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">batch</span><span class="p">.</span><span class="n">y</span><span class="p">.</span><span class="n">cpu</span><span class="p">().</span><span class="n">numpy</span><span class="p">())</span>
        
        <span class="n">val_predictions</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">vstack</span><span class="p">(</span><span class="n">val_predictions</span><span class="p">)</span>
        <span class="n">val_labels</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">vstack</span><span class="p">(</span><span class="n">val_labels</span><span class="p">)</span>
        
        <span class="n">fmax</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">compute_fmax</span><span class="p">(</span><span class="n">val_labels</span><span class="p">,</span> <span class="n">val_predictions</span><span class="p">)</span>
        
        <span class="k">if</span> <span class="n">fmax</span> <span class="o">&gt;</span> <span class="n">best_fmax</span><span class="p">:</span>
            <span class="n">best_fmax</span> <span class="o">=</span> <span class="n">fmax</span>
            <span class="n">torch</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">state_dict</span><span class="p">(),</span> <span class="s">'best_model.pth'</span><span class="p">)</span>
        
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Epoch </span><span class="si">{</span><span class="n">epoch</span><span class="si">}</span><span class="s">: Train Loss = </span><span class="si">{</span><span class="n">train_loss</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="s">, Val Fmax = </span><span class="si">{</span><span class="n">fmax</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h3 id="step-6-multimodal-fusion">Step 6: Multimodal Fusion</h3>

<p><strong>Late fusion of multiple features:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">multimodal_fusion</span><span class="p">(</span>
    <span class="n">plm_embeddings</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span>
    <span class="n">structure_features</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span>
    <span class="n">homology_scores</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span>
    <span class="n">ppi_embeddings</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span>
<span class="p">):</span>
    <span class="s">"""
    Combine multiple feature modalities.
    
    Returns:
        fused_features: [N, D] combined feature vector
    """</span>
    <span class="c1"># Normalize each modality
</span>    <span class="n">plm_norm</span> <span class="o">=</span> <span class="n">plm_embeddings</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">plm_embeddings</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">struct_norm</span> <span class="o">=</span> <span class="n">structure_features</span> <span class="o">/</span> <span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">structure_features</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="o">+</span> <span class="mf">1e-10</span><span class="p">)</span>
    <span class="n">homology_norm</span> <span class="o">=</span> <span class="n">homology_scores</span> <span class="o">/</span> <span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">homology_scores</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="o">+</span> <span class="mf">1e-10</span><span class="p">)</span>
    <span class="n">ppi_norm</span> <span class="o">=</span> <span class="n">ppi_embeddings</span> <span class="o">/</span> <span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">ppi_embeddings</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="o">+</span> <span class="mf">1e-10</span><span class="p">)</span>
    
    <span class="c1"># Concatenate or weighted average
</span>    <span class="c1"># Option 1: Concatenate
</span>    <span class="n">fused</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">([</span><span class="n">plm_norm</span><span class="p">,</span> <span class="n">struct_norm</span><span class="p">,</span> <span class="n">homology_norm</span><span class="p">,</span> <span class="n">ppi_norm</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    
    <span class="c1"># Option 2: Weighted average (if same dimension)
</span>    <span class="c1"># weights = [0.5, 0.2, 0.2, 0.1]  # PLM gets most weight
</span>    <span class="c1"># fused = weights[0] * plm_norm + weights[1] * struct_norm + ...
</span>    
    <span class="k">return</span> <span class="n">fused</span>
</code></pre></div></div>

<p><strong>Early fusion (learned):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MultimodalFusion</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">plm_dim</span><span class="p">,</span> <span class="n">struct_dim</span><span class="p">,</span> <span class="n">homology_dim</span><span class="p">,</span> <span class="n">ppi_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">plm_proj</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">plm_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">struct_proj</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">struct_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">homology_proj</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">homology_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">ppi_proj</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">ppi_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">)</span>
        
        <span class="c1"># Learnable fusion weights
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">fusion_weights</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Parameter</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="n">ones</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span> <span class="o">/</span> <span class="mi">4</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">plm</span><span class="p">,</span> <span class="n">struct</span><span class="p">,</span> <span class="n">homology</span><span class="p">,</span> <span class="n">ppi</span><span class="p">):</span>
        <span class="c1"># Project each modality to same dimension
</span>        <span class="n">plm_proj</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">plm_proj</span><span class="p">(</span><span class="n">plm</span><span class="p">))</span>
        <span class="n">struct_proj</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">struct_proj</span><span class="p">(</span><span class="n">struct</span><span class="p">))</span>
        <span class="n">homology_proj</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">homology_proj</span><span class="p">(</span><span class="n">homology</span><span class="p">))</span>
        <span class="n">ppi_proj</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">ppi_proj</span><span class="p">(</span><span class="n">ppi</span><span class="p">))</span>
        
        <span class="c1"># Weighted combination
</span>        <span class="n">weights</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="n">softmax</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">fusion_weights</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
        <span class="n">fused</span> <span class="o">=</span> <span class="p">(</span><span class="n">weights</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">plm_proj</span> <span class="o">+</span> 
                 <span class="n">weights</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="n">struct_proj</span> <span class="o">+</span> 
                 <span class="n">weights</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">*</span> <span class="n">homology_proj</span> <span class="o">+</span> 
                 <span class="n">weights</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">*</span> <span class="n">ppi_proj</span><span class="p">)</span>
        
        <span class="k">return</span> <span class="n">fused</span>
</code></pre></div></div>

<hr />

<h2 id="complete-pipeline-example">Complete Pipeline Example</h2>

<p><strong>End-to-end pipeline:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">from</span> <span class="nn">sklearn.linear_model</span> <span class="kn">import</span> <span class="n">LogisticRegression</span>
<span class="kn">from</span> <span class="nn">sklearn.multiclass</span> <span class="kn">import</span> <span class="n">OneVsRestClassifier</span>

<span class="c1"># 1. Load data
</span><span class="n">sequences</span> <span class="o">=</span> <span class="n">load_sequences</span><span class="p">(</span><span class="s">"data/uniprot.fasta"</span><span class="p">)</span>
<span class="n">go_annotations</span> <span class="o">=</span> <span class="n">load_go_annotations</span><span class="p">(</span><span class="s">"data/goa.tsv"</span><span class="p">)</span>
<span class="n">ppi_network</span> <span class="o">=</span> <span class="n">load_ppi_network</span><span class="p">(</span><span class="s">"data/string.tsv"</span><span class="p">)</span>

<span class="c1"># 2. Generate features
</span><span class="k">print</span><span class="p">(</span><span class="s">"Generating PLM embeddings..."</span><span class="p">)</span>
<span class="n">X_plm</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">get_esm2_embedding</span><span class="p">(</span><span class="n">seq</span><span class="p">)</span> <span class="k">for</span> <span class="n">seq</span> <span class="ow">in</span> <span class="n">sequences</span><span class="p">])</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Computing homology features..."</span><span class="p">)</span>
<span class="n">X_homology</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">get_blast_homologs</span><span class="p">(</span><span class="n">seq</span><span class="p">)</span> <span class="k">for</span> <span class="n">seq</span> <span class="ow">in</span> <span class="n">sequences</span><span class="p">])</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Building PPI adjacency..."</span><span class="p">)</span>
<span class="n">A</span> <span class="o">=</span> <span class="n">build_ppi_adjacency</span><span class="p">(</span><span class="n">ppi_network</span><span class="p">,</span> <span class="n">sequences</span><span class="p">)</span>

<span class="c1"># 3. Train baseline
</span><span class="k">print</span><span class="p">(</span><span class="s">"Training logistic regression..."</span><span class="p">)</span>
<span class="n">clf</span> <span class="o">=</span> <span class="n">OneVsRestClassifier</span><span class="p">(</span>
    <span class="n">LogisticRegression</span><span class="p">(</span><span class="n">max_iter</span><span class="o">=</span><span class="mi">4000</span><span class="p">,</span> <span class="n">class_weight</span><span class="o">=</span><span class="s">"balanced"</span><span class="p">,</span> <span class="n">solver</span><span class="o">=</span><span class="s">"lbfgs"</span><span class="p">)</span>
<span class="p">)</span>
<span class="n">clf</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_plm</span><span class="p">,</span> <span class="n">Y_train</span><span class="p">)</span>
<span class="n">P0</span> <span class="o">=</span> <span class="n">clf</span><span class="p">.</span><span class="n">predict_proba</span><span class="p">(</span><span class="n">X_val</span><span class="p">)</span>

<span class="c1"># 4. Label smoothing
</span><span class="k">print</span><span class="p">(</span><span class="s">"Applying label smoothing..."</span><span class="p">)</span>
<span class="n">alpha</span> <span class="o">=</span> <span class="mf">0.2</span>
<span class="n">P1</span> <span class="o">=</span> <span class="n">label_smoothing_on_ppi</span><span class="p">(</span><span class="n">P0</span><span class="p">,</span> <span class="n">A</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="n">alpha</span><span class="p">)</span>
<span class="n">P1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">clip</span><span class="p">(</span><span class="n">P1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>

<span class="c1"># 5. Hierarchy closure
</span><span class="k">print</span><span class="p">(</span><span class="s">"Enforcing hierarchy consistency..."</span><span class="p">)</span>
<span class="n">P1</span> <span class="o">=</span> <span class="n">close_under_ancestors</span><span class="p">(</span><span class="n">P1</span><span class="p">,</span> <span class="n">go_dag</span><span class="p">)</span>

<span class="c1"># 6. Threshold tuning
</span><span class="k">print</span><span class="p">(</span><span class="s">"Tuning thresholds..."</span><span class="p">)</span>
<span class="n">thresholds</span><span class="p">,</span> <span class="n">fmax</span> <span class="o">=</span> <span class="n">tune_per_class_thresholds</span><span class="p">(</span><span class="n">P1</span><span class="p">,</span> <span class="n">Y_val</span><span class="p">)</span>
<span class="n">Y_pred</span> <span class="o">=</span> <span class="p">(</span><span class="n">P1</span> <span class="o">&gt;=</span> <span class="n">thresholds</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>

<span class="c1"># 7. Evaluate
</span><span class="k">print</span><span class="p">(</span><span class="s">"Evaluating..."</span><span class="p">)</span>
<span class="n">fmax_bp</span><span class="p">,</span> <span class="n">fmax_mf</span><span class="p">,</span> <span class="n">fmax_cc</span> <span class="o">=</span> <span class="n">compute_fmax_by_ontology</span><span class="p">(</span><span class="n">Y_val</span><span class="p">,</span> <span class="n">Y_pred</span><span class="p">,</span> <span class="n">go_dag</span><span class="p">)</span>
<span class="n">micro_auprc</span><span class="p">,</span> <span class="n">macro_auprc</span> <span class="o">=</span> <span class="n">compute_auprc</span><span class="p">(</span><span class="n">Y_val</span><span class="p">,</span> <span class="n">P1</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Fmax (BP): </span><span class="si">{</span><span class="n">fmax_bp</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Fmax (MF): </span><span class="si">{</span><span class="n">fmax_mf</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Fmax (CC): </span><span class="si">{</span><span class="n">fmax_cc</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Micro AUPRC: </span><span class="si">{</span><span class="n">micro_auprc</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Macro AUPRC: </span><span class="si">{</span><span class="n">macro_auprc</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="when-to-graduate-to-gnns">When to Graduate to GNNs</h2>

<p><strong>Stick with baseline if:</strong></p>
<ul>
  <li>Fmax &gt; 0.5 on validation set</li>
  <li>Training time &lt; 1 hour</li>
  <li>Simple features (PLM + PPI smoothing) sufficient</li>
</ul>

<p><strong>Upgrade to GNNs when:</strong></p>
<ul>
  <li>Baseline plateaus (Fmax &lt; 0.4)</li>
  <li>PPI network is dense and informative</li>
  <li>You have structure data (3D coordinates)</li>
  <li>You need to model complex protein interactions</li>
</ul>

<p><strong>GNN architecture selection:</strong></p>
<ul>
  <li><strong>GCN</strong>: Simple, fast, good for dense graphs</li>
  <li><strong>GAT</strong>: Attention mechanism, better for heterogeneous graphs</li>
  <li><strong>GraphSAGE</strong>: Inductive learning, handles new proteins</li>
  <li><strong>GIN</strong>: More expressive, better for complex patterns</li>
</ul>

<hr />

<h2 id="ablation-study-template">Ablation Study Template</h2>

<p><strong>Compare different approaches:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">results</span> <span class="o">=</span> <span class="p">{}</span>

<span class="c1"># Baseline: PLM only
</span><span class="n">clf_plm</span> <span class="o">=</span> <span class="n">train_logistic</span><span class="p">(</span><span class="n">X_plm</span><span class="p">,</span> <span class="n">Y_train</span><span class="p">)</span>
<span class="n">P_plm</span> <span class="o">=</span> <span class="n">clf_plm</span><span class="p">.</span><span class="n">predict_proba</span><span class="p">(</span><span class="n">X_val</span><span class="p">)</span>
<span class="n">results</span><span class="p">[</span><span class="s">'PLM only'</span><span class="p">]</span> <span class="o">=</span> <span class="n">evaluate</span><span class="p">(</span><span class="n">Y_val</span><span class="p">,</span> <span class="n">P_plm</span><span class="p">)</span>

<span class="c1"># + Homology
</span><span class="n">X_combined</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">concatenate</span><span class="p">([</span><span class="n">X_plm</span><span class="p">,</span> <span class="n">X_homology</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">clf_hom</span> <span class="o">=</span> <span class="n">train_logistic</span><span class="p">(</span><span class="n">X_combined</span><span class="p">,</span> <span class="n">Y_train</span><span class="p">)</span>
<span class="n">P_hom</span> <span class="o">=</span> <span class="n">clf_hom</span><span class="p">.</span><span class="n">predict_proba</span><span class="p">(</span><span class="n">X_val</span><span class="p">)</span>
<span class="n">results</span><span class="p">[</span><span class="s">'PLM + Homology'</span><span class="p">]</span> <span class="o">=</span> <span class="n">evaluate</span><span class="p">(</span><span class="n">Y_val</span><span class="p">,</span> <span class="n">P_hom</span><span class="p">)</span>

<span class="c1"># + PPI smoothing
</span><span class="n">P_smooth</span> <span class="o">=</span> <span class="n">label_smoothing_on_ppi</span><span class="p">(</span><span class="n">P_plm</span><span class="p">,</span> <span class="n">A</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.2</span><span class="p">)</span>
<span class="n">results</span><span class="p">[</span><span class="s">'PLM + PPI smoothing'</span><span class="p">]</span> <span class="o">=</span> <span class="n">evaluate</span><span class="p">(</span><span class="n">Y_val</span><span class="p">,</span> <span class="n">P_smooth</span><span class="p">)</span>

<span class="c1"># + Hierarchy closure
</span><span class="n">P_closed</span> <span class="o">=</span> <span class="n">close_under_ancestors</span><span class="p">(</span><span class="n">P_smooth</span><span class="p">,</span> <span class="n">go_dag</span><span class="p">)</span>
<span class="n">results</span><span class="p">[</span><span class="s">'PLM + PPI + Hierarchy'</span><span class="p">]</span> <span class="o">=</span> <span class="n">evaluate</span><span class="p">(</span><span class="n">Y_val</span><span class="p">,</span> <span class="n">P_closed</span><span class="p">)</span>

<span class="c1"># + Per-class thresholds
</span><span class="n">thresholds</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">tune_per_class_thresholds</span><span class="p">(</span><span class="n">P_closed</span><span class="p">,</span> <span class="n">Y_val</span><span class="p">)</span>
<span class="n">Y_pred</span> <span class="o">=</span> <span class="p">(</span><span class="n">P_closed</span> <span class="o">&gt;=</span> <span class="n">thresholds</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
<span class="n">results</span><span class="p">[</span><span class="s">'Full pipeline'</span><span class="p">]</span> <span class="o">=</span> <span class="n">evaluate</span><span class="p">(</span><span class="n">Y_val</span><span class="p">,</span> <span class="n">Y_pred</span><span class="p">)</span>

<span class="c1"># Print results table
</span><span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">Ablation Results:"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"="</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
<span class="k">for</span> <span class="n">method</span><span class="p">,</span> <span class="n">metrics</span> <span class="ow">in</span> <span class="n">results</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">method</span><span class="si">:</span><span class="mi">30</span><span class="n">s</span><span class="si">}</span><span class="s"> Fmax=</span><span class="si">{</span><span class="n">metrics</span><span class="p">[</span><span class="s">'fmax'</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s"> AUPRC=</span><span class="si">{</span><span class="n">metrics</span><span class="p">[</span><span class="s">'auprc'</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="common-pitfalls">Common Pitfalls</h2>

<h3 id="pitfall-1-oversmoothing">Pitfall 1: Oversmoothing</h3>

<p><strong>Problem:</strong> Too many smoothing steps or high alpha blurs signal.</p>

<p><strong>Solution:</strong></p>
<ul>
  <li>Use one-step smoothing (alpha = 0.1-0.2)</li>
  <li>Monitor performance: if smoothing hurts, reduce alpha</li>
  <li>Filter hub proteins (high degree) from smoothing</li>
</ul>

<h3 id="pitfall-2-ignoring-hierarchy">Pitfall 2: Ignoring Hierarchy</h3>

<p><strong>Problem:</strong> Predictions violate ancestor-child relationships.</p>

<p><strong>Solution:</strong></p>
<ul>
  <li>Always apply hierarchy closure</li>
  <li>Use per-class thresholds (higher for general terms)</li>
  <li>Report both pre- and post-closure metrics</li>
</ul>

<h3 id="pitfall-3-data-leakage">Pitfall 3: Data Leakage</h3>

<p><strong>Problem:</strong> Test set contains proteins from training time period.</p>

<p><strong>Solution:</strong></p>
<ul>
  <li>Strict time-based split (test after training cutoff)</li>
  <li>Remove IEA evidence from both train and test</li>
  <li>Document split dates in paper/report</li>
</ul>

<hr />

<h2 id="best-practices-summary">Best Practices Summary</h2>

<ol>
  <li><strong>Start simple</strong>: PLM + logistic regression baseline</li>
  <li><strong>Add smoothing</strong>: One-step PPI label smoothing (alpha = 0.2)</li>
  <li><strong>Enforce hierarchy</strong>: Ancestor closure after thresholding</li>
  <li><strong>Tune thresholds</strong>: Per-class thresholds for Fmax</li>
  <li><strong>Graduate carefully</strong>: Only add GNNs when baseline plateaus</li>
  <li><strong>Ablate everything</strong>: Compare each component’s contribution</li>
  <li><strong>Document splits</strong>: Time-based splits, evidence filtering</li>
</ol>

<p><strong>Bottom line:</strong> This pipeline is simple, strong, and extensible; add GNNs (GCN/GAT) or multimodal fusion when the baseline plateaus.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Agentic AI Needs Guardrails, Not Just Prompts</title>
					
					<link>https://atabakkh.github.io/ai/2025/07/14/agentic-ai-needs-guardrails.html</link>

					

					<pubDate>Mon, 14 Jul 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/ai/2025/07/14/agentic-ai-needs-guardrails</guid>

					
						<category><![CDATA[ AI ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>By mid-2025 “agents” were on every vendor slide. What separated a demo from something operators could live with was rarely a better system prompt. It was permissions, logging, approval flows, and fallbacks.</em></p>

<p><strong>Context:</strong> Tool-using agents moved from lab curiosity to roadmap item. In discussions I was in, the risk profile changed fast: one bad prompt is a mistake; one bad agent is a blast radius. These are the layers I saw teams put in place when they meant to go beyond the deck.</p>

<hr />

<h2 id="chatbot-vs-agent">Chatbot vs agent</h2>

<p><img src="/img/diagrams/agent-guardrail-stack.svg" alt="Agent guardrail stack diagram" width="750" /></p>

<p>A chatbot returns text. An agent may query warehouses, open tickets, trigger workflows, chain tool calls without a human in the loop. That needs infrastructure controls, not only “be careful” in the prompt.</p>

<hr />

<h2 id="layers-that-actually-mattered">Layers that actually mattered</h2>

<p><strong>Identity and permissions.</strong> Dedicated service account per workflow. Scoped to specific datasets, APIs, actions. No shared “analytics admin” for production agents. Pass user context explicitly.</p>

<p><strong>Tool allowlists.</strong> Deny by default.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">agent</span><span class="pi">:</span> <span class="s">support_triage</span>
<span class="na">allowed_tools</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">search_kb_index</span>
  <span class="pi">-</span> <span class="s">read_case_summary</span>
  <span class="pi">-</span> <span class="s">create_draft_reply</span>
<span class="na">denied_tools</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">execute_sql_adhoc</span>
  <span class="pi">-</span> <span class="s">delete_records</span>
  <span class="pi">-</span> <span class="s">export_bulk_pii</span>
<span class="na">max_tool_calls_per_run</span><span class="pi">:</span> <span class="m">8</span>
</code></pre></div></div>

<p><strong>Data boundaries.</strong> Retrieval limited to approved collections. Row/column filters before context hits the model. <code class="language-plaintext highlighter-rouge">as_of</code> and source IDs on chunks. Prompts do not enforce GDPR. Technical controls enforce data boundaries, while lawful basis, retention, data-subject rights, and organizational processes remain part of compliance.</p>

<p><strong>Approval by risk.</strong></p>

<table>
  <thead>
    <tr>
      <th>Tier</th>
      <th>Examples</th>
      <th>Control</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Read-only</td>
      <td>search, summarize</td>
      <td>Auto</td>
    </tr>
    <tr>
      <td>Low risk</td>
      <td>draft text, suggest tags</td>
      <td>Log + optional review</td>
    </tr>
    <tr>
      <td>High risk</td>
      <td>send email, change record</td>
      <td>Human approval</td>
    </tr>
    <tr>
      <td>Forbidden</td>
      <td>bulk export, privilege change</td>
      <td>Block at tool layer</td>
    </tr>
  </tbody>
</table>

<p><strong>Audit and replay.</strong> Log user, agent version, tools, input hashes, outputs, latency, cost, outcome. Replay for incidents without redoing side effects.</p>

<p><strong>Kill switch.</strong> Env flag or config to disable model calls and return a deterministic safe response. For anything production-facing, I would not skip it.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># lib/kill_switch.py - pattern from personal lab PoC
</span><span class="kn">import</span> <span class="nn">os</span>

<span class="k">def</span> <span class="nf">is_ai_enabled</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"AI_ENABLED"</span><span class="p">,</span> <span class="s">"true"</span><span class="p">).</span><span class="n">lower</span><span class="p">()</span> <span class="o">==</span> <span class="s">"true"</span>

<span class="k">def</span> <span class="nf">with_kill_switch</span><span class="p">(</span><span class="n">fn</span><span class="p">,</span> <span class="n">fallback</span><span class="p">):</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">is_ai_enabled</span><span class="p">():</span>
        <span class="k">return</span> <span class="p">{</span><span class="s">"outcome"</span><span class="p">:</span> <span class="s">"fallback"</span><span class="p">,</span> <span class="s">"result"</span><span class="p">:</span> <span class="n">fallback</span><span class="p">,</span> <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="bp">False</span><span class="p">}</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">{</span><span class="s">"outcome"</span><span class="p">:</span> <span class="s">"success"</span><span class="p">,</span> <span class="s">"result"</span><span class="p">:</span> <span class="n">fn</span><span class="p">(),</span> <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="bp">True</span><span class="p">}</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">{</span><span class="s">"outcome"</span><span class="p">:</span> <span class="s">"error"</span><span class="p">,</span> <span class="s">"result"</span><span class="p">:</span> <span class="n">fallback</span><span class="p">,</span> <span class="s">"error"</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">),</span> <span class="s">"ai_enabled"</span><span class="p">:</span> <span class="bp">True</span><span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="multi-agent-adds-coordination-risk">Multi-agent adds coordination risk</h2>

<p>Shared memory can leak context across trust boundaries. Retry loops multiply cost and tool calls. Blame gets fuzzy.</p>

<p>What helped: orchestrator owns step budget and timeout; sub-agents get minimum context; approval gates between trust zones; cost ceiling per run.</p>

<hr />

<h2 id="metrics-beyond-accuracy">Metrics beyond accuracy</h2>

<p>Tool calls per task (p95), approval queue time, kill-switch rate, policy blocks, cost per successful outcome, human override rate. When override rate climbs, tools are too powerful or guardrails too loose.</p>

<hr />

<h2 id="anti-patterns">Anti-patterns</h2>

<p>“Start open, tighten later” - incidents happen in week two. Prompt-only PII rules. Same agent for internal ops and customer-facing. No version pin so a model update quietly changes behavior.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Agentic AI is an operations and security problem wearing an innovation badge. Permissions, allowlists, approvals, audit, kill switches - that is what separates a useful agent from an expensive autocomplete loop.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Hierarchy-Consistent Inference for GO Predictions</title>
					
					<link>https://atabakkh.github.io/computationalbiology/2025/05/10/hierarchy-consistent-inference.html</link>

					

					<pubDate>Sat, 10 May 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/computationalbiology/2025/05/10/hierarchy-consistent-inference</guid>

					
						<category><![CDATA[ ComputationalBiology ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Problem.</strong> Independent multilabel classifiers for Gene Ontology (GO) often violate the <strong>ancestor rule</strong>: if a child term is predicted “on,” <strong>all its ancestors must also be on</strong>. Curators then fix outputs by hand, and metrics inflate/deflate unpredictably.</p>

<p>This note shows fast, reproducible ways to enforce hierarchy consistency at inference time, plus alternatives and measurement guidance.</p>

<hr />

<h2 id="background-what-consistency-means-in-go">Background: what “consistency” means in GO</h2>

<ul>
  <li>GO is a <strong>DAG</strong> with relations like <code class="language-plaintext highlighter-rouge">is_a</code>, <code class="language-plaintext highlighter-rouge">part_of</code> (and others, e.g., <code class="language-plaintext highlighter-rouge">regulates</code>).</li>
  <li>In practice, for classification we usually <strong>propagate annotations along <code class="language-plaintext highlighter-rouge">is_a</code></strong> and optionally selected transitive relations.</li>
  <li><strong>Consistency constraint:</strong> for any protein (i) and term (t), if (Y_{i,t}=1) then for every ancestor (a\in \mathrm{Anc}(t)), (Y_{i,a}=1).</li>
</ul>

<p>You must state which relations you close under (e.g., <code class="language-plaintext highlighter-rouge">is_a</code> only, or <code class="language-plaintext highlighter-rouge">is_a</code> + <code class="language-plaintext highlighter-rouge">part_of</code>) and ensure your <em>train/eval labels</em> were also <strong>ancestor-closed</strong>.</p>

<hr />

<h2 id="fast-closure-algorithm-set-based-post-threshold">Fast closure algorithm (set-based, post-threshold)</h2>

<p>Ensures every predicted child has all ancestors after thresholding.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">close_under_ancestors</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="n">go_dag</span><span class="p">,</span> <span class="n">thr</span><span class="p">):</span>
    <span class="s">"""
    scores: np.ndarray [N, C] class probabilities (0..1)
    go_dag: object exposing .ancestors(term) and .topo_order()
    thr:    float or per-class array; threshold(s) in [0,1]
    returns: np.uint8 matrix [N, C] with ancestor-closed multilabels
    """</span>
    <span class="n">Y</span> <span class="o">=</span> <span class="p">(</span><span class="n">scores</span> <span class="o">&gt;=</span> <span class="n">thr</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">uint8</span><span class="p">)</span>

    <span class="c1"># parents appear before children in topo order
</span>    <span class="n">order</span> <span class="o">=</span> <span class="n">go_dag</span><span class="p">.</span><span class="n">topo_order</span><span class="p">()</span>          <span class="c1"># e.g., list of term indices
</span>    <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="n">order</span><span class="p">):</span>             <span class="c1"># visit children before parents
</span>        <span class="k">for</span> <span class="n">a</span> <span class="ow">in</span> <span class="n">go_dag</span><span class="p">.</span><span class="n">ancestors</span><span class="p">(</span><span class="n">t</span><span class="p">):</span>     <span class="c1"># ancestors(a) excludes t
</span>            <span class="c1"># If child t is on, force ancestor a on
</span>            <span class="n">Y</span><span class="p">[:,</span> <span class="n">a</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">Y</span><span class="p">[:,</span> <span class="n">a</span><span class="p">],</span> <span class="n">Y</span><span class="p">[:,</span> <span class="n">t</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">Y</span>
</code></pre></div></div>

<p><strong>When to use:</strong> simple, transparent, and fast for most setups; great as a default.</p>

<hr />

<h2 id="vectorized-form-fast-for-large-c">Vectorized form (fast for large C)</h2>

<p>Precompute a <strong>binary ancestor matrix</strong> (A\in{0,1}^{C\times C}) where (A_{a,t}=1) if (a\in\mathrm{Anc}(t)).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># A_anc: [C, C] with 1 if row=ancestor, col=child
# Y:     [N, C] initial post-threshold labels
# Y_closed = Y OR (Y @ A_anc^T)
</span><span class="n">Y_closed</span> <span class="o">=</span> <span class="p">(</span><span class="n">Y</span> <span class="o">|</span> <span class="p">(</span><span class="n">Y</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">A_anc</span><span class="p">.</span><span class="n">T</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">uint8</span><span class="p">)</span>
</code></pre></div></div>

<p>This does the same “turn on all ancestors” in a single matrix multiply (Boolean semiring approximated with dot+&gt;0). Build <code class="language-plaintext highlighter-rouge">A_anc</code> once from your DAG (careful with memory if C is large; use <code class="language-plaintext highlighter-rouge">scipy.sparse</code>).</p>

<hr />

<h2 id="probability-space-consistency-pre-threshold">Probability-space consistency (pre-threshold)</h2>

<p>If you keep <strong>probabilities</strong> consistent <em>before</em> thresholding, you can apply class-specific thresholds later without re-violating parents.</p>

<p><strong>Simple projection:</strong> for each edge (a\to t), enforce (p(t)\le p(a)):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">monotone_project_probs</span><span class="p">(</span><span class="n">P</span><span class="p">,</span> <span class="n">edges</span><span class="p">,</span> <span class="n">iters</span><span class="o">=</span><span class="mi">1</span><span class="p">):</span>
    <span class="s">"""
    P: np.ndarray [N, C] class probabilities
    edges: list of (parent, child) index pairs consistent with topo order
    """</span>
    <span class="n">P</span> <span class="o">=</span> <span class="n">P</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">iters</span><span class="p">):</span>  <span class="c1"># a few passes often suffice
</span>        <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">edges</span><span class="p">:</span>  <span class="c1"># parent, child
</span>            <span class="c1"># clamp child to parent if it violates monotonicity
</span>            <span class="n">P</span><span class="p">[:,</span> <span class="n">t</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">minimum</span><span class="p">(</span><span class="n">P</span><span class="p">[:,</span> <span class="n">t</span><span class="p">],</span> <span class="n">P</span><span class="p">[:,</span> <span class="n">a</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">P</span>
</code></pre></div></div>

<ul>
  <li><strong>Pros:</strong> thresholds won’t re-break the hierarchy.</li>
  <li><strong>Cons:</strong> order-dependent; a few passes help. For stricter solutions, see “Energy-based constraints.”</li>
</ul>

<hr />

<h2 id="alternatives-when-you-need-more-than-post-hoc-closure">Alternatives (when you need more than post-hoc closure)</h2>

<ol>
  <li>
    <p><strong>Hierarchical loss (train-time):</strong> add a penalty or constraint that encourages (p(child)\le p(parent)).</p>

    <ul>
      <li>Example: ( \lambda\sum_{(a,t)} \max(0, p_t - p_a)^2 )</li>
      <li>Pros: model learns constraints; fewer violations.</li>
      <li>Cons: hyper-parameters; possible under-confidence for children.</li>
    </ul>
  </li>
  <li>
    <p><strong>Lowest-ancestor prediction (LA):</strong> predict only the minimal set (no predicted term has a predicted child). Expand to ancestors for the final set.</p>

    <ul>
      <li>Pros: small final label sets; curator-friendly.</li>
      <li>Cons: can hurt recall for mid/upper-level terms if thresholds aren’t tuned well.</li>
    </ul>
  </li>
  <li>
    <table>
      <tbody>
        <tr>
          <td><strong>Energy-based / projected inference:</strong> solve (\min</td>
          <td>p - \hat p</td>
          <td>^2) s.t. (p(child)\le p(parent)) over the DAG (isotonic regression on a DAG).</td>
        </tr>
      </tbody>
    </table>

    <ul>
      <li>Pros: principled projection to the feasible set; order-invariant.</li>
      <li>Cons: more complex (requires a DAG isotonic solver).</li>
    </ul>
  </li>
</ol>

<hr />

<h2 id="where-to-apply-closure-before-vs-after-threshold">Where to apply closure: <strong>before</strong> vs <strong>after</strong> threshold</h2>

<ul>
  <li>
    <p><strong>Pre-threshold (probabilities):</strong> monotone projection -&gt; then per-class thresholds.</p>

    <ul>
      <li>Advantage: any thresholding respects hierarchy; better calibration downstream.</li>
      <li>Caveat: may shrink child probabilities; report calibration metrics.</li>
    </ul>
  </li>
  <li>
    <p><strong>Post-threshold (labels):</strong> fastest, easiest; takes your <code class="language-plaintext highlighter-rouge">Y</code> and enforces ancestor closure.</p>

    <ul>
      <li>Advantage: robust, minimal surgery.</li>
      <li>Caveat: metrics like precision/recall can jump because parents get turned on; <strong>report both</strong> pre- and post-closure metrics in ablations.</li>
    </ul>
  </li>
</ul>

<blockquote>
  <p><strong>Recommendation:</strong> keep the <strong>post-threshold closure</strong> as your mainline (transparent), and optionally provide <strong>pre-threshold monotone projection</strong> for calibration-sensitive users.</p>
</blockquote>

<hr />

<h2 id="practical-details-you-should-document">Practical details you should document</h2>

<ul>
  <li><strong>Ontology scope:</strong> BP/MF/CC trained separately or jointly?</li>
  <li><strong>Relations closed:</strong> <code class="language-plaintext highlighter-rouge">is_a</code> only (common), or <code class="language-plaintext highlighter-rouge">is_a</code> + <code class="language-plaintext highlighter-rouge">part_of</code> (state it).</li>
  <li><strong>Obsoletes &amp; alt IDs:</strong> remove obsoletes; map alt IDs to primaries before building DAG.</li>
  <li><strong>Cross-namespace edges:</strong> if present, specify handling.</li>
  <li><strong>Thresholds:</strong> global vs per-class; how tuned (Fmax on validation)?</li>
  <li><strong>Evidence filtering:</strong> whether IEA is allowed; keep train/eval policies consistent.</li>
  <li><strong>Time-based split:</strong> to avoid future leakage.</li>
</ul>

<hr />

<h2 id="complexity--data-structures">Complexity &amp; data structures</h2>

<ul>
  <li><strong>Set-based closure:</strong> (O(N \times E)) per batch, where (E) = number of DAG edges; with vectorization (O(N \times \mathrm{nnz}(A))).</li>
  <li><strong>Ancestor matrix build:</strong> one-time DAG transitive closure; use <strong>sparse</strong> matrices for (C\sim 5-10k).</li>
  <li><strong>Memory:</strong> a dense (C\times C) can be large; prefer <code class="language-plaintext highlighter-rouge">scipy.sparse.csr_matrix</code>.</li>
</ul>

<p><strong>Sparse example (post-threshold vectorized):</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">from</span> <span class="nn">scipy.sparse</span> <span class="kn">import</span> <span class="n">csr_matrix</span>

<span class="c1"># Build sparse ancestor matrix A_anc [C, C] once:
# rows = ancestor indices, cols = child indices (value=1)
</span><span class="n">A_anc</span> <span class="o">=</span> <span class="n">csr_matrix</span><span class="p">((</span><span class="n">data</span><span class="p">,</span> <span class="p">(</span><span class="n">rows</span><span class="p">,</span> <span class="n">cols</span><span class="p">)),</span> <span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="n">C</span><span class="p">,</span> <span class="n">C</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">close_labels_sparse</span><span class="p">(</span><span class="n">Y</span><span class="p">):</span>
    <span class="c1"># Y: [N, C] uint8
</span>    <span class="c1"># Y_closed = Y OR (Y @ A_anc^T)  (Boolean-OR semantics)
</span>    <span class="n">propagated</span> <span class="o">=</span> <span class="p">(</span><span class="n">Y</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">A_anc</span><span class="p">.</span><span class="n">T</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">uint8</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">Y</span><span class="p">,</span> <span class="n">propagated</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="torch-variant-batched-gpu">Torch variant (batched GPU)</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>

<span class="c1"># P: [B, C] probs; edges: LongTensor [E, 2] (parent, child) topo-sorted
</span><span class="k">def</span> <span class="nf">monotone_project_probs_torch</span><span class="p">(</span><span class="n">P</span><span class="p">,</span> <span class="n">edges</span><span class="p">,</span> <span class="n">iters</span><span class="o">=</span><span class="mi">1</span><span class="p">):</span>
    <span class="n">P</span> <span class="o">=</span> <span class="n">P</span><span class="p">.</span><span class="n">clone</span><span class="p">()</span>
    <span class="n">parents</span><span class="p">,</span> <span class="n">children</span> <span class="o">=</span> <span class="n">edges</span><span class="p">[:,</span><span class="mi">0</span><span class="p">],</span> <span class="n">edges</span><span class="p">[:,</span><span class="mi">1</span><span class="p">]</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">iters</span><span class="p">):</span>
        <span class="n">P</span><span class="p">[:,</span> <span class="n">children</span><span class="p">]</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">minimum</span><span class="p">(</span><span class="n">P</span><span class="p">[:,</span> <span class="n">children</span><span class="p">],</span> <span class="n">P</span><span class="p">[:,</span> <span class="n">parents</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">P</span>

<span class="c1"># Post-threshold closure using a sparse COO ancestor matrix A_anc: [C, C]
</span><span class="k">def</span> <span class="nf">close_labels_torch</span><span class="p">(</span><span class="n">Y</span><span class="p">,</span> <span class="n">A_anc</span><span class="p">):</span>
    <span class="c1"># Y: [B, C] uint8 or bool; A_anc: torch.sparse_coo_tensor
</span>    <span class="n">propagated</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">sparse</span><span class="p">.</span><span class="n">mm</span><span class="p">(</span><span class="n">A_anc</span><span class="p">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">),</span> <span class="n">Y</span><span class="p">.</span><span class="n">T</span><span class="p">.</span><span class="nb">float</span><span class="p">()).</span><span class="n">T</span> <span class="o">&gt;</span> <span class="mi">0</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">Y</span><span class="p">.</span><span class="nb">bool</span><span class="p">()</span> <span class="o">|</span> <span class="n">propagated</span><span class="p">).</span><span class="n">to</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="n">uint8</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="measurement">Measurement</h2>

<p>Always <strong>report both</strong> the raw (possibly inconsistent) and the consistent results in ablations.</p>

<ul>
  <li><strong>Violations (%):</strong> fraction of predicted child labels whose ancestors were off (raw); should be ~0 after closure.</li>
  <li><strong>ΔFmax:</strong> change in Fmax per ontology after closure (often <strong>improves</strong> due to fewer false negatives on parents).</li>
  <li><strong>Calibration (ECE):</strong> pre- vs post-monotone projection if you use probability-space constraints.</li>
  <li><strong>Curator effort:</strong> time saved (qualitative) or % predictions already curator-consistent.</li>
</ul>

<p><strong>Ablation table template</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>| Method                         | Viol% | Fmax_BPP | Fmax_MF | Fmax_CC | micro-auPRC |
|--------------------------------|-------|----------|---------|---------|-------------|
| PLM + per-class thr (raw)      | 12.4  | 0.41     | 0.52    | 0.49    | 0.36        |
| + Post-thr closure (set)       | 0.0   | 0.43     | 0.54    | 0.50    | 0.37        |
| + Pre-thr monotone projection  | 0.0   | 0.44     | 0.55    | 0.51    | 0.38        |
</code></pre></div></div>

<hr />

<h2 id="when-closure-can-hurt-and-how-to-mitigate">When closure can <em>hurt</em> and how to mitigate</h2>

<ul>
  <li>
    <p><strong>Over-activation of very general terms</strong> (high up the DAG) can reduce precision.
<strong>Mitigation:</strong> use <strong>per-class thresholds</strong> (higher for general terms), or apply closure <strong>after</strong> minimal-set selection (lowest-ancestor strategy).</p>
  </li>
  <li>
    <p><strong>Noisy children forcing noisy parents on.</strong>
<strong>Mitigation:</strong> apply <strong>probability-space projection</strong> first, which tends to dampen child scores that exceed parents.</p>
  </li>
</ul>

<hr />

<h2 id="recommendations-practical-defaults">Recommendations (practical defaults)</h2>

<ol>
  <li>Make training labels <strong>ancestor-closed</strong>.</li>
  <li>Use <strong>per-class thresholds</strong> tuned for Fmax on validation.</li>
  <li>At inference, apply <strong>post-threshold closure</strong> (vectorized) as your default.</li>
  <li>For better calibration, add <strong>one pass</strong> of pre-threshold <strong>monotone projection</strong>.</li>
  <li><strong>Document</strong>: relations used, closure stage, and ablation results.</li>
</ol>

<hr />

<h3 id="references">References</h3>
<p>https://pmc.ncbi.nlm.nih.gov/articles/PMC3654867/
https://genomebiology.biomedcentral.com/articles/10.1186/s13059-019-1835-8
https://www.science.org/doi/10.1126/science.ade2574</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Why Data Pipelines Need SLOs Like Software Services</title>
					
					<link>https://atabakkh.github.io/cloud/2025/04/28/data-pipelines-need-slos.html</link>

					

					<pubDate>Mon, 28 Apr 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2025/04/28/data-pipelines-need-slos</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>Product APIs have SLOs. Data pipelines often only have “it ran green”. That gap is why dashboards lied, models trained on stale data, and on-call found out from Slack.</em></p>

<p><strong>Context:</strong> By spring 2025 SRE practices were normal for services. In platform work I was close to, curated tables were still treated like APIs without any written guarantee. These are notes on what changed when we started writing SLOs for data products - the same idea as in <a href="/devops/2025/04/12/slo-burn-rate-alerts.html">SLO burn-rate alerts</a>, applied to pipelines.</p>

<hr />

<h2 id="tables-are-products-too">Tables are products too</h2>

<p><img src="/img/diagrams/pipeline-slo-loop.svg" alt="Pipeline SLO observability loop" width="750" /></p>

<p>If a table feeds revenue reporting, fraud scoring, or a customer-facing feature, it has consumers with expectations. An SLO makes those expectations measurable: how stale can it be, what quality is acceptable, how often must the run succeed.</p>

<p>Logs tell you what happened. SLOs tell you whether consumers can rely on the data.</p>

<hr />

<h2 id="slis-that-worked-for-us">SLIs that worked for us</h2>

<table>
  <thead>
    <tr>
      <th>SLI</th>
      <th>How we measured</th>
      <th>Example target</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Freshness</td>
      <td><code class="language-plaintext highlighter-rouge">now() - max(_loaded_at)</code></td>
      <td>p95 ≤ 30 minutes</td>
    </tr>
    <tr>
      <td>Success rate</td>
      <td>successful / expected runs</td>
      <td>≥ 99.5% over 30 days</td>
    </tr>
    <tr>
      <td>Quality pass rate</td>
      <td>checks passed / checks run</td>
      <td>≥ 99.9%</td>
    </tr>
    <tr>
      <td>Reconciliation</td>
      <td>variance vs source</td>
      <td>≤ 0.5% on key metrics</td>
    </tr>
    <tr>
      <td>Job latency</td>
      <td>duration p95</td>
      <td>≤ 2× baseline</td>
    </tr>
  </tbody>
</table>

<p>Two or three SLIs per critical dataset. Not ten.</p>

<hr />

<h2 id="example-we-used-for-curatedorders">Example we used for <code class="language-plaintext highlighter-rouge">curated.orders</code></h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">dataset</span><span class="pi">:</span> <span class="s">curated.orders</span>
<span class="na">consumer_tier</span><span class="pi">:</span> <span class="s">critical</span>
<span class="na">slos</span><span class="pi">:</span>
  <span class="na">freshness</span><span class="pi">:</span>
    <span class="na">sli</span><span class="pi">:</span> <span class="s">minutes_since_last_successful_load</span>
    <span class="na">target</span><span class="pi">:</span> <span class="s">95% of days ≤ 60 minutes</span>
  <span class="na">availability</span><span class="pi">:</span>
    <span class="na">sli</span><span class="pi">:</span> <span class="s">successful_hourly_runs / expected_runs</span>
    <span class="na">target</span><span class="pi">:</span> <span class="s">99.5% over 30 days</span>
  <span class="na">quality</span><span class="pi">:</span>
    <span class="na">sli</span><span class="pi">:</span> <span class="s">quality_checks_passed / quality_checks_total</span>
    <span class="na">target</span><span class="pi">:</span> <span class="s">99.9% over 30 days</span>
<span class="na">error_budget_policy</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">burn &gt; 50% in 7d -&gt; freeze non-critical changes</span>
  <span class="pi">-</span> <span class="s">burn exhausted -&gt; incident review + tell consumers</span>
<span class="na">owner</span><span class="pi">:</span> <span class="s">data-platform-core</span>
</code></pre></div></div>

<hr />

<h2 id="burn-rates-for-data">Burn rates for data</h2>

<p>Same idea as service SRE:</p>

<ul>
  <li>Fast burn - freshness missed several times in a few hours -&gt; page the owner</li>
  <li>Slow burn - quality pass rate drifting for two weeks -&gt; ticket and trend review</li>
</ul>

<p>Signals live in pipeline metadata and quality tables, not HTTP metrics.</p>

<hr />

<h2 id="what-we-instrumented">What we instrumented</h2>

<p>Run table (job, dataset, start, end, status, rows, bytes). Quality results. Freshness view. A short consumer list for who to notify. Even a simple BigQuery view for status was enough to start.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- ops.run_log + freshness status (illustrative)</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="n">ops</span><span class="p">.</span><span class="n">pipeline_runs</span> <span class="p">(</span>
  <span class="n">run_id</span> <span class="n">STRING</span><span class="p">,</span> <span class="n">dataset</span> <span class="n">STRING</span><span class="p">,</span> <span class="n">scheduled_at</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="n">started_at</span> <span class="nb">TIMESTAMP</span><span class="p">,</span> <span class="n">ended_at</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="n">status</span> <span class="n">STRING</span><span class="p">,</span> <span class="n">rows_written</span> <span class="n">INT64</span><span class="p">,</span> <span class="n">bytes_scanned</span> <span class="n">INT64</span>
<span class="p">);</span>

<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="n">ops</span><span class="p">.</span><span class="n">expected_pipeline_runs</span> <span class="p">(</span>
  <span class="n">dataset</span> <span class="n">STRING</span><span class="p">,</span> <span class="n">scheduled_at</span> <span class="nb">TIMESTAMP</span>
<span class="p">);</span>

<span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="n">ops</span><span class="p">.</span><span class="n">data_product_slo_status</span> <span class="k">AS</span>
<span class="k">WITH</span> <span class="n">observed</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">dataset</span><span class="p">,</span>
    <span class="n">scheduled_at</span><span class="p">,</span>
    <span class="k">MAX</span><span class="p">(</span><span class="n">IF</span><span class="p">(</span><span class="n">status</span> <span class="o">=</span> <span class="s1">'success'</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="k">AS</span> <span class="n">succeeded</span><span class="p">,</span>
    <span class="k">MAX</span><span class="p">(</span><span class="n">IF</span><span class="p">(</span><span class="n">status</span> <span class="o">=</span> <span class="s1">'success'</span><span class="p">,</span> <span class="n">ended_at</span><span class="p">,</span> <span class="k">NULL</span><span class="p">))</span> <span class="k">AS</span> <span class="n">successful_at</span>
  <span class="k">FROM</span> <span class="n">ops</span><span class="p">.</span><span class="n">pipeline_runs</span>
  <span class="k">WHERE</span> <span class="n">scheduled_at</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
  <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">dataset</span><span class="p">,</span> <span class="n">scheduled_at</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">expected</span><span class="p">.</span><span class="n">dataset</span><span class="p">,</span>
  <span class="n">TIMESTAMP_DIFF</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="k">MAX</span><span class="p">(</span><span class="n">observed</span><span class="p">.</span><span class="n">successful_at</span><span class="p">),</span> <span class="k">MINUTE</span><span class="p">)</span> <span class="k">AS</span> <span class="n">lag_min</span><span class="p">,</span>
  <span class="k">AVG</span><span class="p">(</span><span class="n">COALESCE</span><span class="p">(</span><span class="n">observed</span><span class="p">.</span><span class="n">succeeded</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="k">AS</span> <span class="n">success_rate_30d</span>
<span class="k">FROM</span> <span class="n">ops</span><span class="p">.</span><span class="n">expected_pipeline_runs</span> <span class="k">AS</span> <span class="n">expected</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">observed</span> <span class="k">USING</span> <span class="p">(</span><span class="n">dataset</span><span class="p">,</span> <span class="n">scheduled_at</span><span class="p">)</span>
<span class="k">WHERE</span> <span class="n">expected</span><span class="p">.</span><span class="n">scheduled_at</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
  <span class="k">AND</span> <span class="n">expected</span><span class="p">.</span><span class="n">scheduled_at</span> <span class="o">&lt;=</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">()</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">expected</span><span class="p">.</span><span class="n">dataset</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="what-the-conversation-changed">What the conversation changed</h2>

<p>“Hourly is nice; business needs 8am” -&gt; change schedule or SLA.<br />
“We cannot hit 99.9% with vendor delays” -&gt; renegotiate dependency or tier.<br />
“Critical but unowned” -&gt; assign owner before adding consumers.</p>

<p>Data engineers become operators of data products, not only authors of SQL.</p>

<hr />

<h2 id="how-we-started-about-a-week">How we started (about a week)</h2>

<p>Pick three critical datasets and their consumers. Define one freshness and one quality SLI each. Log results from existing jobs. Add one fast-burn alert. Publish the SLO where consumers can see it. Expand after the first incident proves the point.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Green Airflow tasks are not a reliability strategy. SLOs make expectations explicit. If copilots or scoring jobs read your tables, this is how you know the inputs were fit for use - not only that a job completed.</p>

<p><strong>Related:</strong> <a href="/devops/2025/04/12/slo-burn-rate-alerts.html">SLO Burn-Rate Alerts</a> · <a href="/cloud/2025/03/12/cloud-native-data-sync-pipeline-architecture.html">Cloud-Native Data Sync Pipeline</a></p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>SLO Burn-Rate Alerts that Don't Page You at 3am (Unless They Should)</title>
					
					<link>https://atabakkh.github.io/devops/2025/04/12/slo-burn-rate-alerts.html</link>

					

					<pubDate>Sat, 12 Apr 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/devops/2025/04/12/slo-burn-rate-alerts</guid>

					
						<category><![CDATA[ DevOps ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Goal:</strong> page for <em>user pain</em>, not random metric spikes. Burn-rate alerts do that by measuring <strong>how fast you’re spending the error budget</strong> for your SLO.</p>

<hr />

<h2 id="1-concepts-in-60-seconds">1) Concepts in 60 seconds</h2>

<ul>
  <li><strong>SLO window (T):</strong> e.g., 28 days -&gt; <strong>672 hours</strong>.</li>
  <li><strong>Target (S):</strong> e.g., 99.5% availability -&gt; <strong>error budget (EB) = 1 − S = 0.005</strong> of all requests in T.</li>
  <li><strong>Burn rate (BR):</strong> how fast you’re spending EB:
[
\text{BR} = \frac{\text{observed error ratio over window}}{\text{error budget per unit time}} \quad
\approx \quad \frac{\text{errors}/\text{requests}}{EB/T}
]
If BR = 1, you’ll exactly exhaust the budget by the end of T.
If BR = 10, you’d burn <strong>10× too fast</strong>.</li>
</ul>

<p>We alert on <strong>two windows</strong> to catch both “oh-no right now” and “quietly getting worse.”</p>

<hr />

<h2 id="2-pick-two-windows-28-day-slo">2) Pick two windows (28-day SLO)</h2>

<ul>
  <li><strong>Fast burn:</strong> 2h over 28d -&gt; <strong>page on-call now</strong></li>
  <li><strong>Slow burn:</strong> 6h over 28d -&gt; <strong>page during business hours</strong></li>
</ul>

<blockquote>
  <p>The <strong>threshold K</strong> you pick says “if we sustained this for the whole window, we’d consume X% of the total budget.”
For a 28-day window, the mapping is simple:
[
K ;=; \frac{\text{budget fraction to spend in the short window}}{\text{window hours}/672}
]</p>
</blockquote>

<p>Examples (28-day window):</p>

<table>
  <thead>
    <tr>
      <th>Window</th>
      <th style="text-align: right">Spend of total EB</th>
      <th style="text-align: right">K (burn-rate threshold)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1h</td>
      <td style="text-align: right">2%</td>
      <td style="text-align: right">13.44</td>
    </tr>
    <tr>
      <td>2h</td>
      <td style="text-align: right">2%</td>
      <td style="text-align: right">6.72</td>
    </tr>
    <tr>
      <td>6h</td>
      <td style="text-align: right">5%</td>
      <td style="text-align: right">5.60</td>
    </tr>
    <tr>
      <td>12h</td>
      <td style="text-align: right">5%</td>
      <td style="text-align: right">2.80</td>
    </tr>
    <tr>
      <td>24h</td>
      <td style="text-align: right">10%</td>
      <td style="text-align: right">2.80</td>
    </tr>
  </tbody>
</table>

<p>A common pair for 28d is <strong>(2h, K~6.72)</strong> and <strong>(6h, K~5.6)</strong>.
(If you use a 30-day window, these become ~14.4 and ~6, respectively.)</p>

<hr />

<h2 id="3-availability-slo---prometheus-examples">3) Availability SLO - Prometheus examples</h2>

<p>Assume:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">requests_total{status=~"5.."} = errors_total</code></li>
  <li><code class="language-plaintext highlighter-rouge">requests_total</code> includes all (or just user-visible) requests</li>
  <li>SLO: <strong>99.5% availability</strong> over <strong>28 days</strong></li>
</ul>

<pre><code class="language-promql"># Helper: error ratio over 2h and 6h
slo:error_ratio:2h = increase(errors_total[2h]) / increase(requests_total[2h])
slo:error_ratio:6h = increase(errors_total[6h]) / increase(requests_total[6h])

# Error budget fraction (constant): 1 - 0.995 = 0.005
# Burn rates (ratio of observed error to allowed error per unit time)
slo:burn_rate:2h = slo:error_ratio:2h / (0.005 / 28d)
slo:burn_rate:6h = slo:error_ratio:6h / (0.005 / 28d)
</code></pre>

<p><strong>Alerting rules (use two windows, two severities):</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">groups</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">slo-availability</span>
  <span class="na">rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">alert</span><span class="pi">:</span> <span class="s">SLOAvailabilityFastBurn</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">(increase(errors_total[2h]) / increase(requests_total[2h]))</span>
      <span class="s">&gt; (0.005 * (2h / 28d)) * 6.72</span>
    <span class="na">for</span><span class="pi">:</span> <span class="s">5m</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="na">severity</span><span class="pi">:</span> <span class="s">page</span>
      <span class="na">slo</span><span class="pi">:</span> <span class="s2">"</span><span class="s">availability-99.5-28d"</span>
    <span class="na">annotations</span><span class="pi">:</span>
      <span class="na">summary</span><span class="pi">:</span> <span class="s2">"</span><span class="s">SLO</span><span class="nv"> </span><span class="s">fast</span><span class="nv"> </span><span class="s">burn</span><span class="nv"> </span><span class="s">(2h)</span><span class="nv"> </span><span class="s">-</span><span class="nv"> </span><span class="s">spending</span><span class="nv"> </span><span class="s">error</span><span class="nv"> </span><span class="s">budget</span><span class="nv"> </span><span class="s">too</span><span class="nv"> </span><span class="s">fast"</span>
      <span class="na">runbook_url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://your.site/runbooks/slo-availability"</span>
      <span class="na">description</span><span class="pi">:</span> <span class="pi">&gt;</span>
        <span class="s">Error ratio over 2h is above fast-burn threshold for a 99.5% SLO.</span>
        <span class="s">Check deploys, dependency status, and queue lag.</span>

  <span class="pi">-</span> <span class="na">alert</span><span class="pi">:</span> <span class="s">SLOAvailabilitySlowBurn</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">(increase(errors_total[6h]) / increase(requests_total[6h]))</span>
      <span class="s">&gt; (0.005 * (6h / 28d)) * 5.6</span>
    <span class="na">for</span><span class="pi">:</span> <span class="s">15m</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="na">severity</span><span class="pi">:</span> <span class="s">ticket</span>
      <span class="na">slo</span><span class="pi">:</span> <span class="s2">"</span><span class="s">availability-99.5-28d"</span>
    <span class="na">annotations</span><span class="pi">:</span>
      <span class="na">summary</span><span class="pi">:</span> <span class="s2">"</span><span class="s">SLO</span><span class="nv"> </span><span class="s">slow</span><span class="nv"> </span><span class="s">burn</span><span class="nv"> </span><span class="s">(6h)</span><span class="nv"> </span><span class="s">-</span><span class="nv"> </span><span class="s">sustained</span><span class="nv"> </span><span class="s">error</span><span class="nv"> </span><span class="s">budget</span><span class="nv"> </span><span class="s">burn"</span>
      <span class="na">runbook_url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://your.site/runbooks/slo-availability"</span>
</code></pre></div></div>

<blockquote>
  <p>You can also compute <code class="language-plaintext highlighter-rouge">slo:burn_rate:*</code> as <strong>recording rules</strong> and alert on those to simplify expressions and dashboards.</p>
</blockquote>

<hr />

<h2 id="4-latency-slo-p95---prometheus-examples">4) Latency SLO (p95) - Prometheus examples</h2>

<p>Let the SLO be <strong>p95 &lt; 250ms</strong>. We need a <strong>good-events vs all-events</strong> ratio:</p>

<ul>
  <li>Suppose histogram metrics: <code class="language-plaintext highlighter-rouge">http_request_duration_seconds_bucket{le=...}</code></li>
  <li>“Good” means <code class="language-plaintext highlighter-rouge">le="0.25"</code> (≤ 250ms).</li>
  <li>Error ratio for latency SLO = <strong>bad / total = 1 − good/total</strong></li>
</ul>

<pre><code class="language-promql">slo:good_ratio:2h = (
  sum(rate(http_request_duration_seconds_bucket{le="0.25"}[2h]))
/
  sum(rate(http_request_duration_seconds_count[2h]))
)

slo:error_ratio:2h = 1 - slo:good_ratio:2h
slo:error_ratio:6h = 1 - (
  sum(rate(http_request_duration_seconds_bucket{le="0.25"}[6h]))
/
  sum(rate(http_request_duration_seconds_count[6h]))
)

# Burn rate (same idea, EB = 1 - SLO target)
# If target is "95% under 250ms" then EB = 0.05
slo:burn_rate_latency:2h = slo:error_ratio:2h / (0.05 / 28d)
slo:burn_rate_latency:6h = slo:error_ratio:6h / (0.05 / 28d)
</code></pre>

<p><strong>Alerting:</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">alert</span><span class="pi">:</span> <span class="s">SLOLatencyFastBurn</span>
  <span class="na">expr</span><span class="pi">:</span> <span class="s">slo:burn_rate_latency:2h &gt; </span><span class="m">6.72</span>
  <span class="na">for</span><span class="pi">:</span> <span class="s">5m</span>
  <span class="na">labels</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">severity</span><span class="pi">:</span> <span class="nv">page</span><span class="pi">,</span> <span class="nv">slo</span><span class="pi">:</span> <span class="s2">"</span><span class="s">latency-p95-250ms-28d"</span> <span class="pi">}</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">summary</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Latency</span><span class="nv"> </span><span class="s">SLO</span><span class="nv"> </span><span class="s">fast</span><span class="nv"> </span><span class="s">burn</span><span class="nv"> </span><span class="s">(2h)"</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">p95</span><span class="nv"> </span><span class="s">tail</span><span class="nv"> </span><span class="s">spending</span><span class="nv"> </span><span class="s">error</span><span class="nv"> </span><span class="s">budget</span><span class="nv"> </span><span class="s">too</span><span class="nv"> </span><span class="s">fast."</span>

<span class="pi">-</span> <span class="na">alert</span><span class="pi">:</span> <span class="s">SLOLatencySlowBurn</span>
  <span class="na">expr</span><span class="pi">:</span> <span class="s">slo:burn_rate_latency:6h &gt; </span><span class="m">5.6</span>
  <span class="na">for</span><span class="pi">:</span> <span class="s">15m</span>
  <span class="na">labels</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">severity</span><span class="pi">:</span> <span class="nv">ticket</span><span class="pi">,</span> <span class="nv">slo</span><span class="pi">:</span> <span class="s2">"</span><span class="s">latency-p95-250ms-28d"</span> <span class="pi">}</span>
</code></pre></div></div>

<hr />

<h2 id="5-tuning--good-practices">5) Tuning &amp; good practices</h2>

<ul>
  <li><strong>Silence slow-burn on weekends/night:</strong> route <code class="language-plaintext highlighter-rouge">severity=ticket</code> to business hours; keep <code class="language-plaintext highlighter-rouge">severity=page</code> 24/7.</li>
  <li><strong>Minimum traffic guard:</strong> add <code class="language-plaintext highlighter-rouge">sum(rate(requests_total[2h])) &gt; X</code> to avoid flapping at low QPS.</li>
  <li><strong>Per-endpoint vs service-level:</strong> start <strong>service-level</strong>, then carve out top endpoints that dominate the burn.</li>
  <li><strong>Warm-up suppression:</strong> ignore the first N minutes after deploy if you know cold starts cause noisy tails (Cloud Run <code class="language-plaintext highlighter-rouge">min-instances</code> can help).</li>
  <li><strong>Cost safety:</strong> pair burn alerts with <strong>budget spikes</strong> (FinOps) so you don’t scale into runaway cost during incidents.</li>
</ul>

<hr />

<h2 id="6-runbook---keep-it-one-page">6) Runbook - keep it one page</h2>

<p>Every burn alert must link a <strong>1-page runbook</strong> with <em>exact</em> checks:</p>

<ol>
  <li><strong>What changed?</strong> recent deploy? config? autoscaling?</li>
  <li><strong>Dependencies:</strong> DB/redis/queue health; error signatures; saturation.</li>
  <li><strong>Queues:</strong> <strong>lag vs throughput</strong>; enable backoff / DLQ if retry storms.</li>
  <li><strong>Roll back or scale out:</strong> if deploy-related, roll back; else temporarily raise replicas / min-instances.</li>
  <li><strong>If cost spikes:</strong> check <strong>egress/partitions</strong>, BigQuery scans, unbounded logs.</li>
  <li><strong>Close-out:</strong> open a ticket if slow-burn fired; attach graphs + timeline.</li>
</ol>

<blockquote>
  <p>Aim to <strong>resolve or route in 10 minutes</strong>. Burn-rate alerts should be that actionable.</p>
</blockquote>

<hr />

<h2 id="7-why-this-reduces-noise-and-saves-weekends">7) Why this reduces noise (and saves weekends)</h2>

<ul>
  <li><strong>Two windows</strong> catch both <em>explosions</em> and <em>smolders</em> while ignoring momentary blips.</li>
  <li><strong>Math matches the budget:</strong> alerts fire only when you’re consuming a <strong>non-trivial fraction</strong> of the month’s error budget.</li>
  <li><strong>Clear actions:</strong> the runbook ties alerts to concrete fixes (deploy/rollback, dependency, queue, scaling, cost guardrails).</li>
</ul>

<p><strong>Outcome:</strong> alert volume ↓, actionability ↑, weekend peace restored.</p>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Building a Cloud-Native Data Sync Pipeline: Architecture and Lessons Learned</title>
					
					<link>https://atabakkh.github.io/cloud/2025/03/12/cloud-native-data-sync-pipeline-architecture.html</link>

					

					<pubDate>Wed, 12 Mar 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2025/03/12/cloud-native-data-sync-pipeline-architecture</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>How to design a multi-service sync pipeline from cloud to on-premises (or between systems) using hexagonal architecture, Cloud Workflows, and event-driven orchestration.</em></p>

<p><strong>Context:</strong> Keeping cloud data in sync with on-premises or legacy systems is a common integration problem. This post focuses on <strong>solution architecture and design</strong>; two entry points, one event shape, hexagonal structure per service, and tag-based deploys; so the pattern applies to any sync use case.</p>

<hr />

<h2 id="why-this-kind-of-pipeline">Why this kind of pipeline?</h2>

<p>In many organizations, data lives in the cloud (e.g. a data warehouse/lakehouse ) while operational or legacy systems run elsewhere; on-premises or in another cloud. Keeping them in sync demands reliability, clear ownership of each step, and the ability to trigger syncs both on a schedule and on demand (e.g. from a query or API).</p>

<p>We set out to build a pipeline that:</p>

<ul>
  <li>Syncs data from a <strong>cloud source</strong> to a <strong>target system</strong> (e.g. on-premises or another service).</li>
  <li>Supports <strong>two entry points</strong>: a <strong>scheduled batch</strong> (e.g. every N minutes) and an <strong>on-demand</strong> path; e.g. invokable via a remote function or HTTP API so upstream processes can trigger a sync with a single call.</li>
  <li>Keeps the same downstream behaviour regardless of how the sync was triggered.</li>
  <li>Makes each step testable and replaceable without rewriting the whole flow.</li>
</ul>

<p>This post focuses on the <strong>architecture and design</strong> we used, and lessons that apply to any sync pipeline.</p>

<hr />

<h2 id="high-level-shape-two-triggers-one-pipeline">High-level shape: two triggers, one pipeline</h2>

<p>Conceptually, the system looks like this:</p>

<ol>
  <li>
    <p><strong>Scheduler path</strong><br />
A scheduler calls an HTTP service periodically. The service reads “pending” items from a <strong>queue table</strong> in the source (e.g. BigQuery), publishes one event per item to Pub/Sub, and updates queue status. The trigger does not call the target system directly; it only enqueues work.</p>
  </li>
  <li>
    <p><strong>On-demand path</strong><br />
An external caller (e.g. remote function, API, or another job) calls the same HTTP service with payload data. The service publishes the same kind of event to Pub/Sub and optionally writes the payload into a canonical table so both paths converge in one place.</p>
  </li>
  <li>
    <p><strong>After publish</strong><br />
From here on, both paths are identical: Pub/Sub -&gt; Eventarc -&gt; <strong>Cloud Workflows</strong> -&gt; a fixed sequence of <strong>Cloud Run Jobs</strong> (e.g. data sync stage 1, stage 2, optional delay, reconciliation). The workflow reports status back to the trigger (e.g. SyncStarted, SyncEnded, SyncError) so you can track runs.</p>
  </li>
</ol>

<p>So we have <strong>two ways in</strong>, but <strong>one event shape</strong> and <strong>one orchestration pipeline</strong>. That keeps the design simple and makes it easy to add the on-demand path later without duplicating logic.</p>

<hr />

<h2 id="hexagonal-architecture-per-service">Hexagonal architecture per service</h2>

<p>Each service (trigger and each sync/reconciliation job) follows <strong>hexagonal (ports and adapters)</strong> structure:</p>

<ul>
  <li><strong>Domain</strong>: Core models (e.g. sync event types, entity identifiers) and <strong>ports</strong> (interfaces for “read from queue”, “publish event”, “write to target system”).</li>
  <li><strong>Application</strong>: Use cases that depend only on ports; e.g. “for each pending queue row, build event, publish, update status.”</li>
  <li><strong>Adapters</strong>: Concrete implementations; source client (e.g. BigQuery), Pub/Sub publisher, target system client (e.g. DB or API), HTTP server (e.g. FastAPI for the trigger).</li>
</ul>

<p>Benefits:</p>

<ul>
  <li><strong>Testing</strong>: Unit-test use cases with mocks for the source, Pub/Sub, and target. No need to hit real systems for business logic.</li>
  <li><strong>Swapping implementations</strong>: Changing queue implementation or message broker means implementing the same port in a new adapter, not rewriting the core flow.</li>
  <li><strong>Clarity</strong>: “Business rules” live in application + domain; “I/O” lives in adapters.</li>
</ul>

<p>The <strong>shared core</strong> (event schemas and common types) lives in a small library used by the trigger and the jobs, so payloads stay consistent and producer and consumers don’t drift.</p>

<hr />

<h2 id="event-driven-orchestration-with-cloud-workflows">Event-driven orchestration with Cloud Workflows</h2>

<p>We avoided a single monolith that does “read queue -&gt; sync stage 1 -&gt; sync stage 2 -&gt; reconcile.” That would be hard to scale, hard to retry per step, and would mix concerns. Instead:</p>

<ul>
  <li><strong>Pub/Sub</strong> carries a single event type: e.g. base64-encoded JSON with an entity id and payload.</li>
  <li><strong>Eventarc</strong> fires when a message is published; it starts <strong>one Workflows execution per message</strong>.</li>
  <li>The <strong>workflow</strong> is a linear sequence: decode message -&gt; post SyncStarted -&gt; run Job 1 -&gt; run Job 2 -&gt; optional sleep (if target needs propagation time) -&gt; run Job 3 (e.g. reconciliation) -&gt; post SyncEnded (or SyncError on failure).</li>
</ul>

<p>Each <strong>Cloud Run Job</strong> is a separate deployable: it receives the same encoded event as a CLI argument, decodes it, and does one thing (e.g. “sync data from source to target for this entity”). The workflow is the only place that knows the order and any fixed delays; the jobs are stateless and single-purpose.</p>

<p>What this gives you:</p>

<ul>
  <li><strong>Visibility</strong>: Each run is one workflow execution; you can see exactly which step failed.</li>
  <li><strong>Retries and failure handling</strong>: The workflow can post SyncError with details and support retries or alerts without baking that into each job.</li>
  <li><strong>Scaling</strong>: Jobs scale with workload; the trigger only does lightweight “read queue / build event / publish” and stays simple.</li>
</ul>

<hr />

<h2 id="one-event-many-consumers">One event, many consumers</h2>

<p>A key design choice: <strong>one event schema</strong> for both trigger paths. The payload might look like:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">event_type</code>: e.g. <code class="language-plaintext highlighter-rouge">"created"</code>, <code class="language-plaintext highlighter-rouge">"updated"</code></li>
  <li><code class="language-plaintext highlighter-rouge">payload</code>: entity id and whatever fields the sync stages need (ids, names, types, etc.).</li>
</ul>

<p>The trigger (scheduler or on-demand) is responsible for building this payload. All jobs receive the <strong>same</strong> encoded blob. Each job decodes it and uses only what it needs. So you don’t have “scheduler events” vs “on-demand events”; you have “sync events,” and the pipeline doesn’t care who produced them.</p>

<hr />

<h2 id="cicd-tag-based-prod-and-deploy-what-changed">CI/CD: tag-based prod and “deploy what changed”</h2>

<p>Production deployment is <strong>tag-driven</strong>: pushing a release tag (e.g. <code class="language-plaintext highlighter-rouge">v1.0.0</code>) triggers the prod pipeline. The pipeline:</p>

<ol>
  <li>Fetches tags and checks out the tag that was pushed.</li>
  <li>Compares that tag to the <strong>previous</strong> tag (by creation date) and computes which top-level components (e.g. <code class="language-plaintext highlighter-rouge">sync-trigger</code>, <code class="language-plaintext highlighter-rouge">sync-stage-1</code>, <code class="language-plaintext highlighter-rouge">sync-stage-2</code>, <code class="language-plaintext highlighter-rouge">reconciliation</code>, <code class="language-plaintext highlighter-rouge">infrastructure</code>) have file changes.</li>
  <li>Deploys <strong>only those components</strong>, in a fixed order (e.g. infrastructure first), using the same tag for image versions.</li>
</ol>

<p>So you avoid “deploy everything on every run” and avoid deploying an older build by mistake: the tag defines the release, and the diff defines the scope. Pipeline and shared code are excluded from the deploy list so that changes only in docs or shared libraries don’t trigger full redeploys.</p>

<hr />

<h2 id="reconciliation-as-a-separate-step">Reconciliation as a separate step</h2>

<p>One of the jobs is <strong>reconciliation</strong>: it compares source and target (e.g. row counts or checksums for the same entity) and fails if they differ, so the workflow records SyncError. We kept this as a <strong>dedicated job</strong> rather than embedding it into a sync job because:</p>

<ul>
  <li><strong>Single responsibility</strong>: One job “syncs,” the other “verifies.”</li>
  <li><strong>Clear failure semantics</strong>: A reconciliation failure means “source and target are out of sync,” which is easy to monitor and alert on.</li>
  <li><strong>Same event, same pattern</strong>: It receives the same event and uses the same ports/adapters style, so it fits the rest of the architecture.</li>
</ul>

<hr />

<h2 id="things-wed-do-again-and-one-wed-refine">Things we’d do again (and one we’d refine)</h2>

<ul>
  <li><strong>Hexagonal structure per service</strong>: It paid off for testing and for keeping “how we talk to source/target/Pub/Sub” out of the core logic.</li>
  <li><strong>Single event schema and one pipeline after Pub/Sub</strong>: Adding the on-demand path was mostly “add an HTTP adapter that builds the same event and publishes”; the workflow and jobs didn’t need to change.</li>
  <li><strong>Workflows as the only orchestrator</strong>: Having one place that defines the sequence and error handling made reasoning about the system much easier than spreading orchestration across services.</li>
  <li><strong>Tag-based prod and diff-based deploy</strong>: Reduced risk and made releases predictable.</li>
</ul>

<p>One thing we’d refine: a <strong>fixed sleep</strong> in the workflow (e.g. between two stages) is there when the target system needs time to propagate state before the next step. It’s explicit and safe, but it’s a fixed delay. If we were to revisit it, we’d consider a “poll until ready” step with a timeout instead of a blind sleep, so that fast runs don’t wait unnecessarily; while still keeping the design simple.</p>

<hr />

<h2 id="summary">Summary</h2>

<p>To build a <strong>multi-service, event-driven sync pipeline</strong> (cloud to on-premises or between systems) a simple solution would be as follows:</p>

<ul>
  <li><strong>Two entry points</strong> (scheduler and on-demand) that produce the <strong>same event type</strong> and feed the <strong>same workflow</strong>.</li>
  <li><strong>Hexagonal architecture</strong> in each service for clear boundaries and testability.</li>
  <li><strong>Cloud Workflows</strong> as the single place that defines order, status reporting, and error handling.</li>
  <li><strong>Tag-based production releases</strong> and diff-based deploy lists so only changed components are deployed.</li>
</ul>

<p>If you’re building a sync pipeline that must support both scheduled and ad-hoc triggers, this pattern; single event schema, single orchestration, ports and adapters per service; is a solid base to build on.</p>

<hr />
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>A Reproducible Pipeline for GO Function Prediction</title>
					
					<link>https://atabakkh.github.io/computationalbiology/2025/03/07/reproducible-go-pipeline.html</link>

					

					<pubDate>Fri, 07 Mar 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/computationalbiology/2025/03/07/reproducible-go-pipeline</guid>

					
						<category><![CDATA[ ComputationalBiology ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Goal:</strong> Make results <strong>re-runnable</strong> and <strong>comparable</strong> (CAFA-style).</p>

<h2 id="1-data--splits">1) Data &amp; splits</h2>
<ul>
  <li><strong>Sources:</strong> UniProt sequences, GOA annotations; optional PPI (STRING/BioGRID).</li>
  <li><strong>Time-based split:</strong> train &lt;= T0, test (T0, T1); prevents future leakage.</li>
  <li>Remove evidence types you disallow (e.g., IEA) from <strong>both</strong> sides.</li>
</ul>

<h2 id="2-label-processing">2) Label processing</h2>
<ul>
  <li>Propagate labels <strong>up the DAG</strong> (ancestor closure).</li>
  <li>Filter ultra-rare terms <em>or</em> keep and use <code class="language-plaintext highlighter-rouge">class_weight</code>.</li>
  <li>Store class mapping + DAG snapshot with a version tag.</li>
</ul>

<h2 id="3-features">3) Features</h2>
<ul>
  <li><strong>PLM embeddings</strong> (cached <code class="language-plaintext highlighter-rouge">.npy</code>): <code class="language-plaintext highlighter-rouge">N x D</code> float32; L2-normalize.</li>
  <li><strong>PPI:</strong> sparse CSR, symmetrically normalized adjacency <code class="language-plaintext highlighter-rouge">A</code>.</li>
  <li>Optional text TF-IDF for weak supervision.</li>
</ul>

<h2 id="4-models">4) Models</h2>
<ul>
  <li><strong>Baselines:</strong> kNN/BLAST label transfer; logistic regression (one-vs-rest).</li>
  <li><strong>Advanced:</strong> MLP (2-3 layers), <strong>GCN/GAT</strong> on PPI; late fusion of logits.</li>
  <li><strong>Calibration:</strong> isotonic or temperature scaling on validation.</li>
</ul>

<h2 id="5-metrics--plots">5) Metrics &amp; plots</h2>
<ul>
  <li><strong>Fmax</strong> (BP/MF/CC), <strong>micro/macro-auPRC</strong>, <strong>coverage</strong>.</li>
  <li><strong>Calibration:</strong> reliability curves, <strong>ECE</strong>.</li>
  <li>Save <code class="language-plaintext highlighter-rouge">results.json</code> and PR curves (PNG/SVG).</li>
</ul>

<h2 id="6-repro-artifacts">6) Repro artifacts</h2>
<ul>
  <li><code class="language-plaintext highlighter-rouge">environment.yml</code> / <code class="language-plaintext highlighter-rouge">requirements.txt</code> (pin PLM version).</li>
  <li><code class="language-plaintext highlighter-rouge">Makefile</code>: <code class="language-plaintext highlighter-rouge">prepare</code>, <code class="language-plaintext highlighter-rouge">embed</code>, <code class="language-plaintext highlighter-rouge">train</code>, <code class="language-plaintext highlighter-rouge">eval</code>, <code class="language-plaintext highlighter-rouge">plot</code>.</li>
  <li>Seed control: <code class="language-plaintext highlighter-rouge">PYTHONHASHSEED</code>, NumPy, torch.</li>
</ul>

<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">prepare</span><span class="o">:</span>
	python scripts/prepare.py <span class="nt">--cutoff</span> 2024-12-31
<span class="nl">embed</span><span class="o">:</span>
	python scripts/embed_plm.py <span class="nt">--model</span> esm2_t33 <span class="nt">--out</span> data/emb.npy
<span class="nl">train</span><span class="o">:</span>
	python scripts/train.py <span class="nt">--cfg</span> cfgs/logreg.yaml
<span class="nl">eval</span><span class="o">:</span>
	python scripts/eval.py <span class="nt">--cfg</span> cfgs/logreg.yaml <span class="nt">--out</span> results/logreg.json
<span class="nl">plot</span><span class="o">:</span>
	python scripts/plot_pr.py <span class="nt">--in</span> results/logreg.json <span class="nt">--out</span> figs/pr.png
</code></pre></div></div>

<h2 id="error-analysis-quick-wins">Error analysis (quick wins)</h2>

<ul>
  <li>Confusion by <strong>information content (IC)</strong> bins (rare vs common terms).</li>
  <li>Per-ontology breakdown (BP/MF/CC).</li>
  <li>Check <strong>hierarchy violations</strong> before/after closure step.</li>
</ul>

<p><strong>Deliverables:</strong> zipped <code class="language-plaintext highlighter-rouge">results/</code> + <code class="language-plaintext highlighter-rouge">cfgs/</code> + short <code class="language-plaintext highlighter-rouge">README</code> = review-ready.</p>

<hr />

<h2 id="detailed-implementation-guide">Detailed Implementation Guide</h2>

<h3 id="step-1-data-preparation-and-splits">Step 1: Data Preparation and Splits</h3>

<p><strong>Time-based split rationale:</strong></p>
<ul>
  <li>Prevents <strong>temporal leakage</strong>: test data must be from after training cutoff</li>
  <li>Mimics real-world deployment: train on past, predict on future</li>
  <li>Standard in CAFA (Critical Assessment of Function Annotation) evaluations</li>
</ul>

<p><strong>Example implementation:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>

<span class="k">def</span> <span class="nf">prepare_data_splits</span><span class="p">(</span>
    <span class="n">uniprot_file</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">goa_file</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">cutoff_date</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"2024-12-31"</span><span class="p">,</span>
    <span class="n">test_end_date</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"2025-03-31"</span>
<span class="p">):</span>
    <span class="s">"""
    Split data by time to prevent future leakage.
    
    Args:
        uniprot_file: UniProt sequences file
        goa_file: GOA annotations file
        cutoff_date: Training cutoff (YYYY-MM-DD)
        test_end_date: Test set end date
    
    Returns:
        train_df, test_df: DataFrames with sequences and annotations
    """</span>
    <span class="c1"># Load sequences
</span>    <span class="n">sequences</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">uniprot_file</span><span class="p">)</span>
    <span class="n">sequences</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">sequences</span><span class="p">[</span><span class="s">'date'</span><span class="p">])</span>
    
    <span class="c1"># Load annotations
</span>    <span class="n">annotations</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">goa_file</span><span class="p">)</span>
    <span class="n">annotations</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">annotations</span><span class="p">[</span><span class="s">'date'</span><span class="p">])</span>
    
    <span class="c1"># Time-based split
</span>    <span class="n">train_sequences</span> <span class="o">=</span> <span class="n">sequences</span><span class="p">[</span><span class="n">sequences</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="n">cutoff_date</span><span class="p">]</span>
    <span class="n">test_sequences</span> <span class="o">=</span> <span class="n">sequences</span><span class="p">[</span>
        <span class="p">(</span><span class="n">sequences</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">cutoff_date</span><span class="p">)</span> <span class="o">&amp;</span> 
        <span class="p">(</span><span class="n">sequences</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="n">test_end_date</span><span class="p">)</span>
    <span class="p">]</span>
    
    <span class="c1"># Remove IEA (Inferred from Electronic Annotation) if not allowed
</span>    <span class="n">train_annotations</span> <span class="o">=</span> <span class="n">annotations</span><span class="p">[</span>
        <span class="p">(</span><span class="n">annotations</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="n">cutoff_date</span><span class="p">)</span> <span class="o">&amp;</span>
        <span class="p">(</span><span class="n">annotations</span><span class="p">[</span><span class="s">'evidence'</span><span class="p">]</span> <span class="o">!=</span> <span class="s">'IEA'</span><span class="p">)</span>
    <span class="p">]</span>
    <span class="n">test_annotations</span> <span class="o">=</span> <span class="n">annotations</span><span class="p">[</span>
        <span class="p">(</span><span class="n">annotations</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">cutoff_date</span><span class="p">)</span> <span class="o">&amp;</span>
        <span class="p">(</span><span class="n">annotations</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="n">test_end_date</span><span class="p">)</span> <span class="o">&amp;</span>
        <span class="p">(</span><span class="n">annotations</span><span class="p">[</span><span class="s">'evidence'</span><span class="p">]</span> <span class="o">!=</span> <span class="s">'IEA'</span><span class="p">)</span>
    <span class="p">]</span>
    
    <span class="k">return</span> <span class="n">train_sequences</span><span class="p">,</span> <span class="n">test_sequences</span><span class="p">,</span> <span class="n">train_annotations</span><span class="p">,</span> <span class="n">test_annotations</span>
</code></pre></div></div>

<p><strong>PPI data integration (optional):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">load_ppi_network</span><span class="p">(</span><span class="n">string_file</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.7</span><span class="p">):</span>
    <span class="s">"""
    Load protein-protein interaction network from STRING.
    
    Args:
        string_file: STRING interactions file
        confidence_threshold: Minimum confidence score
    
    Returns:
        adjacency_matrix: scipy.sparse.csr_matrix
        protein_to_index: dict mapping protein ID to matrix index
    """</span>
    <span class="kn">import</span> <span class="nn">scipy.sparse</span> <span class="k">as</span> <span class="n">sp</span>
    
    <span class="n">ppi_df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">string_file</span><span class="p">,</span> <span class="n">sep</span><span class="o">=</span><span class="s">'</span><span class="se">\t</span><span class="s">'</span><span class="p">)</span>
    <span class="n">ppi_df</span> <span class="o">=</span> <span class="n">ppi_df</span><span class="p">[</span><span class="n">ppi_df</span><span class="p">[</span><span class="s">'combined_score'</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="n">confidence_threshold</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">]</span>
    
    <span class="c1"># Create protein index mapping
</span>    <span class="n">all_proteins</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">ppi_df</span><span class="p">[</span><span class="s">'protein1'</span><span class="p">].</span><span class="n">unique</span><span class="p">())</span> <span class="o">|</span> 
                          <span class="nb">set</span><span class="p">(</span><span class="n">ppi_df</span><span class="p">[</span><span class="s">'protein2'</span><span class="p">].</span><span class="n">unique</span><span class="p">()))</span>
    <span class="n">protein_to_index</span> <span class="o">=</span> <span class="p">{</span><span class="n">p</span><span class="p">:</span> <span class="n">i</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">p</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">all_proteins</span><span class="p">)}</span>
    
    <span class="c1"># Build sparse adjacency matrix
</span>    <span class="n">rows</span> <span class="o">=</span> <span class="p">[</span><span class="n">protein_to_index</span><span class="p">[</span><span class="n">p1</span><span class="p">]</span> <span class="k">for</span> <span class="n">p1</span> <span class="ow">in</span> <span class="n">ppi_df</span><span class="p">[</span><span class="s">'protein1'</span><span class="p">]]</span>
    <span class="n">cols</span> <span class="o">=</span> <span class="p">[</span><span class="n">protein_to_index</span><span class="p">[</span><span class="n">p2</span><span class="p">]</span> <span class="k">for</span> <span class="n">p2</span> <span class="ow">in</span> <span class="n">ppi_df</span><span class="p">[</span><span class="s">'protein2'</span><span class="p">]]</span>
    <span class="n">data</span> <span class="o">=</span> <span class="n">ppi_df</span><span class="p">[</span><span class="s">'combined_score'</span><span class="p">].</span><span class="n">values</span> <span class="o">/</span> <span class="mf">1000.0</span>  <span class="c1"># Normalize to [0,1]
</span>    
    <span class="n">n</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">all_proteins</span><span class="p">)</span>
    <span class="n">adjacency</span> <span class="o">=</span> <span class="n">sp</span><span class="p">.</span><span class="n">csr_matrix</span><span class="p">((</span><span class="n">data</span><span class="p">,</span> <span class="p">(</span><span class="n">rows</span><span class="p">,</span> <span class="n">cols</span><span class="p">)),</span> <span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">n</span><span class="p">))</span>
    
    <span class="c1"># Make symmetric (undirected graph)
</span>    <span class="n">adjacency</span> <span class="o">=</span> <span class="n">adjacency</span> <span class="o">+</span> <span class="n">adjacency</span><span class="p">.</span><span class="n">T</span>
    <span class="n">adjacency</span><span class="p">.</span><span class="n">data</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">minimum</span><span class="p">(</span><span class="n">adjacency</span><span class="p">.</span><span class="n">data</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span>  <span class="c1"># Cap at 1.0
</span>    
    <span class="k">return</span> <span class="n">adjacency</span><span class="p">,</span> <span class="n">protein_to_index</span>
</code></pre></div></div>

<hr />

<h3 id="step-2-label-processing-and-dag-propagation">Step 2: Label Processing and DAG Propagation</h3>

<p><strong>Ancestor closure implementation:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">networkx</span> <span class="k">as</span> <span class="n">nx</span>
<span class="kn">from</span> <span class="nn">goatools</span> <span class="kn">import</span> <span class="n">obo_parser</span>

<span class="k">def</span> <span class="nf">propagate_annotations</span><span class="p">(</span><span class="n">annotations</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">,</span> <span class="n">go_dag_file</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="s">"""
    Propagate GO annotations up the DAG (ancestor closure).
    
    Args:
        annotations: DataFrame with columns [protein_id, go_term]
        go_dag_file: Path to GO OBO file
    
    Returns:
        propagated_annotations: DataFrame with ancestor-closed labels
    """</span>
    <span class="c1"># Load GO DAG
</span>    <span class="n">go_dag</span> <span class="o">=</span> <span class="n">obo_parser</span><span class="p">.</span><span class="n">GODag</span><span class="p">(</span><span class="n">go_dag_file</span><span class="p">)</span>
    
    <span class="c1"># Build graph for efficient traversal
</span>    <span class="n">G</span> <span class="o">=</span> <span class="n">nx</span><span class="p">.</span><span class="n">DiGraph</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">term_id</span><span class="p">,</span> <span class="n">term</span> <span class="ow">in</span> <span class="n">go_dag</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="k">for</span> <span class="n">parent_id</span> <span class="ow">in</span> <span class="n">term</span><span class="p">.</span><span class="n">parents</span><span class="p">:</span>
            <span class="n">G</span><span class="p">.</span><span class="n">add_edge</span><span class="p">(</span><span class="n">term_id</span><span class="p">,</span> <span class="n">parent_id</span><span class="p">)</span>
    
    <span class="c1"># Propagate annotations
</span>    <span class="n">propagated</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">annotations</span><span class="p">.</span><span class="n">iterrows</span><span class="p">():</span>
        <span class="n">protein_id</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="s">'protein_id'</span><span class="p">]</span>
        <span class="n">go_term</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="s">'go_term'</span><span class="p">]</span>
        
        <span class="c1"># Add original annotation
</span>        <span class="n">propagated</span><span class="p">.</span><span class="n">add</span><span class="p">((</span><span class="n">protein_id</span><span class="p">,</span> <span class="n">go_term</span><span class="p">))</span>
        
        <span class="c1"># Add all ancestors
</span>        <span class="k">if</span> <span class="n">go_term</span> <span class="ow">in</span> <span class="n">go_dag</span><span class="p">:</span>
            <span class="n">ancestors</span> <span class="o">=</span> <span class="n">nx</span><span class="p">.</span><span class="n">ancestors</span><span class="p">(</span><span class="n">G</span><span class="p">,</span> <span class="n">go_term</span><span class="p">)</span>
            <span class="k">for</span> <span class="n">ancestor</span> <span class="ow">in</span> <span class="n">ancestors</span><span class="p">:</span>
                <span class="n">propagated</span><span class="p">.</span><span class="n">add</span><span class="p">((</span><span class="n">protein_id</span><span class="p">,</span> <span class="n">ancestor</span><span class="p">))</span>
    
    <span class="k">return</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="n">propagated</span><span class="p">),</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s">'protein_id'</span><span class="p">,</span> <span class="s">'go_term'</span><span class="p">])</span>
</code></pre></div></div>

<p><strong>Filtering rare terms:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">filter_rare_terms</span><span class="p">(</span><span class="n">annotations</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">,</span> <span class="n">min_count</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">10</span><span class="p">):</span>
    <span class="s">"""
    Filter out GO terms with too few annotations.
    
    Args:
        annotations: DataFrame with [protein_id, go_term]
        min_count: Minimum number of proteins per term
    
    Returns:
        filtered_annotations: DataFrame with rare terms removed
    """</span>
    <span class="n">term_counts</span> <span class="o">=</span> <span class="n">annotations</span><span class="p">[</span><span class="s">'go_term'</span><span class="p">].</span><span class="n">value_counts</span><span class="p">()</span>
    <span class="n">common_terms</span> <span class="o">=</span> <span class="n">term_counts</span><span class="p">[</span><span class="n">term_counts</span> <span class="o">&gt;=</span> <span class="n">min_count</span><span class="p">].</span><span class="n">index</span>
    
    <span class="k">return</span> <span class="n">annotations</span><span class="p">[</span><span class="n">annotations</span><span class="p">[</span><span class="s">'go_term'</span><span class="p">].</span><span class="n">isin</span><span class="p">(</span><span class="n">common_terms</span><span class="p">)]</span>
</code></pre></div></div>

<hr />

<h3 id="step-3-feature-engineering">Step 3: Feature Engineering</h3>

<p><strong>PLM embeddings (cached):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoModel</span><span class="p">,</span> <span class="n">AutoTokenizer</span>
<span class="kn">import</span> <span class="nn">torch</span>

<span class="k">def</span> <span class="nf">generate_plm_embeddings</span><span class="p">(</span>
    <span class="n">sequences</span><span class="p">:</span> <span class="nb">list</span><span class="p">,</span>
    <span class="n">model_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"facebook/esm2_t33_650M_UR50D"</span><span class="p">,</span>
    <span class="n">batch_size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">32</span><span class="p">,</span>
    <span class="n">cache_file</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"embeddings.npy"</span>
<span class="p">):</span>
    <span class="s">"""
    Generate protein language model embeddings.
    
    Args:
        sequences: List of protein sequences (strings)
        model_name: HuggingFace model identifier
        batch_size: Batch size for inference
        cache_file: Path to save embeddings
    
    Returns:
        embeddings: numpy array [N, D] of L2-normalized embeddings
    """</span>
    <span class="k">if</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">exists</span><span class="p">(</span><span class="n">cache_file</span><span class="p">):</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Loading cached embeddings from </span><span class="si">{</span><span class="n">cache_file</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="n">cache_file</span><span class="p">)</span>
    
    <span class="n">device</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">device</span><span class="p">(</span><span class="s">"cuda"</span> <span class="k">if</span> <span class="n">torch</span><span class="p">.</span><span class="n">cuda</span><span class="p">.</span><span class="n">is_available</span><span class="p">()</span> <span class="k">else</span> <span class="s">"cpu"</span><span class="p">)</span>
    <span class="n">tokenizer</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>
    <span class="n">model</span> <span class="o">=</span> <span class="n">AutoModel</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_name</span><span class="p">).</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
    <span class="n">model</span><span class="p">.</span><span class="nb">eval</span><span class="p">()</span>
    
    <span class="n">embeddings</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="n">no_grad</span><span class="p">():</span>
        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">sequences</span><span class="p">),</span> <span class="n">batch_size</span><span class="p">):</span>
            <span class="n">batch</span> <span class="o">=</span> <span class="n">sequences</span><span class="p">[</span><span class="n">i</span><span class="p">:</span><span class="n">i</span><span class="o">+</span><span class="n">batch_size</span><span class="p">]</span>
            
            <span class="c1"># Tokenize
</span>            <span class="n">encoded</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">(</span>
                <span class="n">batch</span><span class="p">,</span>
                <span class="n">padding</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
                <span class="n">truncation</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
                <span class="n">max_length</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span>
                <span class="n">return_tensors</span><span class="o">=</span><span class="s">"pt"</span>
            <span class="p">).</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
            
            <span class="c1"># Get embeddings (mean pooling over sequence)
</span>            <span class="n">outputs</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="o">**</span><span class="n">encoded</span><span class="p">)</span>
            <span class="n">batch_embeddings</span> <span class="o">=</span> <span class="n">outputs</span><span class="p">.</span><span class="n">last_hidden_state</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
            
            <span class="c1"># L2 normalize
</span>            <span class="n">batch_embeddings</span> <span class="o">=</span> <span class="n">batch_embeddings</span> <span class="o">/</span> <span class="n">batch_embeddings</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
            
            <span class="n">embeddings</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">batch_embeddings</span><span class="p">.</span><span class="n">cpu</span><span class="p">().</span><span class="n">numpy</span><span class="p">())</span>
    
    <span class="n">embeddings</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">vstack</span><span class="p">(</span><span class="n">embeddings</span><span class="p">)</span>
    
    <span class="c1"># Save cache
</span>    <span class="n">np</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">cache_file</span><span class="p">,</span> <span class="n">embeddings</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Saved embeddings to </span><span class="si">{</span><span class="n">cache_file</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">embeddings</span>
</code></pre></div></div>

<p><strong>PPI network normalization:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">normalize_adjacency</span><span class="p">(</span><span class="n">adjacency</span><span class="p">:</span> <span class="n">sp</span><span class="p">.</span><span class="n">csr_matrix</span><span class="p">):</span>
    <span class="s">"""
    Symmetrically normalize adjacency matrix for GCN.
    
    Returns:
        normalized_adj: D^(-1/2) A D^(-1/2)
    """</span>
    <span class="c1"># Add self-loops
</span>    <span class="n">adjacency</span> <span class="o">=</span> <span class="n">adjacency</span> <span class="o">+</span> <span class="n">sp</span><span class="p">.</span><span class="n">eye</span><span class="p">(</span><span class="n">adjacency</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
    
    <span class="c1"># Compute degree matrix
</span>    <span class="n">degree</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">adjacency</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)).</span><span class="n">flatten</span><span class="p">()</span>
    <span class="n">degree_sqrt_inv</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">degree</span><span class="p">)</span>
    <span class="n">degree_sqrt_inv</span><span class="p">[</span><span class="n">np</span><span class="p">.</span><span class="n">isinf</span><span class="p">(</span><span class="n">degree_sqrt_inv</span><span class="p">)]</span> <span class="o">=</span> <span class="mf">0.0</span>
    
    <span class="c1"># Normalize: D^(-1/2) A D^(-1/2)
</span>    <span class="n">D_inv_sqrt</span> <span class="o">=</span> <span class="n">sp</span><span class="p">.</span><span class="n">diags</span><span class="p">(</span><span class="n">degree_sqrt_inv</span><span class="p">)</span>
    <span class="n">normalized</span> <span class="o">=</span> <span class="n">D_inv_sqrt</span> <span class="o">@</span> <span class="n">adjacency</span> <span class="o">@</span> <span class="n">D_inv_sqrt</span>
    
    <span class="k">return</span> <span class="n">normalized</span>
</code></pre></div></div>

<hr />

<h3 id="step-4-model-training">Step 4: Model Training</h3>

<p><strong>Baseline: Logistic Regression</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sklearn.linear_model</span> <span class="kn">import</span> <span class="n">LogisticRegression</span>
<span class="kn">from</span> <span class="nn">sklearn.multiclass</span> <span class="kn">import</span> <span class="n">OneVsRestClassifier</span>
<span class="kn">from</span> <span class="nn">sklearn.preprocessing</span> <span class="kn">import</span> <span class="n">StandardScaler</span>

<span class="k">def</span> <span class="nf">train_logistic_baseline</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">X_val</span><span class="p">,</span> <span class="n">y_val</span><span class="p">):</span>
    <span class="s">"""
    Train one-vs-rest logistic regression baseline.
    
    Returns:
        model: Trained classifier
        metrics: Dictionary of validation metrics
    """</span>
    <span class="c1"># Scale features
</span>    <span class="n">scaler</span> <span class="o">=</span> <span class="n">StandardScaler</span><span class="p">()</span>
    <span class="n">X_train_scaled</span> <span class="o">=</span> <span class="n">scaler</span><span class="p">.</span><span class="n">fit_transform</span><span class="p">(</span><span class="n">X_train</span><span class="p">)</span>
    <span class="n">X_val_scaled</span> <span class="o">=</span> <span class="n">scaler</span><span class="p">.</span><span class="n">transform</span><span class="p">(</span><span class="n">X_val</span><span class="p">)</span>
    
    <span class="c1"># Train
</span>    <span class="n">clf</span> <span class="o">=</span> <span class="n">OneVsRestClassifier</span><span class="p">(</span>
        <span class="n">LogisticRegression</span><span class="p">(</span>
            <span class="n">max_iter</span><span class="o">=</span><span class="mi">4000</span><span class="p">,</span>
            <span class="n">class_weight</span><span class="o">=</span><span class="s">'balanced'</span><span class="p">,</span>
            <span class="n">solver</span><span class="o">=</span><span class="s">'lbfgs'</span><span class="p">,</span>
            <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span>
        <span class="p">),</span>
        <span class="n">n_jobs</span><span class="o">=-</span><span class="mi">1</span>
    <span class="p">)</span>
    <span class="n">clf</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train_scaled</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
    
    <span class="c1"># Evaluate
</span>    <span class="n">y_pred_proba</span> <span class="o">=</span> <span class="n">clf</span><span class="p">.</span><span class="n">predict_proba</span><span class="p">(</span><span class="n">X_val_scaled</span><span class="p">)</span>
    <span class="n">metrics</span> <span class="o">=</span> <span class="n">evaluate_predictions</span><span class="p">(</span><span class="n">y_val</span><span class="p">,</span> <span class="n">y_pred_proba</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">clf</span><span class="p">,</span> <span class="n">scaler</span><span class="p">,</span> <span class="n">metrics</span>
</code></pre></div></div>

<p><strong>Advanced: GCN for PPI</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch.nn</span> <span class="k">as</span> <span class="n">nn</span>
<span class="kn">import</span> <span class="nn">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>

<span class="k">class</span> <span class="nc">GCNLayer</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">linear</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="p">,</span> <span class="n">out_features</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">adj</span><span class="p">):</span>
        <span class="c1"># GCN: H' = D^(-1/2) A D^(-1/2) H W
</span>        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">linear</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">sparse</span><span class="p">.</span><span class="n">mm</span><span class="p">(</span><span class="n">adj</span><span class="p">,</span> <span class="n">x</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">F</span><span class="p">.</span><span class="n">relu</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">GCNClassifier</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">,</span> <span class="n">num_classes</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">gcn1</span> <span class="o">=</span> <span class="n">GCNLayer</span><span class="p">(</span><span class="n">input_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">gcn2</span> <span class="o">=</span> <span class="n">GCNLayer</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">classifier</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">hidden_dim</span><span class="p">,</span> <span class="n">num_classes</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">adj</span><span class="p">):</span>
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">gcn1</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">adj</span><span class="p">)</span>
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">gcn2</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">adj</span><span class="p">)</span>
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">classifier</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">torch</span><span class="p">.</span><span class="n">sigmoid</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>  <span class="c1"># Multi-label classification
</span></code></pre></div></div>

<hr />

<h3 id="step-5-evaluation-metrics">Step 5: Evaluation Metrics</h3>

<p><strong>CAFA-style evaluation:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sklearn.metrics</span> <span class="kn">import</span> <span class="n">precision_recall_curve</span><span class="p">,</span> <span class="n">auc</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="k">def</span> <span class="nf">compute_fmax</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">y_pred_proba</span><span class="p">,</span> <span class="n">thresholds</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="s">"""
    Compute Fmax (maximum F1 over thresholds).
    
    Returns:
        fmax: Maximum F1 score
        best_threshold: Threshold achieving Fmax
    """</span>
    <span class="k">if</span> <span class="n">thresholds</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">thresholds</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="mf">0.01</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.01</span><span class="p">)</span>
    
    <span class="n">fmax</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="n">best_threshold</span> <span class="o">=</span> <span class="mf">0.0</span>
    
    <span class="k">for</span> <span class="n">threshold</span> <span class="ow">in</span> <span class="n">thresholds</span><span class="p">:</span>
        <span class="n">y_pred</span> <span class="o">=</span> <span class="p">(</span><span class="n">y_pred_proba</span> <span class="o">&gt;=</span> <span class="n">threshold</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
        
        <span class="c1"># Per-class precision/recall
</span>        <span class="n">tp</span> <span class="o">=</span> <span class="p">(</span><span class="n">y_true</span> <span class="o">*</span> <span class="n">y_pred</span><span class="p">).</span><span class="nb">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
        <span class="n">fp</span> <span class="o">=</span> <span class="p">((</span><span class="mi">1</span> <span class="o">-</span> <span class="n">y_true</span><span class="p">)</span> <span class="o">*</span> <span class="n">y_pred</span><span class="p">).</span><span class="nb">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
        <span class="n">fn</span> <span class="o">=</span> <span class="p">(</span><span class="n">y_true</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">y_pred</span><span class="p">)).</span><span class="nb">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
        
        <span class="n">precision</span> <span class="o">=</span> <span class="n">tp</span> <span class="o">/</span> <span class="p">(</span><span class="n">tp</span> <span class="o">+</span> <span class="n">fp</span> <span class="o">+</span> <span class="mf">1e-10</span><span class="p">)</span>
        <span class="n">recall</span> <span class="o">=</span> <span class="n">tp</span> <span class="o">/</span> <span class="p">(</span><span class="n">tp</span> <span class="o">+</span> <span class="n">fn</span> <span class="o">+</span> <span class="mf">1e-10</span><span class="p">)</span>
        
        <span class="c1"># Macro-averaged F1
</span>        <span class="n">f1</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">precision</span> <span class="o">*</span> <span class="n">recall</span> <span class="o">/</span> <span class="p">(</span><span class="n">precision</span> <span class="o">+</span> <span class="n">recall</span> <span class="o">+</span> <span class="mf">1e-10</span><span class="p">)</span>
        <span class="n">f1_macro</span> <span class="o">=</span> <span class="n">f1</span><span class="p">.</span><span class="n">mean</span><span class="p">()</span>
        
        <span class="k">if</span> <span class="n">f1_macro</span> <span class="o">&gt;</span> <span class="n">fmax</span><span class="p">:</span>
            <span class="n">fmax</span> <span class="o">=</span> <span class="n">f1_macro</span>
            <span class="n">best_threshold</span> <span class="o">=</span> <span class="n">threshold</span>
    
    <span class="k">return</span> <span class="n">fmax</span><span class="p">,</span> <span class="n">best_threshold</span>

<span class="k">def</span> <span class="nf">compute_auprc</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">y_pred_proba</span><span class="p">):</span>
    <span class="s">"""
    Compute micro and macro averaged AUPRC.
    """</span>
    <span class="c1"># Micro-averaged (flatten all predictions)
</span>    <span class="n">y_true_flat</span> <span class="o">=</span> <span class="n">y_true</span><span class="p">.</span><span class="n">flatten</span><span class="p">()</span>
    <span class="n">y_pred_flat</span> <span class="o">=</span> <span class="n">y_pred_proba</span><span class="p">.</span><span class="n">flatten</span><span class="p">()</span>
    
    <span class="n">precision</span><span class="p">,</span> <span class="n">recall</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">precision_recall_curve</span><span class="p">(</span><span class="n">y_true_flat</span><span class="p">,</span> <span class="n">y_pred_flat</span><span class="p">)</span>
    <span class="n">micro_auprc</span> <span class="o">=</span> <span class="n">auc</span><span class="p">(</span><span class="n">recall</span><span class="p">,</span> <span class="n">precision</span><span class="p">)</span>
    
    <span class="c1"># Macro-averaged (per-class, then average)
</span>    <span class="n">macro_auprcs</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">y_true</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]):</span>
        <span class="k">if</span> <span class="n">y_true</span><span class="p">[:,</span> <span class="n">i</span><span class="p">].</span><span class="nb">sum</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>  <span class="c1"># Skip classes with no positives
</span>            <span class="n">p</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">precision_recall_curve</span><span class="p">(</span><span class="n">y_true</span><span class="p">[:,</span> <span class="n">i</span><span class="p">],</span> <span class="n">y_pred_proba</span><span class="p">[:,</span> <span class="n">i</span><span class="p">])</span>
            <span class="n">macro_auprcs</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">auc</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">p</span><span class="p">))</span>
    
    <span class="n">macro_auprc</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">macro_auprcs</span><span class="p">)</span> <span class="k">if</span> <span class="n">macro_auprcs</span> <span class="k">else</span> <span class="mf">0.0</span>
    
    <span class="k">return</span> <span class="n">micro_auprc</span><span class="p">,</span> <span class="n">macro_auprc</span>
</code></pre></div></div>

<hr />

<h3 id="step-6-reproducibility-artifacts">Step 6: Reproducibility Artifacts</h3>

<p><strong>Complete Makefile:</strong></p>
<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">.PHONY</span><span class="o">:</span> <span class="nf">prepare embed train eval plot clean</span>

<span class="c"># Configuration
</span><span class="nv">CUTOFF_DATE</span> <span class="o">=</span> 2024-12-31
<span class="nv">MODEL</span> <span class="o">=</span> esm2_t33_650M_UR50D
<span class="nv">CONFIG</span> <span class="o">=</span> cfgs/logreg.yaml

<span class="c"># Data preparation
</span><span class="nl">prepare</span><span class="o">:</span>
	python scripts/prepare.py <span class="se">\</span>
		<span class="nt">--uniprot</span> data/uniprot.fasta <span class="se">\</span>
		<span class="nt">--goa</span> data/goa.tsv <span class="se">\</span>
		<span class="nt">--cutoff</span> <span class="nv">$(CUTOFF_DATE)</span> <span class="se">\</span>
		<span class="nt">--out</span> data/splits/

<span class="c"># Generate embeddings
</span><span class="nl">embed</span><span class="o">:</span>
	python scripts/embed_plm.py <span class="se">\</span>
		<span class="nt">--sequences</span> data/splits/train_sequences.fasta <span class="se">\</span>
		<span class="nt">--model</span> <span class="nv">$(MODEL)</span> <span class="se">\</span>
		<span class="nt">--batch-size</span> 32 <span class="se">\</span>
		<span class="nt">--out</span> data/embeddings/train_emb.npy

<span class="c"># Train model
</span><span class="nl">train</span><span class="o">:</span>
	python scripts/train.py <span class="se">\</span>
		<span class="nt">--cfg</span> <span class="nv">$(CONFIG)</span> <span class="se">\</span>
		<span class="nt">--train-emb</span> data/embeddings/train_emb.npy <span class="se">\</span>
		<span class="nt">--train-labels</span> data/splits/train_labels.npy <span class="se">\</span>
		<span class="nt">--val-emb</span> data/embeddings/val_emb.npy <span class="se">\</span>
		<span class="nt">--val-labels</span> data/splits/val_labels.npy <span class="se">\</span>
		<span class="nt">--out</span> models/logreg.pkl

<span class="c"># Evaluate
</span><span class="nl">eval</span><span class="o">:</span>
	python scripts/eval.py <span class="se">\</span>
		<span class="nt">--model</span> models/logreg.pkl <span class="se">\</span>
		<span class="nt">--test-emb</span> data/embeddings/test_emb.npy <span class="se">\</span>
		<span class="nt">--test-labels</span> data/splits/test_labels.npy <span class="se">\</span>
		<span class="nt">--out</span> results/logreg.json

<span class="c"># Plot results
</span><span class="nl">plot</span><span class="o">:</span>
	python scripts/plot_pr.py <span class="se">\</span>
		<span class="nt">--in</span> results/logreg.json <span class="se">\</span>
		<span class="nt">--out</span> figs/pr_curve.png

<span class="c"># Clean intermediate files
</span><span class="nl">clean</span><span class="o">:</span>
	<span class="nb">rm</span> <span class="nt">-rf</span> data/embeddings/<span class="k">*</span>.npy
	<span class="nb">rm</span> <span class="nt">-rf</span> models/<span class="k">*</span>.pkl
	<span class="nb">rm</span> <span class="nt">-rf</span> results/<span class="k">*</span>.json
</code></pre></div></div>

<p><strong>Environment file:</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># environment.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">go-prediction</span>
<span class="na">channels</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">conda-forge</span>
  <span class="pi">-</span> <span class="s">pytorch</span>
<span class="na">dependencies</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">python=3.10</span>
  <span class="pi">-</span> <span class="s">numpy=1.24.3</span>
  <span class="pi">-</span> <span class="s">pandas=2.0.3</span>
  <span class="pi">-</span> <span class="s">scikit-learn=1.3.0</span>
  <span class="pi">-</span> <span class="s">pytorch=2.0.1</span>
  <span class="pi">-</span> <span class="s">transformers=4.33.2</span>
  <span class="pi">-</span> <span class="s">networkx=3.1</span>
  <span class="pi">-</span> <span class="s">matplotlib=3.7.2</span>
  <span class="pi">-</span> <span class="s">seaborn=0.12.2</span>
  <span class="pi">-</span> <span class="s">pip</span>
  <span class="pi">-</span> <span class="na">pip</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">goatools==1.3.0</span>
    <span class="pi">-</span> <span class="s">biopython==1.81</span>
</code></pre></div></div>

<p><strong>Seed control:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">random</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">torch</span>

<span class="k">def</span> <span class="nf">set_seeds</span><span class="p">(</span><span class="n">seed</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">42</span><span class="p">):</span>
    <span class="s">"""Set all random seeds for reproducibility."""</span>
    <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">[</span><span class="s">'PYTHONHASHSEED'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
    <span class="n">random</span><span class="p">.</span><span class="n">seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
    <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
    <span class="n">torch</span><span class="p">.</span><span class="n">manual_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
    <span class="n">torch</span><span class="p">.</span><span class="n">cuda</span><span class="p">.</span><span class="n">manual_seed_all</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
    <span class="n">torch</span><span class="p">.</span><span class="n">backends</span><span class="p">.</span><span class="n">cudnn</span><span class="p">.</span><span class="n">deterministic</span> <span class="o">=</span> <span class="bp">True</span>
    <span class="n">torch</span><span class="p">.</span><span class="n">backends</span><span class="p">.</span><span class="n">cudnn</span><span class="p">.</span><span class="n">benchmark</span> <span class="o">=</span> <span class="bp">False</span>
</code></pre></div></div>

<hr />

<h2 id="error-analysis-and-debugging">Error Analysis and Debugging</h2>

<p><strong>Confusion by information content:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">analyze_by_ic</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">,</span> <span class="n">go_dag</span><span class="p">,</span> <span class="n">ic_scores</span><span class="p">):</span>
    <span class="s">"""
    Analyze errors by information content (IC) bins.
    
    IC measures term specificity: high IC = specific, low IC = general.
    """</span>
    <span class="c1"># Bin terms by IC
</span>    <span class="n">ic_bins</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">'rare'</span><span class="p">:</span> <span class="n">ic_scores</span> <span class="o">&lt;</span> <span class="mf">2.0</span><span class="p">,</span>
        <span class="s">'medium'</span><span class="p">:</span> <span class="p">(</span><span class="n">ic_scores</span> <span class="o">&gt;=</span> <span class="mf">2.0</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span><span class="n">ic_scores</span> <span class="o">&lt;</span> <span class="mf">4.0</span><span class="p">),</span>
        <span class="s">'common'</span><span class="p">:</span> <span class="n">ic_scores</span> <span class="o">&gt;=</span> <span class="mf">4.0</span>
    <span class="p">}</span>
    
    <span class="k">for</span> <span class="n">bin_name</span><span class="p">,</span> <span class="n">mask</span> <span class="ow">in</span> <span class="n">ic_bins</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="n">y_true_bin</span> <span class="o">=</span> <span class="n">y_true</span><span class="p">[:,</span> <span class="n">mask</span><span class="p">]</span>
        <span class="n">y_pred_bin</span> <span class="o">=</span> <span class="n">y_pred</span><span class="p">[:,</span> <span class="n">mask</span><span class="p">]</span>
        
        <span class="n">fmax</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">compute_fmax</span><span class="p">(</span><span class="n">y_true_bin</span><span class="p">,</span> <span class="n">y_pred_bin</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">bin_name</span><span class="si">}</span><span class="s"> terms (IC </span><span class="si">{</span><span class="n">ic_bins</span><span class="p">[</span><span class="n">bin_name</span><span class="p">]</span><span class="si">}</span><span class="s">): Fmax = </span><span class="si">{</span><span class="n">fmax</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Per-ontology breakdown:</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">analyze_by_ontology</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">,</span> <span class="n">go_terms</span><span class="p">):</span>
    <span class="s">"""
    Analyze performance separately for BP, MF, CC.
    """</span>
    <span class="n">ontologies</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">'BP'</span><span class="p">:</span> <span class="p">[</span><span class="n">t</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">go_terms</span> <span class="k">if</span> <span class="n">t</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="s">'GO:'</span><span class="p">)</span> <span class="ow">and</span> <span class="n">go_dag</span><span class="p">[</span><span class="n">t</span><span class="p">].</span><span class="n">namespace</span> <span class="o">==</span> <span class="s">'biological_process'</span><span class="p">],</span>
        <span class="s">'MF'</span><span class="p">:</span> <span class="p">[</span><span class="n">t</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">go_terms</span> <span class="k">if</span> <span class="n">t</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="s">'GO:'</span><span class="p">)</span> <span class="ow">and</span> <span class="n">go_dag</span><span class="p">[</span><span class="n">t</span><span class="p">].</span><span class="n">namespace</span> <span class="o">==</span> <span class="s">'molecular_function'</span><span class="p">],</span>
        <span class="s">'CC'</span><span class="p">:</span> <span class="p">[</span><span class="n">t</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">go_terms</span> <span class="k">if</span> <span class="n">t</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="s">'GO:'</span><span class="p">)</span> <span class="ow">and</span> <span class="n">go_dag</span><span class="p">[</span><span class="n">t</span><span class="p">].</span><span class="n">namespace</span> <span class="o">==</span> <span class="s">'cellular_component'</span><span class="p">]</span>
    <span class="p">}</span>
    
    <span class="k">for</span> <span class="n">ont</span><span class="p">,</span> <span class="n">terms</span> <span class="ow">in</span> <span class="n">ontologies</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="n">term_indices</span> <span class="o">=</span> <span class="p">[</span><span class="n">go_terms</span><span class="p">.</span><span class="n">index</span><span class="p">(</span><span class="n">t</span><span class="p">)</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">terms</span> <span class="k">if</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">go_terms</span><span class="p">]</span>
        <span class="k">if</span> <span class="n">term_indices</span><span class="p">:</span>
            <span class="n">y_true_ont</span> <span class="o">=</span> <span class="n">y_true</span><span class="p">[:,</span> <span class="n">term_indices</span><span class="p">]</span>
            <span class="n">y_pred_ont</span> <span class="o">=</span> <span class="n">y_pred</span><span class="p">[:,</span> <span class="n">term_indices</span><span class="p">]</span>
            <span class="n">fmax</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">compute_fmax</span><span class="p">(</span><span class="n">y_true_ont</span><span class="p">,</span> <span class="n">y_pred_ont</span><span class="p">)</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">ont</span><span class="si">}</span><span class="s">: Fmax = </span><span class="si">{</span><span class="n">fmax</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="best-practices-summary">Best Practices Summary</h2>

<ol>
  <li><strong>Time-based splits</strong>: Always use temporal splits to prevent leakage</li>
  <li><strong>Ancestor closure</strong>: Propagate labels up the DAG for consistency</li>
  <li><strong>Cache embeddings</strong>: Save PLM embeddings to avoid recomputation</li>
  <li><strong>Version control</strong>: Tag data, code, and model versions</li>
  <li><strong>Documentation</strong>: Include README with setup, usage, and results</li>
  <li><strong>Reproducibility</strong>: Pin all dependencies, set random seeds</li>
  <li><strong>Error analysis</strong>: Analyze failures by IC, ontology, hierarchy violations</li>
</ol>

<p><strong>Deliverables checklist:</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">results/</code> directory with JSON metrics and plots</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">cfgs/</code> directory with all configuration files</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">README.md</code> with setup instructions and results summary</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">environment.yml</code> or <code class="language-plaintext highlighter-rouge">requirements.txt</code> with pinned versions</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">Makefile</code> for reproducible execution</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Seed control in all scripts</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Data version tags (cutoff dates, GO version)</li>
</ul>

<p><strong>Deliverables:</strong> Zipped <code class="language-plaintext highlighter-rouge">results/</code> + <code class="language-plaintext highlighter-rouge">cfgs/</code> + short <code class="language-plaintext highlighter-rouge">README</code> = review-ready.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Cost-to-Serve in 30 Minutes: A Practical Quickstart</title>
					
					<link>https://atabakkh.github.io/finops/2025/02/07/cost-to-serve-quickstart.html</link>

					

					<pubDate>Fri, 07 Feb 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/finops/2025/02/07/cost-to-serve-quickstart</guid>

					
						<category><![CDATA[ FinOps ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Takeaway:</strong> You don’t need perfection-just a <strong>directionally correct</strong> cost per request/job.</p>

<p>Cost-to-serve (CTS) is the foundation of FinOps: understanding what each request, job, or user action actually costs. This guide shows you how to build a working CTS model in 30 minutes using BigQuery and your existing billing data.</p>

<hr />

<h2 id="why-cost-to-serve-matters">Why Cost-to-Serve Matters</h2>

<p>Traditional cloud cost reporting shows <strong>where</strong> you spend money (services, regions, projects), but not <strong>why</strong> (which features, endpoints, customers). Cost-to-serve bridges that gap by connecting costs to business metrics.</p>

<p><strong>Business value:</strong></p>
<ul>
  <li><strong>Prioritize engineering work</strong> based on cost impact</li>
  <li><strong>Price products</strong> based on actual infrastructure costs</li>
  <li><strong>Identify waste</strong> (high-cost, low-value endpoints)</li>
  <li><strong>Forecast costs</strong> based on traffic growth</li>
  <li><strong>Optimize ROI</strong> by focusing on high-value, low-cost features</li>
</ul>

<p><strong>Example insight:</strong></p>
<blockquote>
  <p>“Our <code class="language-plaintext highlighter-rouge">/api/v1/recommendations</code> endpoint costs €0.12 per request but only generates €0.08 in revenue. We should optimize it or deprecate it.”</p>
</blockquote>

<hr />

<h2 id="prerequisites">Prerequisites</h2>

<p>You need three things:</p>

<ol>
  <li><strong>Cloud billing export</strong> (already enabled in most GCP projects)
    <ul>
      <li>BigQuery billing export table</li>
      <li>Or AWS Cost and Usage Report (CUR) in S3</li>
    </ul>
  </li>
  <li><strong>Traffic/request metrics</strong> (from your monitoring)
    <ul>
      <li>Request counts per service/endpoint</li>
      <li>Job counts for batch workloads</li>
      <li>User action counts for product metrics</li>
    </ul>
  </li>
  <li><strong>BigQuery access</strong> (or equivalent SQL engine)
    <ul>
      <li>Read access to billing export</li>
      <li>Write access to create views/tables</li>
    </ul>
  </li>
</ol>

<hr />

<h2 id="step-1-set-up-billing-export-if-not-already-done">Step 1: Set Up Billing Export (If Not Already Done)</h2>

<p><strong>For GCP:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Enable billing export to BigQuery</span>
gcloud billing budgets create <span class="se">\</span>
  <span class="nt">--billing-account</span><span class="o">=</span>YOUR_BILLING_ACCOUNT <span class="se">\</span>
  <span class="nt">--display-name</span><span class="o">=</span><span class="s2">"Billing Export"</span> <span class="se">\</span>
  <span class="nt">--budget-amount</span><span class="o">=</span>1000USD <span class="se">\</span>
  <span class="nt">--threshold-rule</span><span class="o">=</span><span class="nv">percent</span><span class="o">=</span>50 <span class="se">\</span>
  <span class="nt">--threshold-rule</span><span class="o">=</span><span class="nv">percent</span><span class="o">=</span>90

<span class="c"># The export table is automatically created at:</span>
<span class="c"># `gcp_billing_export_v1_XXXXXX.gcp_billing_export_v1_XXXXXX`</span>
</code></pre></div></div>

<p><strong>For AWS:</strong></p>
<ul>
  <li>Enable Cost and Usage Report (CUR) in AWS Cost Management</li>
  <li>Export to S3 bucket</li>
  <li>Use AWS Glue/Athena or import to BigQuery</li>
</ul>

<hr />

<h2 id="step-2-collect-traffic-metrics">Step 2: Collect Traffic Metrics</h2>

<p>You need request/job counts per service. Here are common sources:</p>

<h3 id="option-a-application-metrics-prometheuscloud-monitoring">Option A: Application Metrics (Prometheus/Cloud Monitoring)</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Example: Extract request counts from Cloud Monitoring</span>
<span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="n">traffic_metrics</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="k">EXTRACT</span><span class="p">(</span><span class="nb">DATE</span> <span class="k">FROM</span> <span class="nb">timestamp</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">date</span><span class="p">,</span>
  <span class="n">JSON_EXTRACT_SCALAR</span><span class="p">(</span><span class="n">resource</span><span class="p">.</span><span class="n">labels</span><span class="p">,</span> <span class="s1">'$.service_name'</span><span class="p">)</span> <span class="k">AS</span> <span class="n">service</span><span class="p">,</span>
  <span class="n">JSON_EXTRACT_SCALAR</span><span class="p">(</span><span class="n">resource</span><span class="p">.</span><span class="n">labels</span><span class="p">,</span> <span class="s1">'$.method'</span><span class="p">)</span> <span class="k">AS</span> <span class="k">method</span><span class="p">,</span>
  <span class="n">JSON_EXTRACT_SCALAR</span><span class="p">(</span><span class="n">resource</span><span class="p">.</span><span class="n">labels</span><span class="p">,</span> <span class="s1">'$.endpoint'</span><span class="p">)</span> <span class="k">AS</span> <span class="n">endpoint</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="n">value</span> <span class="k">AS</span> <span class="n">INT64</span><span class="p">))</span> <span class="k">AS</span> <span class="n">request_count</span>
<span class="k">FROM</span>
  <span class="nv">`your-project.monitoring.metrics`</span>
<span class="k">WHERE</span>
  <span class="n">metric</span><span class="p">.</span><span class="k">type</span> <span class="o">=</span> <span class="s1">'serviceruntime.googleapis.com/api/request_count'</span>
  <span class="k">AND</span> <span class="k">EXTRACT</span><span class="p">(</span><span class="nb">DATE</span> <span class="k">FROM</span> <span class="nb">timestamp</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="nb">date</span><span class="p">,</span> <span class="n">service</span><span class="p">,</span> <span class="k">method</span><span class="p">,</span> <span class="n">endpoint</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="option-b-application-logs-cloud-loggingcloudwatch">Option B: Application Logs (Cloud Logging/CloudWatch)</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Example: Count requests from application logs</span>
<span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="n">traffic_from_logs</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="k">EXTRACT</span><span class="p">(</span><span class="nb">DATE</span> <span class="k">FROM</span> <span class="nb">timestamp</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">date</span><span class="p">,</span>
  <span class="n">JSON_EXTRACT_SCALAR</span><span class="p">(</span><span class="n">jsonPayload</span><span class="p">,</span> <span class="s1">'$.service'</span><span class="p">)</span> <span class="k">AS</span> <span class="n">service</span><span class="p">,</span>
  <span class="n">JSON_EXTRACT_SCALAR</span><span class="p">(</span><span class="n">jsonPayload</span><span class="p">,</span> <span class="s1">'$.endpoint'</span><span class="p">)</span> <span class="k">AS</span> <span class="n">endpoint</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">request_count</span>
<span class="k">FROM</span>
  <span class="nv">`your-project.logs.cloudfunctions_googleapis_com_cloud_functions`</span>
<span class="k">WHERE</span>
  <span class="n">jsonPayload</span><span class="p">.</span><span class="n">message</span> <span class="k">LIKE</span> <span class="s1">'%Request%'</span>
  <span class="k">AND</span> <span class="k">EXTRACT</span><span class="p">(</span><span class="nb">DATE</span> <span class="k">FROM</span> <span class="nb">timestamp</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="nb">date</span><span class="p">,</span> <span class="n">service</span><span class="p">,</span> <span class="n">endpoint</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="option-c-load-balancer-logs">Option C: Load Balancer Logs</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Example: Extract from Cloud Load Balancer logs</span>
<span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="n">traffic_from_lb</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="k">EXTRACT</span><span class="p">(</span><span class="nb">DATE</span> <span class="k">FROM</span> <span class="nb">timestamp</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">date</span><span class="p">,</span>
  <span class="n">http_request</span><span class="p">.</span><span class="n">request_method</span> <span class="k">AS</span> <span class="k">method</span><span class="p">,</span>
  <span class="n">http_request</span><span class="p">.</span><span class="n">request_url</span> <span class="k">AS</span> <span class="n">endpoint</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">request_count</span>
<span class="k">FROM</span>
  <span class="nv">`your-project.logs.requests`</span>
<span class="k">WHERE</span>
  <span class="k">EXTRACT</span><span class="p">(</span><span class="nb">DATE</span> <span class="k">FROM</span> <span class="nb">timestamp</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="nb">date</span><span class="p">,</span> <span class="k">method</span><span class="p">,</span> <span class="n">endpoint</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>If you don’t have traffic metrics yet:</strong></p>
<ul>
  <li>Start with service-level aggregation (total requests per service)</li>
  <li>Add endpoint-level granularity later</li>
  <li>Use application logs as a fallback</li>
</ul>

<hr />

<h2 id="step-3-build-the-cost-to-serve-query">Step 3: Build the Cost-to-Serve Query</h2>

<p>Here’s the complete BigQuery query that joins costs with traffic:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Cost-to-Serve: Cost per request by service</span>
<span class="k">WITH</span> <span class="n">cost_by_service</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">service</span><span class="p">.</span><span class="n">description</span> <span class="k">AS</span> <span class="n">service</span><span class="p">,</span>
    <span class="nb">DATE</span><span class="p">(</span><span class="n">usage_start_time</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">date</span><span class="p">,</span>
    <span class="k">SUM</span><span class="p">(</span><span class="n">cost</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_cost_eur</span><span class="p">,</span>
    <span class="k">SUM</span><span class="p">(</span><span class="k">usage</span><span class="p">.</span><span class="n">amount</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_usage</span>
  <span class="k">FROM</span>
    <span class="nv">`your-project.billing_export.gcp_billing_export_v1_XXXXXX`</span>
  <span class="k">WHERE</span>
    <span class="n">project</span><span class="p">.</span><span class="n">id</span> <span class="k">IN</span> <span class="p">(</span><span class="s1">'prod-a'</span><span class="p">,</span> <span class="s1">'prod-b'</span><span class="p">)</span>
    <span class="k">AND</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">usage_start_time</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
  <span class="k">GROUP</span> <span class="k">BY</span>
    <span class="n">service</span><span class="p">,</span> <span class="nb">date</span>
<span class="p">),</span>
<span class="n">traffic_by_service</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">service</span><span class="p">,</span>
    <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">date</span><span class="p">,</span>
    <span class="k">SUM</span><span class="p">(</span><span class="n">requests</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_requests</span>
  <span class="k">FROM</span>
    <span class="nv">`your-project.analytics.traffic_metrics`</span>  <span class="c1">-- Your traffic view/table</span>
  <span class="k">WHERE</span>
    <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
  <span class="k">GROUP</span> <span class="k">BY</span>
    <span class="n">service</span><span class="p">,</span> <span class="nb">date</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">COALESCE</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">service</span><span class="p">,</span> <span class="n">t</span><span class="p">.</span><span class="n">service</span><span class="p">)</span> <span class="k">AS</span> <span class="n">service</span><span class="p">,</span>
  <span class="n">COALESCE</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="nb">date</span><span class="p">,</span> <span class="n">t</span><span class="p">.</span><span class="nb">date</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">date</span><span class="p">,</span>
  <span class="k">c</span><span class="p">.</span><span class="n">total_cost_eur</span><span class="p">,</span>
  <span class="n">t</span><span class="p">.</span><span class="n">total_requests</span><span class="p">,</span>
  <span class="c1">-- Cost per request</span>
  <span class="k">c</span><span class="p">.</span><span class="n">total_cost_eur</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="n">t</span><span class="p">.</span><span class="n">total_requests</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">AS</span> <span class="n">eur_per_request</span><span class="p">,</span>
  <span class="c1">-- Cost per 1k requests (common metric)</span>
  <span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">total_cost_eur</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="n">t</span><span class="p">.</span><span class="n">total_requests</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">*</span> <span class="mi">1000</span> <span class="k">AS</span> <span class="n">eur_per_1k_requests</span><span class="p">,</span>
  <span class="c1">-- Cost per 1M requests</span>
  <span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">total_cost_eur</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="n">t</span><span class="p">.</span><span class="n">total_requests</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">*</span> <span class="mi">1000000</span> <span class="k">AS</span> <span class="n">eur_per_1M_requests</span>
<span class="k">FROM</span>
  <span class="n">cost_by_service</span> <span class="k">c</span>
<span class="k">FULL</span> <span class="k">OUTER</span> <span class="k">JOIN</span>
  <span class="n">traffic_by_service</span> <span class="n">t</span>
<span class="k">USING</span> <span class="p">(</span><span class="n">service</span><span class="p">,</span> <span class="nb">date</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="nb">date</span> <span class="k">DESC</span><span class="p">,</span> <span class="n">service</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Save this as a view:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="nv">`your-project.analytics.cost_to_serve`</span> <span class="k">AS</span>
<span class="c1">-- (paste the query above)</span>
</code></pre></div></div>

<hr />

<h2 id="step-4-add-endpoint-level-granularity-optional">Step 4: Add Endpoint-Level Granularity (Optional)</h2>

<p>For more actionable insights, break down costs by endpoint:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Cost-to-Serve by endpoint (requires endpoint-level traffic data)</span>
<span class="k">WITH</span> <span class="n">cost_by_service</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="c1">-- Same as above</span>
<span class="p">),</span>
<span class="n">traffic_by_endpoint</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">service</span><span class="p">,</span>
    <span class="n">endpoint</span><span class="p">,</span>
    <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">date</span><span class="p">,</span>
    <span class="k">SUM</span><span class="p">(</span><span class="n">requests</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_requests</span>
  <span class="k">FROM</span>
    <span class="nv">`your-project.analytics.traffic_metrics`</span>
  <span class="k">WHERE</span>
    <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
  <span class="k">GROUP</span> <span class="k">BY</span>
    <span class="n">service</span><span class="p">,</span> <span class="n">endpoint</span><span class="p">,</span> <span class="nb">date</span>
<span class="p">),</span>
<span class="n">service_cost_per_request</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">service</span><span class="p">,</span>
    <span class="nb">date</span><span class="p">,</span>
    <span class="n">total_cost_eur</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="n">total_requests</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">AS</span> <span class="n">eur_per_request</span>
  <span class="k">FROM</span>
    <span class="c1">-- Use the service-level CTS query above</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">t</span><span class="p">.</span><span class="n">service</span><span class="p">,</span>
  <span class="n">t</span><span class="p">.</span><span class="n">endpoint</span><span class="p">,</span>
  <span class="n">t</span><span class="p">.</span><span class="nb">date</span><span class="p">,</span>
  <span class="n">t</span><span class="p">.</span><span class="n">total_requests</span><span class="p">,</span>
  <span class="n">s</span><span class="p">.</span><span class="n">eur_per_request</span> <span class="o">*</span> <span class="n">t</span><span class="p">.</span><span class="n">total_requests</span> <span class="k">AS</span> <span class="n">estimated_endpoint_cost</span><span class="p">,</span>
  <span class="n">s</span><span class="p">.</span><span class="n">eur_per_request</span> <span class="k">AS</span> <span class="n">cost_per_request</span>
<span class="k">FROM</span>
  <span class="n">traffic_by_endpoint</span> <span class="n">t</span>
<span class="k">JOIN</span>
  <span class="n">service_cost_per_request</span> <span class="n">s</span>
<span class="k">USING</span> <span class="p">(</span><span class="n">service</span><span class="p">,</span> <span class="nb">date</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="n">estimated_endpoint_cost</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="step-5-create-executive-dashboard">Step 5: Create Executive Dashboard</h2>

<p>Use the CTS view to build dashboards:</p>

<h3 id="daily-cost-per-service">Daily Cost per Service</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">service</span><span class="p">,</span>
  <span class="nb">date</span><span class="p">,</span>
  <span class="n">eur_per_1k_requests</span><span class="p">,</span>
  <span class="n">total_requests</span><span class="p">,</span>
  <span class="n">total_cost_eur</span><span class="p">,</span>
  <span class="c1">-- Trend: compare to previous day</span>
  <span class="n">LAG</span><span class="p">(</span><span class="n">eur_per_1k_requests</span><span class="p">)</span> <span class="n">OVER</span> <span class="p">(</span>
    <span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">service</span>
    <span class="k">ORDER</span> <span class="k">BY</span> <span class="nb">date</span>
  <span class="p">)</span> <span class="k">AS</span> <span class="n">prev_day_cost_per_1k</span><span class="p">,</span>
  <span class="n">eur_per_1k_requests</span> <span class="o">-</span> <span class="n">LAG</span><span class="p">(</span><span class="n">eur_per_1k_requests</span><span class="p">)</span> <span class="n">OVER</span> <span class="p">(</span>
    <span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">service</span>
    <span class="k">ORDER</span> <span class="k">BY</span> <span class="nb">date</span>
  <span class="p">)</span> <span class="k">AS</span> <span class="n">cost_change</span>
<span class="k">FROM</span>
  <span class="nv">`your-project.analytics.cost_to_serve`</span>
<span class="k">WHERE</span>
  <span class="nb">date</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="nb">date</span> <span class="k">DESC</span><span class="p">,</span> <span class="n">total_cost_eur</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="top-cost-drivers">Top Cost Drivers</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">service</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">total_cost_eur</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_cost_30d</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">total_requests</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_requests_30d</span><span class="p">,</span>
  <span class="k">AVG</span><span class="p">(</span><span class="n">eur_per_1k_requests</span><span class="p">)</span> <span class="k">AS</span> <span class="n">avg_cost_per_1k</span><span class="p">,</span>
  <span class="c1">-- Cost efficiency: lower is better</span>
  <span class="k">AVG</span><span class="p">(</span><span class="n">eur_per_1k_requests</span><span class="p">)</span> <span class="k">AS</span> <span class="n">efficiency_score</span>
<span class="k">FROM</span>
  <span class="nv">`your-project.analytics.cost_to_serve`</span>
<span class="k">WHERE</span>
  <span class="nb">date</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="n">service</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="n">total_cost_30d</span> <span class="k">DESC</span>
<span class="k">LIMIT</span> <span class="mi">10</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="cost-anomaly-detection">Cost Anomaly Detection</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Flag services with sudden cost increases</span>
<span class="k">WITH</span> <span class="n">daily_avg</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">service</span><span class="p">,</span>
    <span class="nb">date</span><span class="p">,</span>
    <span class="n">eur_per_1k_requests</span><span class="p">,</span>
    <span class="k">AVG</span><span class="p">(</span><span class="n">eur_per_1k_requests</span><span class="p">)</span> <span class="n">OVER</span> <span class="p">(</span>
      <span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">service</span>
      <span class="k">ORDER</span> <span class="k">BY</span> <span class="nb">date</span>
      <span class="k">ROWS</span> <span class="k">BETWEEN</span> <span class="mi">6</span> <span class="k">PRECEDING</span> <span class="k">AND</span> <span class="mi">1</span> <span class="k">PRECEDING</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="n">rolling_7d_avg</span><span class="p">,</span>
    <span class="n">STDDEV</span><span class="p">(</span><span class="n">eur_per_1k_requests</span><span class="p">)</span> <span class="n">OVER</span> <span class="p">(</span>
      <span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">service</span>
      <span class="k">ORDER</span> <span class="k">BY</span> <span class="nb">date</span>
      <span class="k">ROWS</span> <span class="k">BETWEEN</span> <span class="mi">6</span> <span class="k">PRECEDING</span> <span class="k">AND</span> <span class="mi">1</span> <span class="k">PRECEDING</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="n">rolling_7d_stddev</span>
  <span class="k">FROM</span>
    <span class="nv">`your-project.analytics.cost_to_serve`</span>
  <span class="k">WHERE</span>
    <span class="nb">date</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">service</span><span class="p">,</span>
  <span class="nb">date</span><span class="p">,</span>
  <span class="n">eur_per_1k_requests</span><span class="p">,</span>
  <span class="n">rolling_7d_avg</span><span class="p">,</span>
  <span class="c1">-- Flag if cost is 2 standard deviations above average</span>
  <span class="k">CASE</span>
    <span class="k">WHEN</span> <span class="n">eur_per_1k_requests</span> <span class="o">&gt;</span> <span class="n">rolling_7d_avg</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">rolling_7d_stddev</span>
    <span class="k">THEN</span> <span class="s1">'ANOMALY'</span>
    <span class="k">ELSE</span> <span class="s1">'NORMAL'</span>
  <span class="k">END</span> <span class="k">AS</span> <span class="n">status</span>
<span class="k">FROM</span>
  <span class="n">daily_avg</span>
<span class="k">WHERE</span>
  <span class="n">status</span> <span class="o">=</span> <span class="s1">'ANOMALY'</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="nb">date</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="step-6-correlate-with-business-metrics">Step 6: Correlate with Business Metrics</h2>

<p>Connect costs to business value:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Cost-to-Serve with revenue/user metrics</span>
<span class="k">WITH</span> <span class="n">cts</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="nv">`your-project.analytics.cost_to_serve`</span>
<span class="p">),</span>
<span class="n">revenue</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">service</span><span class="p">,</span>
    <span class="nb">DATE</span><span class="p">(</span><span class="n">transaction_time</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">date</span><span class="p">,</span>
    <span class="k">SUM</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span> <span class="k">AS</span> <span class="n">revenue_eur</span><span class="p">,</span>
    <span class="k">COUNT</span><span class="p">(</span><span class="k">DISTINCT</span> <span class="n">user_id</span><span class="p">)</span> <span class="k">AS</span> <span class="n">active_users</span>
  <span class="k">FROM</span>
    <span class="nv">`your-project.analytics.transactions`</span>
  <span class="k">WHERE</span>
    <span class="nb">DATE</span><span class="p">(</span><span class="n">transaction_time</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
  <span class="k">GROUP</span> <span class="k">BY</span>
    <span class="n">service</span><span class="p">,</span> <span class="nb">date</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="k">c</span><span class="p">.</span><span class="n">service</span><span class="p">,</span>
  <span class="k">c</span><span class="p">.</span><span class="nb">date</span><span class="p">,</span>
  <span class="k">c</span><span class="p">.</span><span class="n">eur_per_1k_requests</span> <span class="k">AS</span> <span class="n">cost_per_1k</span><span class="p">,</span>
  <span class="n">r</span><span class="p">.</span><span class="n">revenue_eur</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">total_requests</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1000</span> <span class="k">AS</span> <span class="n">revenue_per_1k</span><span class="p">,</span>
  <span class="c1">-- Profit margin per request</span>
  <span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">revenue_eur</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">total_requests</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span> <span class="o">-</span> <span class="k">c</span><span class="p">.</span><span class="n">eur_per_1k_requests</span> <span class="k">AS</span> <span class="n">profit_per_1k</span><span class="p">,</span>
  <span class="c1">-- ROI</span>
  <span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">revenue_eur</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">total_cost_eur</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="k">AS</span> <span class="n">roi</span>
<span class="k">FROM</span>
  <span class="n">cts</span> <span class="k">c</span>
<span class="k">JOIN</span>
  <span class="n">revenue</span> <span class="n">r</span>
<span class="k">USING</span> <span class="p">(</span><span class="n">service</span><span class="p">,</span> <span class="nb">date</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="n">profit_per_1k</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="common-use-cases">Common Use Cases</h2>

<h3 id="1-track-cost-trends">1. Track Cost Trends</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Weekly cost trend</span>
<span class="k">SELECT</span>
  <span class="k">EXTRACT</span><span class="p">(</span><span class="n">WEEK</span> <span class="k">FROM</span> <span class="nb">date</span><span class="p">)</span> <span class="k">AS</span> <span class="n">week</span><span class="p">,</span>
  <span class="n">service</span><span class="p">,</span>
  <span class="k">AVG</span><span class="p">(</span><span class="n">eur_per_1k_requests</span><span class="p">)</span> <span class="k">AS</span> <span class="n">avg_cost_per_1k</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">total_cost_eur</span><span class="p">)</span> <span class="k">AS</span> <span class="n">weekly_cost</span>
<span class="k">FROM</span>
  <span class="nv">`your-project.analytics.cost_to_serve`</span>
<span class="k">WHERE</span>
  <span class="nb">date</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">12</span> <span class="n">WEEK</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="n">week</span><span class="p">,</span> <span class="n">service</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="n">week</span> <span class="k">DESC</span><span class="p">,</span> <span class="n">weekly_cost</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="2-flag-high-cost-endpoints">2. Flag High-Cost Endpoints</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Endpoints costing more than €0.10 per request</span>
<span class="k">SELECT</span>
  <span class="n">service</span><span class="p">,</span>
  <span class="n">endpoint</span><span class="p">,</span>
  <span class="nb">date</span><span class="p">,</span>
  <span class="n">cost_per_request</span><span class="p">,</span>
  <span class="n">total_requests</span><span class="p">,</span>
  <span class="n">estimated_endpoint_cost</span>
<span class="k">FROM</span>
  <span class="nv">`your-project.analytics.cost_to_serve_by_endpoint`</span>
<span class="k">WHERE</span>
  <span class="n">cost_per_request</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mi">10</span>
  <span class="k">AND</span> <span class="nb">date</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="n">estimated_endpoint_cost</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="3-forecast-future-costs">3. Forecast Future Costs</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Simple linear forecast based on traffic growth</span>
<span class="k">WITH</span> <span class="n">historical</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">service</span><span class="p">,</span>
    <span class="k">AVG</span><span class="p">(</span><span class="n">eur_per_1k_requests</span><span class="p">)</span> <span class="k">AS</span> <span class="n">avg_cost_per_1k</span><span class="p">,</span>
    <span class="k">AVG</span><span class="p">(</span><span class="n">total_requests</span><span class="p">)</span> <span class="k">AS</span> <span class="n">avg_daily_requests</span><span class="p">,</span>
    <span class="c1">-- Traffic growth rate (simplified)</span>
    <span class="p">(</span><span class="k">MAX</span><span class="p">(</span><span class="n">total_requests</span><span class="p">)</span> <span class="o">-</span> <span class="k">MIN</span><span class="p">(</span><span class="n">total_requests</span><span class="p">))</span> <span class="o">/</span> <span class="k">COUNT</span><span class="p">(</span><span class="k">DISTINCT</span> <span class="nb">date</span><span class="p">)</span> <span class="k">AS</span> <span class="n">daily_growth</span>
  <span class="k">FROM</span>
    <span class="nv">`your-project.analytics.cost_to_serve`</span>
  <span class="k">WHERE</span>
    <span class="nb">date</span> <span class="o">&gt;=</span> <span class="n">DATE_SUB</span><span class="p">(</span><span class="k">CURRENT_DATE</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
  <span class="k">GROUP</span> <span class="k">BY</span>
    <span class="n">service</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">service</span><span class="p">,</span>
  <span class="n">avg_cost_per_1k</span><span class="p">,</span>
  <span class="n">avg_daily_requests</span><span class="p">,</span>
  <span class="c1">-- Forecast 30 days ahead</span>
  <span class="n">avg_daily_requests</span> <span class="o">+</span> <span class="p">(</span><span class="n">daily_growth</span> <span class="o">*</span> <span class="mi">30</span><span class="p">)</span> <span class="k">AS</span> <span class="n">forecasted_daily_requests</span><span class="p">,</span>
  <span class="p">(</span><span class="n">avg_daily_requests</span> <span class="o">+</span> <span class="p">(</span><span class="n">daily_growth</span> <span class="o">*</span> <span class="mi">30</span><span class="p">))</span> <span class="o">*</span> <span class="n">avg_cost_per_1k</span> <span class="o">/</span> <span class="mi">1000</span> <span class="o">*</span> <span class="mi">30</span> <span class="k">AS</span> <span class="n">forecasted_monthly_cost</span>
<span class="k">FROM</span>
  <span class="n">historical</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="troubleshooting">Troubleshooting</h2>

<h3 id="problem-no-traffic-data">Problem: No traffic data</h3>

<p><strong>Solution:</strong> Start with service-level aggregation from billing export only:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">service</span><span class="p">.</span><span class="n">description</span> <span class="k">AS</span> <span class="n">service</span><span class="p">,</span>
  <span class="nb">DATE</span><span class="p">(</span><span class="n">usage_start_time</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">date</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">cost</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_cost</span>
<span class="k">FROM</span>
  <span class="nv">`your-project.billing_export.gcp_billing_export_v1_XXXXXX`</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="n">service</span><span class="p">,</span> <span class="nb">date</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="problem-costs-dont-match-billing">Problem: Costs don’t match billing</h3>

<p><strong>Solution:</strong></p>
<ul>
  <li>Check billing export delay (usually 24-48 hours)</li>
  <li>Verify project filters</li>
  <li>Include all cost components (compute, storage, network, etc.)</li>
</ul>

<h3 id="problem-missing-service-labels">Problem: Missing service labels</h3>

<p><strong>Solution:</strong></p>
<ul>
  <li>Use <code class="language-plaintext highlighter-rouge">resource.name</code> or <code class="language-plaintext highlighter-rouge">resource.type</code> as fallback</li>
  <li>Add custom labels in application code</li>
  <li>Use Cloud Monitoring resource labels</li>
</ul>

<hr />

<h2 id="best-practices">Best Practices</h2>

<ol>
  <li><strong>Start simple</strong>: Service-level CTS is enough to get started</li>
  <li><strong>Iterate</strong>: Add endpoint-level granularity after service-level works</li>
  <li><strong>Automate</strong>: Schedule daily queries to keep data fresh</li>
  <li><strong>Alert</strong>: Set up alerts for cost anomalies (&gt; 20% increase)</li>
  <li><strong>Correlate</strong>: Connect costs to business metrics (revenue, users)</li>
  <li><strong>Document</strong>: Keep a data dictionary of service/endpoint mappings</li>
</ol>

<hr />

<h2 id="expected-outcomes">Expected Outcomes</h2>

<p>With cost-to-serve in place, you should be able to:</p>

<ul>
  <li><strong>Track €/1k requests</strong> in executive dashboards</li>
  <li><strong>Correlate costs</strong> with p95 latency and error rates</li>
  <li><strong>Flag endpoints</strong> whose cost increases but value decreases</li>
  <li><strong>Prioritize optimization</strong> work based on cost impact</li>
  <li><strong>Forecast costs</strong> based on traffic growth</li>
  <li><strong>Price products</strong> based on actual infrastructure costs</li>
</ul>

<p><strong>Outcome:</strong> Aligns engineering work with money-decisions get faster and calmer.</p>

<hr />

<h2 id="next-steps">Next Steps</h2>

<ul>
  <li><strong>Week 1</strong>: Set up service-level CTS</li>
  <li><strong>Week 2</strong>: Add endpoint-level granularity</li>
  <li><strong>Week 3</strong>: Correlate with business metrics</li>
  <li><strong>Week 4</strong>: Build executive dashboards and alerts</li>
</ul>

<p>Want help setting this up?</p>
<ul>
  <li><a href="mailto:atabakkheirkhah@gmail.com">Book a 20-min intro</a></li>
</ul>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Hadoop/Oracle -> BigQuery: 7 Pitfalls That Blow Up Cost (and Fixes)</title>
					
					<link>https://atabakkh.github.io/cloud/2025/01/15/bigquery-migration-pitfalls.html</link>

					

					<pubDate>Wed, 15 Jan 2025 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2025/01/15/bigquery-migration-pitfalls</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Context:</strong> BigQuery is fast to adopt-and easy to overspend on. Here’s a comprehensive checklist I use in migrations to avoid common cost pitfalls.</p>

<p>Migrating from Hadoop/Oracle to BigQuery can reduce costs by 50-70%, but only if you avoid these common mistakes. This guide covers the 8 most expensive pitfalls and how to fix them.</p>

<hr />

<h2 id="pitfall-1-no-access-pattern-design">Pitfall 1: No Access-Pattern Design</h2>

<p><strong>The Problem:</strong> Tables designed without understanding query patterns lead to full table scans and expensive joins.</p>

<p><strong>Why it’s expensive:</strong></p>
<ul>
  <li>Full table scans: $5 per TB scanned</li>
  <li>Unnecessary joins: Multiplied scan costs</li>
  <li>Missing pre-aggregations: Repeated expensive computations</li>
</ul>

<p><strong>The Fix:</strong> Model serving tables to match WHERE/JOIN patterns; pre-compute heavy joins.</p>

<p><strong>Example:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- BAD: Generic table, every query scans everything</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">events</span> <span class="p">(</span>
  <span class="n">event_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">user_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_type</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_ts</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="k">data</span> <span class="n">JSON</span>
<span class="p">);</span>

<span class="c1">-- GOOD: Serving table optimized for common queries</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">serving</span><span class="p">.</span><span class="n">user_events_recent</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="n">user_id</span><span class="p">,</span>
  <span class="n">event_type</span><span class="p">,</span>
  <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span> <span class="k">AS</span> <span class="n">event_date</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">event_count</span><span class="p">,</span>
  <span class="n">ARRAY_AGG</span><span class="p">(</span><span class="n">STRUCT</span><span class="p">(</span><span class="n">event_id</span><span class="p">,</span> <span class="n">event_ts</span><span class="p">,</span> <span class="k">data</span><span class="p">)</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">event_ts</span> <span class="k">DESC</span> <span class="k">LIMIT</span> <span class="mi">100</span><span class="p">)</span> <span class="k">AS</span> <span class="n">recent_events</span>
<span class="k">FROM</span>
  <span class="n">landing</span><span class="p">.</span><span class="n">raw_events</span>
<span class="k">WHERE</span>
  <span class="n">event_ts</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span>
  <span class="n">user_id</span><span class="p">,</span> <span class="n">event_type</span><span class="p">,</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">);</span>

<span class="c1">-- Pre-computed join for dashboard queries</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">serving</span><span class="p">.</span><span class="n">user_events_with_profile</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="n">ue</span><span class="p">.</span><span class="o">*</span><span class="p">,</span>
  <span class="n">up</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
  <span class="n">up</span><span class="p">.</span><span class="n">email</span><span class="p">,</span>
  <span class="n">up</span><span class="p">.</span><span class="n">subscription_tier</span>
<span class="k">FROM</span>
  <span class="n">serving</span><span class="p">.</span><span class="n">user_events_recent</span> <span class="n">ue</span>
<span class="k">JOIN</span>
  <span class="n">curated</span><span class="p">.</span><span class="n">user_profiles</span> <span class="n">up</span>
<span class="k">USING</span> <span class="p">(</span><span class="n">user_id</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>Best practices:</strong></p>
<ul>
  <li><strong>Analyze query logs</strong> from your old system to identify patterns</li>
  <li><strong>Create serving tables</strong> for common query patterns</li>
  <li><strong>Pre-compute joins</strong> that appear in &gt; 10% of queries</li>
  <li><strong>Document access patterns</strong> in table comments</li>
</ul>

<hr />

<h2 id="pitfall-2-missing-partitioncluster-strategy">Pitfall 2: Missing Partition/Cluster Strategy</h2>

<p><strong>The Problem:</strong> Without partitioning and clustering, every query scans the entire table.</p>

<p><strong>Why it’s expensive:</strong></p>
<ul>
  <li>Full table scans: $5 per TB scanned</li>
  <li>No query pruning: Can’t skip irrelevant data</li>
  <li>Slow queries: Higher latency = higher costs</li>
</ul>

<p><strong>The Fix:</strong> Always partition by date/time; cluster by high-cardinality columns used in WHERE/JOIN.</p>

<p><strong>Example:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">--  BAD: No partitioning, every query scans everything</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">events</span> <span class="p">(</span>
  <span class="n">event_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">user_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_ts</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="k">data</span> <span class="n">JSON</span>
<span class="p">);</span>

<span class="c1">--  GOOD: Partitioned by date, clustered by user_id and event_type</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">curated</span><span class="p">.</span><span class="n">fact_events</span> <span class="p">(</span>
  <span class="n">event_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">user_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_type</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_ts</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="k">data</span> <span class="n">JSON</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">event_type</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">description</span> <span class="o">=</span> <span class="nv">"Partitioned by date for time-range queries, clustered by user_id and event_type for user-specific queries"</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong>Partitioning strategy:</strong></p>
<ul>
  <li><strong>Daily partitions</strong> for high-volume data (events, logs)</li>
  <li><strong>Monthly partitions</strong> for lower-volume data (aggregates, reports)</li>
  <li><strong>Integer partitioning</strong> for non-date columns (e.g., <code class="language-plaintext highlighter-rouge">shard_id</code>)</li>
</ul>

<p><strong>Clustering strategy:</strong></p>
<ul>
  <li><strong>Cluster by columns</strong> used in WHERE clauses (user_id, account_id)</li>
  <li><strong>Cluster by join keys</strong> (foreign keys)</li>
  <li><strong>Limit to 4 columns</strong> (more columns = diminishing returns)</li>
  <li><strong>Avoid high-cardinality</strong> columns (UUIDs, hashes) unless frequently filtered</li>
</ul>

<p><strong>Real-world example:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Events table: partitioned by date, clustered by user and type</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">event_type</span>
<span class="k">AS</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">landing</span><span class="p">.</span><span class="n">raw_events</span><span class="p">;</span>

<span class="c1">-- Query only scans relevant partitions</span>
<span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> 
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="k">BETWEEN</span> <span class="s1">'2025-01-01'</span> <span class="k">AND</span> <span class="s1">'2025-01-07'</span>  <span class="c1">-- Only scans 7 partitions</span>
  <span class="k">AND</span> <span class="n">user_id</span> <span class="o">=</span> <span class="s1">'user-123'</span>                               <span class="c1">-- Clustering helps</span>
  <span class="k">AND</span> <span class="n">event_type</span> <span class="o">=</span> <span class="s1">'purchase'</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="pitfall-3-full-scans-via-wildcards">Pitfall 3: Full Scans via Wildcards</h2>

<p><strong>The Problem:</strong> Wildcard queries scan all matching tables/partitions, even if you only need recent data.</p>

<p><strong>Why it’s expensive:</strong></p>
<ul>
  <li>Scans all partitions: Even old data you don’t need</li>
  <li>No partition pruning: Can’t skip irrelevant partitions</li>
  <li>Multiplied costs: Each partition scanned separately</li>
</ul>

<p><strong>The Fix:</strong> Always filter by <code class="language-plaintext highlighter-rouge">_PARTITIONDATE</code> or partition column; prefer parameterized queries.</p>

<p><strong>Example:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">--  BAD: Wildcard scans all partitions (expensive!)</span>
<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="nv">`project.dataset.events_*`</span>
<span class="k">WHERE</span> <span class="n">event_type</span> <span class="o">=</span> <span class="s1">'purchase'</span><span class="p">;</span>

<span class="c1">--  GOOD: Filter by partition date first</span>
<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="nv">`project.dataset.events_*`</span>
<span class="k">WHERE</span> <span class="n">_PARTITIONDATE</span> <span class="k">BETWEEN</span> <span class="s1">'2025-01-01'</span> <span class="k">AND</span> <span class="s1">'2025-01-31'</span>
  <span class="k">AND</span> <span class="n">event_type</span> <span class="o">=</span> <span class="s1">'purchase'</span><span class="p">;</span>

<span class="c1">--  BETTER: Use partitioned table with date filter</span>
<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="k">BETWEEN</span> <span class="s1">'2025-01-01'</span> <span class="k">AND</span> <span class="s1">'2025-01-31'</span>
  <span class="k">AND</span> <span class="n">event_type</span> <span class="o">=</span> <span class="s1">'purchase'</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Best practices:</strong></p>
<ul>
  <li><strong>Always filter by partition</strong> before other WHERE clauses</li>
  <li><strong>Use parameterized queries</strong> to enforce partition filters</li>
  <li><strong>Set query timeouts</strong> to prevent runaway scans</li>
  <li><strong>Use <code class="language-plaintext highlighter-rouge">_PARTITIONTIME</code></strong> for timestamp-partitioned tables</li>
</ul>

<p><strong>Parameterized query example:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Enforce partition filter via parameter</span>
<span class="k">DECLARE</span> <span class="n">start_date</span> <span class="nb">DATE</span> <span class="k">DEFAULT</span> <span class="s1">'2025-01-01'</span><span class="p">;</span>
<span class="k">DECLARE</span> <span class="n">end_date</span> <span class="nb">DATE</span> <span class="k">DEFAULT</span> <span class="s1">'2025-01-31'</span><span class="p">;</span>

<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="k">BETWEEN</span> <span class="n">start_date</span> <span class="k">AND</span> <span class="n">end_date</span>
  <span class="k">AND</span> <span class="n">event_type</span> <span class="o">=</span> <span class="s1">'purchase'</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="pitfall-4-hot-keys--skew">Pitfall 4: Hot Keys &amp; Skew</h2>

<p><strong>The Problem:</strong> Clustering by high-cardinality or skewed columns causes hot partitions and slow queries.</p>

<p><strong>Why it’s expensive:</strong></p>
<ul>
  <li>Hot partitions: Uneven data distribution</li>
  <li>Slow queries: High latency = higher costs</li>
  <li>Throttling: BigQuery may throttle skewed queries</li>
</ul>

<p><strong>The Fix:</strong> Re-shard by adding a salting key; avoid clustering by super-high-cardinality IDs.</p>

<p><strong>Example:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">--  BAD: Clustering by UUID (high cardinality, no benefit)</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">events</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">event_id</span>  <span class="c1">-- UUID, 1B+ unique values, no benefit</span>
<span class="k">AS</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">landing</span><span class="p">.</span><span class="n">raw_events</span><span class="p">;</span>

<span class="c1">--  GOOD: Add salting key for high-cardinality columns</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">shard_id</span><span class="p">,</span> <span class="n">user_id</span>  <span class="c1">-- Shard_id = MOD(FARM_FINGERPRINT(event_id), 100)</span>
<span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="o">*</span><span class="p">,</span>
  <span class="k">MOD</span><span class="p">(</span><span class="n">FARM_FINGERPRINT</span><span class="p">(</span><span class="n">event_id</span><span class="p">),</span> <span class="mi">100</span><span class="p">)</span> <span class="k">AS</span> <span class="n">shard_id</span>  <span class="c1">-- 100 shards</span>
<span class="k">FROM</span> <span class="n">landing</span><span class="p">.</span><span class="n">raw_events</span><span class="p">;</span>

<span class="c1">-- Query with shard filter for better distribution</span>
<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="o">=</span> <span class="s1">'2025-01-15'</span>
  <span class="k">AND</span> <span class="n">shard_id</span> <span class="o">=</span> <span class="k">MOD</span><span class="p">(</span><span class="n">FARM_FINGERPRINT</span><span class="p">(</span><span class="s1">'target-event-id'</span><span class="p">),</span> <span class="mi">100</span><span class="p">)</span>
  <span class="k">AND</span> <span class="n">event_id</span> <span class="o">=</span> <span class="s1">'target-event-id'</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>When to use salting:</strong></p>
<ul>
  <li><strong>High-cardinality columns</strong> (&gt; 1M unique values)</li>
  <li><strong>Skewed distributions</strong> (80/20 rule)</li>
  <li><strong>Frequent point lookups</strong> by ID</li>
</ul>

<p><strong>Salting strategy:</strong></p>
<ul>
  <li><strong>100-1000 shards</strong> typically sufficient</li>
  <li><strong>Use <code class="language-plaintext highlighter-rouge">FARM_FINGERPRINT</code></strong> for deterministic hashing</li>
  <li><strong>Include shard in WHERE</strong> clause for best performance</li>
</ul>

<hr />

<h2 id="pitfall-5-raw-dump-as-final-model">Pitfall 5: Raw Dump as “Final Model”</h2>

<p><strong>The Problem:</strong> Using raw data as the final model leads to repeated expensive transformations in every query.</p>

<p><strong>Why it’s expensive:</strong></p>
<ul>
  <li>Repeated transformations: Same computation in every query</li>
  <li>No optimization: Can’t pre-aggregate or pre-join</li>
  <li>Complex queries: Higher complexity = higher costs</li>
</ul>

<p><strong>The Fix:</strong> Implement a layered architecture: landing -&gt; curated -&gt; serving; materialize BI views where needed; document lineage.</p>

<p><strong>Example architecture:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Layer 1: Landing (raw data, minimal transformation)</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">landing</span><span class="p">.</span><span class="n">raw_events</span> <span class="p">(</span>
  <span class="n">event_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">user_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_ts</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="n">raw_data</span> <span class="n">JSON</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">);</span>

<span class="c1">-- Layer 2: Curated (cleaned, validated, typed)</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span> <span class="p">(</span>
  <span class="n">event_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">user_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_type</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_ts</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="n">event_data</span> <span class="n">STRUCT</span><span class="o">&lt;</span>
    <span class="n">product_id</span> <span class="n">STRING</span><span class="p">,</span>
    <span class="n">amount</span> <span class="n">FLOAT64</span><span class="p">,</span>
    <span class="n">currency</span> <span class="n">STRING</span>
  <span class="o">&gt;</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">event_type</span>
<span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="n">event_id</span><span class="p">,</span>
  <span class="n">user_id</span><span class="p">,</span>
  <span class="n">JSON_EXTRACT_SCALAR</span><span class="p">(</span><span class="n">raw_data</span><span class="p">,</span> <span class="s1">'$.type'</span><span class="p">)</span> <span class="k">AS</span> <span class="n">event_type</span><span class="p">,</span>
  <span class="n">event_ts</span><span class="p">,</span>
  <span class="n">STRUCT</span><span class="p">(</span>
    <span class="n">JSON_EXTRACT_SCALAR</span><span class="p">(</span><span class="n">raw_data</span><span class="p">,</span> <span class="s1">'$.product_id'</span><span class="p">)</span> <span class="k">AS</span> <span class="n">product_id</span><span class="p">,</span>
    <span class="k">CAST</span><span class="p">(</span><span class="n">JSON_EXTRACT_SCALAR</span><span class="p">(</span><span class="n">raw_data</span><span class="p">,</span> <span class="s1">'$.amount'</span><span class="p">)</span> <span class="k">AS</span> <span class="n">FLOAT64</span><span class="p">)</span> <span class="k">AS</span> <span class="n">amount</span><span class="p">,</span>
    <span class="n">JSON_EXTRACT_SCALAR</span><span class="p">(</span><span class="n">raw_data</span><span class="p">,</span> <span class="s1">'$.currency'</span><span class="p">)</span> <span class="k">AS</span> <span class="n">currency</span>
  <span class="p">)</span> <span class="k">AS</span> <span class="n">event_data</span>
<span class="k">FROM</span> <span class="n">landing</span><span class="p">.</span><span class="n">raw_events</span>
<span class="k">WHERE</span> <span class="n">event_ts</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">90</span> <span class="k">DAY</span><span class="p">);</span>

<span class="c1">-- Layer 3: Serving (pre-aggregated, optimized for queries)</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">serving</span><span class="p">.</span><span class="n">user_daily_stats</span> <span class="p">(</span>
  <span class="n">user_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_date</span> <span class="nb">DATE</span><span class="p">,</span>
  <span class="n">total_events</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">total_revenue</span> <span class="n">FLOAT64</span><span class="p">,</span>
  <span class="n">unique_products</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">last_event_ts</span> <span class="nb">TIMESTAMP</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">event_date</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">user_id</span>
<span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="n">user_id</span><span class="p">,</span>
  <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span> <span class="k">AS</span> <span class="n">event_date</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_events</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">IF</span><span class="p">(</span><span class="n">event_type</span> <span class="o">=</span> <span class="s1">'purchase'</span><span class="p">,</span> <span class="n">event_data</span><span class="p">.</span><span class="n">amount</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="k">AS</span> <span class="n">total_revenue</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="k">DISTINCT</span> <span class="n">event_data</span><span class="p">.</span><span class="n">product_id</span><span class="p">)</span> <span class="k">AS</span> <span class="n">unique_products</span><span class="p">,</span>
  <span class="k">MAX</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span> <span class="k">AS</span> <span class="n">last_event_ts</span>
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">WHERE</span> <span class="n">event_ts</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">user_id</span><span class="p">,</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>Best practices:</strong></p>
<ul>
  <li><strong>Landing layer</strong>: Raw data, minimal transformation, long retention</li>
  <li><strong>Curated layer</strong>: Cleaned, validated, typed, medium retention</li>
  <li><strong>Serving layer</strong>: Pre-aggregated, optimized for queries, short retention</li>
  <li><strong>Document lineage</strong>: Track data flow from landing -&gt; curated -&gt; serving</li>
</ul>

<hr />

<h2 id="pitfall-6-no-retention--lifecycle">Pitfall 6: No Retention / Lifecycle</h2>

<p><strong>The Problem:</strong> Keeping all data forever leads to ever-increasing storage and query costs.</p>

<p><strong>Why it’s expensive:</strong></p>
<ul>
  <li>Storage costs: $0.02 per GB/month (adds up over years)</li>
  <li>Query costs: Scans include old data you don’t need</li>
  <li>Slower queries: More data = slower scans</li>
</ul>

<p><strong>The Fix:</strong> Implement table TTLs and partition expiration; set log retention by class (hot/warm/cold).</p>

<p><strong>Example:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">--  GOOD: Partition expiration for old data</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span> <span class="p">(</span>
  <span class="n">event_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">user_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_ts</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="k">data</span> <span class="n">JSON</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">event_type</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">partition_expiration_days</span> <span class="o">=</span> <span class="mi">90</span><span class="p">,</span>  <span class="c1">-- Auto-delete partitions older than 90 days</span>
  <span class="n">description</span> <span class="o">=</span> <span class="nv">"Events table with 90-day retention"</span>
<span class="p">);</span>

<span class="c1">--  GOOD: Table-level TTL for entire tables</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">staging</span><span class="p">.</span><span class="n">temp_results</span> <span class="p">(</span>
  <span class="n">result_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="k">data</span> <span class="n">JSON</span><span class="p">,</span>
  <span class="n">created_at</span> <span class="nb">TIMESTAMP</span>
<span class="p">)</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">expiration_timestamp</span> <span class="o">=</span> <span class="n">TIMESTAMP_ADD</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>  <span class="c1">-- Delete after 7 days</span>
<span class="p">);</span>

<span class="c1">--  GOOD: Different retention by data class</span>
<span class="c1">-- Hot data: 30 days (frequently queried)</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">serving</span><span class="p">.</span><span class="n">events_recent</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">OPTIONS</span> <span class="p">(</span><span class="n">partition_expiration_days</span> <span class="o">=</span> <span class="mi">30</span><span class="p">)</span>
<span class="k">AS</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span> <span class="k">WHERE</span> <span class="n">event_ts</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">30</span> <span class="k">DAY</span><span class="p">);</span>

<span class="c1">-- Warm data: 90 days (occasionally queried)</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">OPTIONS</span> <span class="p">(</span><span class="n">partition_expiration_days</span> <span class="o">=</span> <span class="mi">90</span><span class="p">)</span>
<span class="k">AS</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">landing</span><span class="p">.</span><span class="n">raw_events</span> <span class="k">WHERE</span> <span class="n">event_ts</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">90</span> <span class="k">DAY</span><span class="p">);</span>

<span class="c1">-- Cold data: Archive to Cloud Storage (rarely queried)</span>
<span class="c1">-- Use BigQuery export jobs to move old partitions to GCS</span>
</code></pre></div></div>

<p><strong>Retention strategy:</strong></p>
<ul>
  <li><strong>Hot data</strong> (frequently queried): 7-30 days</li>
  <li><strong>Warm data</strong> (occasionally queried): 30-90 days</li>
  <li><strong>Cold data</strong> (rarely queried): Archive to Cloud Storage</li>
  <li><strong>Compliance data</strong>: Keep as required by regulations</li>
</ul>

<p><strong>Cost savings:</strong></p>
<ul>
  <li>90-day retention vs. forever: <strong>75% storage cost reduction</strong></li>
  <li>Partition expiration: <strong>Automatic cleanup</strong>, no manual maintenance</li>
</ul>

<hr />

<h2 id="pitfall-7-no-cost-guardrails">Pitfall 7: No Cost Guardrails</h2>

<p><strong>The Problem:</strong> Without guardrails, runaway queries can cost thousands of dollars in minutes.</p>

<p><strong>Why it’s expensive:</strong></p>
<ul>
  <li>Unbounded scans: Queries can scan 100+ TB</li>
  <li>No limits: One bad query can blow your budget</li>
  <li>No alerts: You don’t know until the bill arrives</li>
</ul>

<p><strong>The Fix:</strong> Set per-dataset quotas, use reservations for steady workloads, implement cost controls (budgets, alerts).</p>

<p><strong>Example:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">--  GOOD: Per-dataset query size limit</span>
<span class="c1">-- Set via BigQuery console or gcloud:</span>
<span class="n">gcloud</span> <span class="n">alpha</span> <span class="n">bq</span> <span class="n">datasets</span> <span class="k">update</span> <span class="n">my_dataset</span> <span class="err">\</span>
  <span class="c1">--default-table-expiration=3600 \</span>
  <span class="c1">--default-partition-expiration=86400 \</span>
  <span class="c1">--max-staleness=3600</span>

<span class="c1">--  GOOD: Per-query size limit (via query options)</span>
<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="k">BETWEEN</span> <span class="s1">'2025-01-01'</span> <span class="k">AND</span> <span class="s1">'2025-01-31'</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">maximum_bytes_billed</span> <span class="o">=</span> <span class="mi">10000000000</span>  <span class="c1">-- 10 GB limit</span>
<span class="p">);</span>

<span class="c1">--  GOOD: Per-user daily quota (via IAM)</span>
<span class="c1">-- Set in BigQuery console: IAM &amp; Admin &gt; Quotas</span>
<span class="c1">-- Limit: 1 TB per user per day</span>
</code></pre></div></div>

<p><strong>Cost control strategy:</strong></p>
<ol>
  <li><strong>Per-dataset quotas</strong>: Limit total bytes scanned per day</li>
  <li><strong>Per-user quotas</strong>: Limit individual user costs</li>
  <li><strong>Per-query limits</strong>: Use <code class="language-plaintext highlighter-rouge">maximum_bytes_billed</code> in queries</li>
  <li><strong>Budget alerts</strong>: Set up Cloud Billing alerts at 50%, 90%, 100%</li>
  <li><strong>Reservations</strong>: For steady workloads, use flat-rate pricing</li>
</ol>

<p><strong>Budget alert example:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create budget alert at 90% of monthly budget</span>
gcloud billing budgets create <span class="se">\</span>
  <span class="nt">--billing-account</span><span class="o">=</span>YOUR_BILLING_ACCOUNT <span class="se">\</span>
  <span class="nt">--display-name</span><span class="o">=</span><span class="s2">"BigQuery Budget Alert"</span> <span class="se">\</span>
  <span class="nt">--budget-amount</span><span class="o">=</span>1000USD <span class="se">\</span>
  <span class="nt">--threshold-rule</span><span class="o">=</span><span class="nv">percent</span><span class="o">=</span>90 <span class="se">\</span>
  <span class="nt">--threshold-rule</span><span class="o">=</span><span class="nv">percent</span><span class="o">=</span>100 <span class="se">\</span>
  <span class="nt">--filter-projects</span><span class="o">=</span>PROJECT_ID
</code></pre></div></div>

<hr />

<h2 id="pitfall-8-cte-performance-issues">Pitfall 8: CTE Performance Issues</h2>

<p><strong>The Problem:</strong> Using a single CTE in multiple downstream SELECTs can cause multiple executions, multiplying costs.</p>

<p><strong>Why it’s expensive:</strong></p>
<ul>
  <li>Multiple executions: CTE computed once per reference</li>
  <li>No materialization: Results not cached between references</li>
  <li>Higher complexity: Query planner may not optimize well</li>
</ul>

<p><strong>The Fix:</strong> For CTEs used multiple times, materialize to a temp table to avoid repeated execution.</p>

<p><strong>Example:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">--  BAD: CTE used multiple times (may execute multiple times)</span>
<span class="k">WITH</span> <span class="n">expensive_cte</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="n">user_id</span><span class="p">,</span>
    <span class="n">event_type</span><span class="p">,</span>
    <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">event_count</span>
  <span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
  <span class="k">WHERE</span> <span class="n">event_date</span> <span class="k">BETWEEN</span> <span class="s1">'2025-01-01'</span> <span class="k">AND</span> <span class="s1">'2025-01-31'</span>
  <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">event_type</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">user_id</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">event_count</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_events</span>
<span class="k">FROM</span> <span class="n">expensive_cte</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">user_id</span>

<span class="k">UNION</span> <span class="k">ALL</span>

<span class="k">SELECT</span>
  <span class="n">event_type</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">event_count</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_events</span>
<span class="k">FROM</span> <span class="n">expensive_cte</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">event_type</span><span class="p">;</span>

<span class="c1">--  GOOD: Materialize to temp table (executes once)</span>
<span class="k">CREATE</span> <span class="k">TEMP</span> <span class="k">TABLE</span> <span class="n">temp_user_events</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="n">user_id</span><span class="p">,</span>
  <span class="n">event_type</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">event_count</span>
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="k">BETWEEN</span> <span class="s1">'2025-01-01'</span> <span class="k">AND</span> <span class="s1">'2025-01-31'</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">event_type</span><span class="p">;</span>

<span class="k">SELECT</span>
  <span class="n">user_id</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">event_count</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_events</span>
<span class="k">FROM</span> <span class="n">temp_user_events</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">user_id</span>

<span class="k">UNION</span> <span class="k">ALL</span>

<span class="k">SELECT</span>
  <span class="n">event_type</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">event_count</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total_events</span>
<span class="k">FROM</span> <span class="n">temp_user_events</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">event_type</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>When to use temp tables:</strong></p>
<ul>
  <li><strong>CTE used 2+ times</strong> in the same query</li>
  <li><strong>Complex CTE</strong> (joins, aggregations, window functions)</li>
  <li><strong>Large intermediate results</strong> (&gt; 1 GB)</li>
</ul>

<p><strong>Best practices:</strong></p>
<ul>
  <li><strong>Use CTEs</strong> for single-use, simple transformations</li>
  <li><strong>Use temp tables</strong> for multi-use, complex transformations</li>
  <li><strong>Monitor query execution</strong> to identify CTE performance issues</li>
</ul>

<p><strong>Quick win SQL</strong> (cap scan size &amp; prove impact)</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">DECLARE</span> <span class="n">max_bytes</span> <span class="n">INT64</span> <span class="k">DEFAULT</span> <span class="mi">10000000000</span><span class="p">;</span> <span class="c1">-- 10 GB</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">fact_events</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="k">BETWEEN</span> <span class="o">@</span><span class="k">start</span> <span class="k">AND</span> <span class="o">@</span><span class="k">end</span>
<span class="k">OPTIONS</span> <span class="p">(</span><span class="n">max_bytes_billed</span> <span class="o">=</span> <span class="n">max_bytes</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>Outcome:</strong> predictable cost, faster queries, cleaner analytics. Start with a 2-week readiness check, then a 4-week blueprint.</p>

<hr />

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Terraform Guardrails that Save Real Money (and Incidents)</title>
					
					<link>https://atabakkh.github.io/finops/2024/12/21/terraform-guardrails-finops.html</link>

					

					<pubDate>Sat, 21 Dec 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/finops/2024/12/21/terraform-guardrails-finops</guid>

					
						<category><![CDATA[ FinOps ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Takeaway:</strong> a handful of Terraform patterns prevent <strong>surprise spend</strong>, reduce <strong>pager incidents</strong>, and make <strong>audits</strong> easy.</p>

<hr />

<h2 id="0-foundations-state-versions-naming">0) Foundations (state, versions, naming)</h2>

<p><strong>Remote state + locking</strong></p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"&gt;= 1.6.0"</span>
  <span class="nx">backend</span> <span class="s2">"gcs"</span> <span class="p">{</span>
    <span class="nx">bucket</span> <span class="p">=</span> <span class="s2">"tf-state-prod"</span>
    <span class="nx">prefix</span> <span class="p">=</span> <span class="s2">"platform"</span>
  <span class="p">}</span>
  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">google</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">source</span> <span class="p">=</span> <span class="s2">"hashicorp/google"</span><span class="err">,</span> <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 5.30"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Naming &amp; labels</strong></p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"api"</span>
  <span class="nx">env</span>    <span class="p">=</span> <span class="s2">"prod"</span>
  <span class="nx">labels</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">owner</span> <span class="p">=</span> <span class="s2">"platform"</span><span class="err">,</span> <span class="nx">env</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">env</span><span class="err">,</span> <span class="nx">costcenter</span> <span class="p">=</span> <span class="s2">"fintech"</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="1-default-labels--cost-centers-propagate-everywhere">1) Default labels &amp; cost centers (propagate everywhere)</h2>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"labels"</span> <span class="p">{</span>
  <span class="nx">type</span>    <span class="p">=</span> <span class="nx">map</span><span class="err">(</span><span class="nx">string</span><span class="err">)</span>
  <span class="nx">default</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">owner</span> <span class="p">=</span> <span class="s2">"platform"</span><span class="err">,</span> <span class="nx">env</span> <span class="p">=</span> <span class="s2">"prod"</span><span class="err">,</span> <span class="nx">costcenter</span> <span class="p">=</span> <span class="s2">"fintech"</span> <span class="p">}</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"google_compute_instance"</span> <span class="s2">"api"</span> <span class="p">{</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"api-prod-1"</span>
  <span class="c1"># ...</span>
  <span class="nx">labels</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">labels</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>Make <code class="language-plaintext highlighter-rouge">labels</code> a module input and <strong>require</strong> it for all resources. It unlocks per-team <strong>cost allocation</strong> and better budgets.</p>
</blockquote>

<hr />

<h2 id="2-budgets--alerts-gcp---tie-to-pubsub--email--slack">2) Budgets &amp; alerts (GCP) - tie to Pub/Sub / email / Slack</h2>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"google_billing_budget"</span> <span class="s2">"prod"</span> <span class="p">{</span>
  <span class="nx">amount</span> <span class="p">{</span> <span class="nx">specified_amount</span> <span class="p">{</span> <span class="nx">units</span> <span class="p">=</span> <span class="s2">"20000"</span> <span class="p">}</span> <span class="p">}</span> <span class="c1"># €20k/month</span>
  <span class="nx">threshold_rules</span> <span class="p">{</span> <span class="nx">threshold_percent</span> <span class="p">=</span> <span class="mf">0.5</span> <span class="p">}</span>
  <span class="nx">threshold_rules</span> <span class="p">{</span> <span class="nx">threshold_percent</span> <span class="p">=</span> <span class="mf">0.9</span> <span class="p">}</span>
  <span class="nx">all_updates_rule</span> <span class="p">{</span>
    <span class="nx">pubsub_topic</span>           <span class="p">=</span> <span class="nx">google_pubsub_topic</span><span class="err">.</span><span class="nx">budget</span><span class="err">.</span><span class="nx">id</span>
    <span class="nx">schema_version</span>         <span class="p">=</span> <span class="s2">"1.0"</span>
    <span class="nx">monitoring_notification_channels</span> <span class="p">=</span> <span class="p">[</span><span class="nx">google_monitoring_notification_channel</span><span class="err">.</span><span class="nx">email</span><span class="err">.</span><span class="nx">id</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>Route Pub/Sub to Cloud Functions/Run -&gt; Slack. Add <strong>per-label</strong> budgets for big spenders (datasets, projects).</p>
</blockquote>

<hr />

<h2 id="3-storage-lifecycle-bigquerygcs---ttls-and-partition-discipline">3) Storage lifecycle (BigQuery/GCS) - TTLs and partition discipline</h2>

<p><strong>BigQuery dataset defaults</strong></p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"google_bigquery_dataset"</span> <span class="s2">"logs"</span> <span class="p">{</span>
  <span class="nx">dataset_id</span>                  <span class="p">=</span> <span class="s2">"logs"</span>
  <span class="nx">default_table_expiration_ms</span> <span class="p">=</span> <span class="mi">30</span> <span class="err">*</span> <span class="mi">24</span> <span class="err">*</span> <span class="mi">60</span> <span class="err">*</span> <span class="mi">60</span> <span class="err">*</span> <span class="mi">1000</span>  <span class="c1"># 30d</span>
  <span class="nx">default_partition_expiration_ms</span> <span class="p">=</span> <span class="mi">30</span> <span class="err">*</span> <span class="mi">24</span> <span class="err">*</span> <span class="mi">60</span> <span class="err">*</span> <span class="mi">60</span> <span class="err">*</span> <span class="mi">1000</span>
  <span class="nx">labels</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">labels</span>
  <span class="c1"># Important: force partition filters for cost control</span>
  <span class="nx">default_encryption_configuration</span> <span class="p">{</span> <span class="nx">kms_key_name</span> <span class="p">=</span> <span class="nx">google_kms_crypto_key</span><span class="err">.</span><span class="nx">bq</span><span class="err">.</span><span class="nx">id</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Require partition filter at table level</strong></p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"google_bigquery_table"</span> <span class="s2">"logs"</span> <span class="p">{</span>
  <span class="nx">dataset_id</span> <span class="p">=</span> <span class="nx">google_bigquery_dataset</span><span class="err">.</span><span class="nx">logs</span><span class="err">.</span><span class="nx">dataset_id</span>
  <span class="nx">table_id</span>   <span class="p">=</span> <span class="s2">"http_access"</span>
  <span class="nx">time_partitioning</span> <span class="p">{</span> <span class="nx">type</span> <span class="p">=</span> <span class="s2">"DAY"</span> <span class="p">}</span>
  <span class="nx">require_partition_filter</span> <span class="p">=</span> <span class="kc">true</span>
  <span class="nx">labels</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">labels</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>GCS lifecycle</strong></p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"google_storage_bucket"</span> <span class="s2">"logs"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="p">=</span> <span class="s2">"logs-${local.env}"</span>
  <span class="nx">location</span> <span class="p">=</span> <span class="s2">"EU"</span>
  <span class="nx">versioning</span> <span class="p">{</span> <span class="nx">enabled</span> <span class="p">=</span> <span class="kc">true</span> <span class="p">}</span>
  <span class="nx">lifecycle_rule</span> <span class="p">{</span>
    <span class="nx">condition</span> <span class="p">{</span> <span class="nx">age</span> <span class="p">=</span> <span class="mi">30</span> <span class="p">}</span>
    <span class="nx">action</span>    <span class="p">{</span> <span class="nx">type</span> <span class="p">=</span> <span class="s2">"Delete"</span> <span class="p">}</span>
  <span class="p">}</span>
  <span class="nx">labels</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">labels</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="4-org-policies--iam-boundaries---stop-expensive-mistakes-up-front">4) Org policies &amp; IAM boundaries - stop expensive mistakes up front</h2>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"google_org_policy_policy"</span> <span class="s2">"disable_serial_ports"</span> <span class="p">{</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"organizations/${var.org_id}/policies/compute.disableSerialPortAccess"</span>
  <span class="nx">parent</span> <span class="p">=</span> <span class="s2">"organizations/${var.org_id}"</span>
  <span class="nx">spec</span> <span class="p">{</span>
    <span class="nx">rules</span> <span class="p">{</span> <span class="nx">enforce</span> <span class="p">=</span> <span class="kc">true</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"google_org_policy_policy"</span> <span class="s2">"vm_external_ip_denied"</span> <span class="p">{</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"organizations/${var.org_id}/policies/compute.vmExternalIpAccess"</span>
  <span class="nx">parent</span> <span class="p">=</span> <span class="s2">"organizations/${var.org_id}"</span>
  <span class="nx">spec</span> <span class="p">{</span> <span class="nx">rules</span> <span class="p">{</span> <span class="nx">deny_all</span> <span class="p">=</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">}</span> <span class="c1"># enforce private-by-default; use exceptions per project</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>Add policies for <strong>uniform bucket-level access</strong>, CMEK required, <strong>restrict egress</strong> locations, and <strong>allowed machine types</strong> to prevent oversized SKUs.</p>
</blockquote>

<hr />

<h2 id="5-cmek-customer-managed-keys---encryption-defaults">5) CMEK (customer-managed keys) - encryption defaults</h2>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"google_kms_key_ring"</span> <span class="s2">"platform"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="p">=</span> <span class="s2">"platform"</span>
  <span class="nx">location</span> <span class="p">=</span> <span class="s2">"europe-west3"</span>
<span class="p">}</span>
<span class="nx">resource</span> <span class="s2">"google_kms_crypto_key"</span> <span class="s2">"bq"</span> <span class="p">{</span>
  <span class="nx">name</span>            <span class="p">=</span> <span class="s2">"bq-default"</span>
  <span class="nx">key_ring</span>        <span class="p">=</span> <span class="nx">google_kms_key_ring</span><span class="err">.</span><span class="nx">platform</span><span class="err">.</span><span class="nx">id</span>
  <span class="nx">rotation_period</span> <span class="p">=</span> <span class="s2">"7776000s"</span> <span class="c1"># 90 days</span>
<span class="p">}</span>
<span class="c1"># Use in BigQuery/GCS/Disks as defaults (see dataset example above)</span>
</code></pre></div></div>

<hr />

<h2 id="6-p95-based-autoscaling-k8s---scale-on-user-pain-not-cpu">6) p95-based autoscaling (K8s) - scale on user pain, not CPU</h2>

<p>Export <code class="language-plaintext highlighter-rouge">http_p95_latency_ms</code> (OTel/Datadog -&gt; Prom -&gt; custom metric). Drive HPA off <strong>latency/queue depth</strong>, not CPU.</p>

<p><strong>HPA sketch</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">autoscaling/v2</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">HorizontalPodAutoscaler</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">api</span> <span class="pi">}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">apiVersion</span><span class="pi">:</span> <span class="nv">apps/v1</span><span class="pi">,</span> <span class="nv">kind</span><span class="pi">:</span> <span class="nv">Deployment</span><span class="pi">,</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">api</span> <span class="pi">}</span>
  <span class="na">minReplicas</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">maxReplicas</span><span class="pi">:</span> <span class="m">20</span>
  <span class="na">behavior</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">scaleDown</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">stabilizationWindowSeconds</span><span class="pi">:</span> <span class="nv">300</span> <span class="pi">}</span> <span class="pi">}</span>
  <span class="na">metrics</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Pods</span>
    <span class="na">pods</span><span class="pi">:</span>
      <span class="na">metric</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">http_p95_latency_ms</span> <span class="pi">}</span>
      <span class="na">target</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">type</span><span class="pi">:</span> <span class="nv">AverageValue</span><span class="pi">,</span> <span class="nv">averageValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">250"</span> <span class="pi">}</span> <span class="c1"># ms SLO</span>
</code></pre></div></div>

<p><strong>KEDA for queue lag</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">keda.sh/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ScaledObject</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">worker-queue</span> <span class="pi">}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">kind</span><span class="pi">:</span> <span class="nv">Deployment</span><span class="pi">,</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">worker</span> <span class="pi">}</span>
  <span class="na">triggers</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">prometheus</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">serverAddress</span><span class="pi">:</span> <span class="s">http://prometheus:9090</span>
      <span class="na">metricName</span><span class="pi">:</span> <span class="s">queue_lag</span>
      <span class="na">threshold</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1000"</span>
</code></pre></div></div>

<blockquote>
  <p>You can manage HPA/KEDA via Terraform’s <code class="language-plaintext highlighter-rouge">kubernetes_manifest</code> provider if you prefer IaC ownership.</p>
</blockquote>

<hr />

<h2 id="7-kill-switches-for-retry-storms-operational-safety">7) Kill-switches for retry storms (operational safety)</h2>

<ul>
  <li><strong>Config flag</strong> to disable retries or lower concurrency.</li>
  <li>Wire as a <strong>ConfigMap/Secret</strong> that Terraform can set for incident mode.</li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ConfigMap</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">ops-flags</span> <span class="pi">}</span>
<span class="na">data</span><span class="pi">:</span>
  <span class="na">DISABLE_RETRY</span><span class="pi">:</span> <span class="s2">"</span><span class="s">false"</span>
  <span class="na">MAX_CONCURRENCY</span><span class="pi">:</span> <span class="s2">"</span><span class="s">8"</span>
</code></pre></div></div>

<blockquote>
  <p>Your worker reads these at runtime; toggling reduces cascading failures and <strong>cost runaways</strong> during incidents.</p>
</blockquote>

<hr />

<h2 id="8-scheduled-savings-non-prod-off-hours">8) Scheduled savings (non-prod off-hours)</h2>

<p>Turn down non-prod nights/weekends.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"google_cloud_scheduler_job"</span> <span class="s2">"stop_nonprod"</span> <span class="p">{</span>
  <span class="nx">name</span>        <span class="p">=</span> <span class="s2">"stop-nonprod-evening"</span>
  <span class="nx">schedule</span>    <span class="p">=</span> <span class="s2">"0 20 * * 1-5"</span> <span class="c1"># 20:00 Mon-Fri</span>
  <span class="nx">http_target</span> <span class="p">{</span>
    <span class="nx">uri</span>         <span class="p">=</span> <span class="nx">google_cloud_run_service</span><span class="err">.</span><span class="nx">ops_hook</span><span class="err">.</span><span class="nx">status</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="err">.</span><span class="nx">url</span>
    <span class="nx">http_method</span> <span class="p">=</span> <span class="s2">"POST"</span>
    <span class="nx">oidc_token</span> <span class="p">{</span> <span class="nx">service_account_email</span> <span class="p">=</span> <span class="nx">google_service_account</span><span class="err">.</span><span class="nx">ops</span><span class="err">.</span><span class="nx">email</span> <span class="p">}</span>
    <span class="nx">body</span>        <span class="p">=</span> <span class="nx">base64encode</span><span class="err">(</span><span class="s2">"{</span><span class="se">\"</span><span class="s2">action</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">scale_down</span><span class="se">\"</span><span class="s2">}"</span><span class="err">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="9-monitoring-policies-slo--cost">9) Monitoring policies (SLO &amp; cost)</h2>

<p><strong>Spend anomaly alert (Monitoring)</strong></p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"google_monitoring_alert_policy"</span> <span class="s2">"cost_anomaly"</span> <span class="p">{</span>
  <span class="nx">display_name</span> <span class="p">=</span> <span class="s2">"Cost Anomaly Alert"</span>
  <span class="nx">conditions</span> <span class="p">{</span>
    <span class="nx">display_name</span> <span class="p">=</span> <span class="s2">"Daily cost &gt; baseline * 1.5"</span>
    <span class="nx">condition_monitoring_query_language</span> <span class="p">{</span>
      <span class="nx">query</span> <span class="p">=</span> <span class="s2">"fetch billing | metric 'billing.googleapis.com/daily_cost' | condition ratio &gt; 1.5"</span>
      <span class="nx">duration</span> <span class="p">=</span> <span class="s2">"1800s"</span>
      <span class="nx">trigger</span> <span class="p">{</span> <span class="nx">count</span> <span class="p">=</span> <span class="mi">1</span> <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="nx">notification_channels</span> <span class="p">=</span> <span class="p">[</span><span class="nx">google_monitoring_notification_channel</span><span class="err">.</span><span class="nx">email</span><span class="err">.</span><span class="nx">id</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Pair this with SLO <strong>burn-rate alerts</strong> (availability &amp; latency) so you’re paging on <strong>user pain</strong> and watching spend.</p>

<hr />

<h2 id="10-policy-as-code-in-ci-prevent-bad-merges">10) Policy-as-code in CI (prevent bad merges)</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">terraform fmt -check</code> / <code class="language-plaintext highlighter-rouge">validate</code> / <code class="language-plaintext highlighter-rouge">plan</code></li>
  <li><strong>Infracost</strong> to show <strong>€ delta</strong> in PRs</li>
  <li>OPA/Conftest/Sentinel to block policies (no external IPs, TTL required, labels required)</li>
</ul>

<p><strong>GitHub Actions sketch</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Terraform Validate</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">terraform validate</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Infracost</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">infracost/actions/setup@v2</span>
<span class="c1"># ... compute &amp; post diff as PR comment</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Conftest (OPA)</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">conftest test policy/ --input terraform.plan.json</span>
</code></pre></div></div>

<hr />

<h2 id="11-drift-detection--prevent_destroy">11) Drift detection &amp; <code class="language-plaintext highlighter-rouge">prevent_destroy</code></h2>

<p><strong>Detect drift regularly</strong>; and protect critical resources.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"google_bigquery_dataset"</span> <span class="s2">"core"</span> <span class="p">{</span>
  <span class="nx">dataset_id</span> <span class="p">=</span> <span class="s2">"core"</span>
  <span class="c1"># ...</span>
  <span class="nx">lifecycle</span> <span class="p">{</span>
    <span class="nx">prevent_destroy</span> <span class="p">=</span> <span class="kc">true</span>
    <span class="nx">ignore_changes</span>  <span class="p">=</span> <span class="p">[</span><span class="nx">labels</span><span class="p">]</span> <span class="c1"># if labels mutate out-of-band</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="12-environment-separation-blast-radius">12) Environment separation (blast radius)</h2>

<ul>
  <li>Separate <strong>projects</strong> per env (<code class="language-plaintext highlighter-rouge">proj-nonprod</code>, <code class="language-plaintext highlighter-rouge">proj-prod</code>).</li>
  <li>Separate <strong>state</strong> per env/team.</li>
  <li>Use <strong>service perimeters</strong> (VPC-SC) for sensitive data projects.</li>
</ul>

<hr />

<h2 id="13-optional-reservations--caps-bigquery-gke">13) Optional: reservations &amp; caps (BigQuery, GKE)</h2>

<ul>
  <li><strong>BigQuery Reservations</strong> for predictable cost; assign by label/project.</li>
  <li><strong>GKE Autopilot</strong> with <strong>max nodes</strong> per node pool; cap cluster scale to avoid budget blow-outs.</li>
</ul>

<hr />

<h2 id="14-documentation-block-per-module">14) Documentation block (per module)</h2>

<p>Each module README should state:</p>

<ul>
  <li><strong>Labels required</strong>, <strong>CMEK used</strong>, <strong>Org policies assumed</strong></li>
  <li><strong>SLO/Alert hooks</strong> (what metrics/alerts are provided)</li>
  <li><strong>Cost levers</strong> (TTL, partition filters, SKU size)</li>
</ul>

<hr />

<h2 id="recap-outcome">Recap outcome</h2>

<ul>
  <li><strong>20–30% cost</strong> reduction via TTLs, partition filters, right-sizing, off-hours.</li>
  <li><strong>Fewer incidents</strong> with p95-driven scaling, retry kill-switches, and org policies.</li>
  <li><strong>Audit ease</strong> thanks to labels, CMEK, budgets, and policy-as-code.</li>
</ul>

<hr />

<h3 id="minimal-checklist-print--enforce">Minimal checklist (print &amp; enforce)</h3>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Remote state + provider versions pinned</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Labels enforced on <strong>all</strong> resources</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Budgets + cost anomaly alerts wired</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />BigQuery <strong>require_partition_filter</strong> + TTLs</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Org policies: external IPs off (default), CMEK required</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />p95/queue-based autoscaling; HPA/KEDA in IaC</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Retry kill-switches; runbook link from alerts</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Non-prod schedules; drift detection; <code class="language-plaintext highlighter-rouge">prevent_destroy</code></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />CI: validate/plan + Infracost + OPA policy checks</li>
</ul>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Data Contracts: The Missing Agreement Between Engineers and Business</title>
					
					<link>https://atabakkh.github.io/cloud/2024/11/04/data-contracts-missing-agreement.html</link>

					

					<pubDate>Mon, 04 Nov 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2024/11/04/data-contracts-missing-agreement</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>Engineers need stable schemas. Business needs trustworthy definitions. Without something written down, both sides guess - and every dashboard, migration, and AI pilot pays for it.</em></p>

<p><strong>Context:</strong> By late 2024 the “data mesh” wave had cooled into something more useful in the programs I saw: domain ownership and explicit interfaces. Data contracts became the artifact I kept pushing for - smaller than a mesh program, more real than a Slack message saying “don’t break my table”.</p>

<hr />

<h2 id="what-i-mean-by-a-contract">What I mean by a contract</h2>

<p>A published agreement for a dataset or stream:</p>

<ul>
  <li>Schema - fields, types, meaning</li>
  <li>SLAs - freshness, availability</li>
  <li>Quality rules you can measure</li>
  <li>Ownership - who decides changes</li>
  <li>Change policy - how breaking changes are handled</li>
</ul>

<p>It is not a 40-page legal doc. It is not a wiki page nobody reads. It is closer to an API spec for data.</p>

<p><img src="/img/diagrams/data-contract-loop.svg" alt="Producer, contract, and consumer loop for data contracts" width="750" /></p>

<table>
  <thead>
    <tr>
      <th>Piece</th>
      <th>What it answers</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Schema</td>
      <td>What is in the table, and what the fields mean</td>
    </tr>
    <tr>
      <td>Freshness SLA</td>
      <td>How late can it be before consumers should not trust it</td>
    </tr>
    <tr>
      <td>Quality rules</td>
      <td>Which checks fail the pipeline</td>
    </tr>
    <tr>
      <td>Owners</td>
      <td>Who decides a breaking change</td>
    </tr>
    <tr>
      <td>Change policy</td>
      <td>How much notice, and where it is announced</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="why-it-mattered-more-in-2024">Why it mattered more in 2024</h2>

<p>Without contracts, every consumer reimplements the same joins, builds a private “fix” layer that drifts, and blames upstream only after an incident.</p>

<p>With GenAI and early copilots reading tables and documents, the cost went up. Automated consumers do not have the human intuition for “this looks wrong”.</p>

<hr />

<h2 id="a-minimal-example-we-used-as-a-starting-point">A minimal example we used as a starting point</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">curated.orders</span>
<span class="na">version</span><span class="pi">:</span> <span class="s">2.1.0</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">One row per completed order; excludes cancelled before payment</span>
<span class="na">owners</span><span class="pi">:</span>
  <span class="na">business</span><span class="pi">:</span> <span class="s">order-management</span>
  <span class="na">technical</span><span class="pi">:</span> <span class="s">data-platform-core</span>
<span class="na">schedule</span><span class="pi">:</span> <span class="s">hourly at :05</span>
<span class="na">freshness</span><span class="pi">:</span>
  <span class="na">max_lag_minutes</span><span class="pi">:</span> <span class="m">75</span>
  <span class="na">measured_on</span><span class="pi">:</span> <span class="s">_loaded_at</span>
<span class="na">schema</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_id</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">STRING</span>
    <span class="na">constraints</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">unique</span><span class="pi">,</span> <span class="nv">not_null</span><span class="pi">]</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">customer_id</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">STRING</span>
    <span class="na">constraints</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">not_null</span><span class="pi">,</span> <span class="nv">fk</span><span class="pi">:</span><span class="nv">curated.customers</span><span class="pi">]</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">revenue_eur</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">NUMERIC</span>
    <span class="na">semantics</span><span class="pi">:</span> <span class="s">net of VAT; matches finance definition v4</span>
<span class="na">quality_checks</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">revenue_reconciliation</span>
    <span class="na">rule</span><span class="pi">:</span> <span class="s">sum(revenue_eur) within 0.5% of finance.daily_orders</span>
<span class="na">change_policy</span><span class="pi">:</span>
  <span class="na">breaking</span><span class="pi">:</span> <span class="s">14 business days notice</span>
  <span class="na">channel</span><span class="pi">:</span> <span class="s2">"</span><span class="s">#data-contracts"</span>
<span class="na">consumers</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">bi_revenue_dashboard</span>
  <span class="pi">-</span> <span class="s">fraud_scoring_v3</span>
</code></pre></div></div>

<p>Store it in git. Validate in CI. Surface violations where owners already look.</p>

<p>A tiny Python check I used in personal labs to fail a pipeline when the contract freshness lag blows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># scripts/check_contract_freshness.py  (illustrative PoC)
</span><span class="kn">from</span> <span class="nn">google.cloud</span> <span class="kn">import</span> <span class="n">bigquery</span>

<span class="n">DATASET</span><span class="p">,</span> <span class="n">MAX_LAG_MIN</span> <span class="o">=</span> <span class="s">"curated.orders"</span><span class="p">,</span> <span class="mi">75</span>

<span class="n">client</span> <span class="o">=</span> <span class="n">bigquery</span><span class="p">.</span><span class="n">Client</span><span class="p">()</span>
<span class="n">lag</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">client</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="sa">f</span><span class="s">"""
  SELECT TIMESTAMP_DIFF(CURRENT_TIMESTAMP(), MAX(_loaded_at), MINUTE) AS lag
  FROM `</span><span class="si">{</span><span class="n">DATASET</span><span class="si">}</span><span class="s">`
"""</span><span class="p">).</span><span class="n">result</span><span class="p">())[</span><span class="mi">0</span><span class="p">][</span><span class="s">"lag"</span><span class="p">]</span>

<span class="k">if</span> <span class="n">lag</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="n">lag</span> <span class="o">&gt;</span> <span class="n">MAX_LAG_MIN</span><span class="p">:</span>
    <span class="k">raise</span> <span class="nb">SystemExit</span><span class="p">(</span><span class="sa">f</span><span class="s">"freshness contract failed: lag=</span><span class="si">{</span><span class="n">lag</span><span class="si">}</span><span class="s"> min (max </span><span class="si">{</span><span class="n">MAX_LAG_MIN</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"ok: lag=</span><span class="si">{</span><span class="n">lag</span><span class="si">}</span><span class="s"> min"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="enforcement-without-freezing-the-org">Enforcement without freezing the org</h2>

<p><strong>Producers:</strong> CI on schema versus contract, quality tests on each run, and semantic versioning (patch = compatible fixes, minor = backward-compatible additions, major = breaking changes).</p>

<p><strong>Consumers:</strong> pin to a version, subscribe to change notes, test against fixtures.</p>

<p><strong>Platform:</strong> a registry (even a folder in git), violation dashboard, escalate when SLAs breach twice in a week.</p>

<p>We usually started with five critical datasets, not the whole lake.</p>

<hr />

<h2 id="objections-i-heard">Objections I heard</h2>

<p>“We move too fast for contracts.” You already move fast - you pay in firefighting. Contracts reduce rework; they do not forbid change.</p>

<p>“Business will not read YAML.” They understand “revenue matches finance” and “ready by 8am”. Write the meaning in plain language; keep the machine checks in code.</p>

<p>“We already have documentation.” Docs describe intent. Contracts define obligations with tests.</p>

<hr />

<h2 id="during-migrations">During migrations</h2>

<p>During legacy-to-cloud migrations, contracts forced the parity conversation early: which fields were really required, which quirks to keep or fix, and who would sign off when numbers shifted. Consumers knew what “done” meant.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Contracts align engineers and business on what the data promises. They sit between raw pipelines and reliable BI, ML, or copilots. Start small, enforce automatically, expand domain by domain.</p>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Benchmarking Gene Function Prediction: Pitfalls & Fixes</title>
					
					<link>https://atabakkh.github.io/computationalbiology/2024/10/10/benchmarking-go-pitfalls.html</link>

					

					<pubDate>Thu, 10 Oct 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/computationalbiology/2024/10/10/benchmarking-go-pitfalls</guid>

					
						<category><![CDATA[ ComputationalBiology ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Purpose:</strong> A short checklist to avoid inflated or unstable GO results.</p>

<h2 id="pitfalls---fixes">Pitfalls -&gt; Fixes</h2>
<p>1) <strong>Random splits &lt;&gt; real life</strong><br />
   Use <strong>time-based splits</strong>; report T0/T1 explicitly.</p>

<p>2) <strong>Ancestor leakage in labels</strong><br />
   Propagate labels up the DAG in <strong>both</strong> train and eval.</p>

<p>3) <strong>Non-hierarchical inference</strong><br />
   Post-process to enforce ancestor closure or use hierarchical losses.</p>

<p>4) <strong>Cherry-picked metrics</strong><br />
   Always report <strong>Fmax</strong> + <strong>micro/macro-auPRC</strong>, <strong>coverage</strong>, <strong>ECE</strong>.</p>

<p>5) <strong>Long-tail collapse</strong><br />
   Balance classes (weights), evaluate by <strong>IC bins</strong>, and show rare-term PR.</p>

<p>6) <strong>No calibration</strong><br />
   Add isotonic/temperature scaling; include <strong>reliability plots</strong>.</p>

<p>7) <strong>Irreproducible environment</strong><br />
   Pin versions; export <code class="language-plaintext highlighter-rouge">results.json</code>, seeds, and cfgs.</p>

<h2 id="minimal-report-example">Minimal report (example)</h2>
<ul>
  <li>Data snapshot + evidence types</li>
  <li>Time split dates; #proteins (train/val/test)</li>
  <li>Metrics: Fmax (BP/MF/CC), micro/macro-auPRC, coverage, ECE</li>
  <li>Ablations: +homology, +PPI smoothing, +hierarchy closure</li>
</ul>

<p><strong>Rule of thumb:</strong> if someone else can’t re-run your <code class="language-plaintext highlighter-rouge">eval.py</code> and get the same <code class="language-plaintext highlighter-rouge">results.json</code>, the benchmark isn’t done.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Easy Wins with Network Label Smoothing for Function Prediction</title>
					
					<link>https://atabakkh.github.io/computationalbiology/2024/09/25/gnn-label-smoothing.html</link>

					

					<pubDate>Wed, 25 Sep 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/computationalbiology/2024/09/25/gnn-label-smoothing</guid>

					
						<category><![CDATA[ ComputationalBiology ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Idea:</strong> <strong>One</strong> smoothing step over a normalized PPI graph can yield consistent gains before you build a full GNN.</p>

<h2 id="method">Method</h2>
<p>Let <code class="language-plaintext highlighter-rouge">P0 ∈ [0,1]^{NxC}</code> be class probabilities from your sequence model and <code class="language-plaintext highlighter-rouge">A</code> the symmetrically normalized adjacency:</p>

<p>[
P_1 = (1 - \alpha) P_0 + \alpha \, \hat{A} P_0, \quad \alpha \in [0.1,0.3]
]</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">scipy.sparse</span> <span class="kn">import</span> <span class="n">csr_matrix</span>
<span class="n">A</span> <span class="o">=</span> <span class="n">load_ppi_csr</span><span class="p">()</span>                    <span class="c1"># NxN
# symmetric normalization D^{-1/2} A D^{-1/2}
</span><span class="n">deg</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">A</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="mi">1</span><span class="p">))[:,</span><span class="mi">0</span><span class="p">];</span> <span class="n">Dm12</span> <span class="o">=</span> <span class="mf">1.0</span><span class="o">/</span><span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">deg</span><span class="p">,</span><span class="mf">1e-6</span><span class="p">))</span>
<span class="n">A_norm</span> <span class="o">=</span> <span class="n">A</span><span class="p">.</span><span class="n">multiply</span><span class="p">(</span><span class="n">Dm12</span><span class="p">).</span><span class="n">T</span><span class="p">.</span><span class="n">multiply</span><span class="p">(</span><span class="n">Dm12</span><span class="p">).</span><span class="n">tocsr</span><span class="p">()</span>

<span class="n">alpha</span> <span class="o">=</span> <span class="mf">0.2</span>
<span class="n">P1</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">alpha</span><span class="p">)</span> <span class="o">*</span> <span class="n">P0</span> <span class="o">+</span> <span class="n">alpha</span> <span class="o">*</span> <span class="n">A_norm</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">P0</span><span class="p">)</span>
<span class="n">P1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">clip</span><span class="p">(</span><span class="n">P1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="practical-notes">Practical notes</h2>

<ul>
  <li><strong>Hubs:</strong> cap degree or use personalized smoothing to reduce bias.</li>
  <li><strong>Disconnected nodes:</strong> fallback to <code class="language-plaintext highlighter-rouge">P0</code>.</li>
  <li><strong>Calibration:</strong> re-calibrate after smoothing.</li>
</ul>

<h2 id="expected-gains">Expected gains</h2>

<p>Small but robust lifts in <strong>Fmax</strong>/<strong>auPRC</strong>, especially for <strong>mid-frequency</strong> terms. If no gains, inspect graph quality and degree distribution.</p>

<p><strong>When to upgrade:</strong> if smoothing helps, consider <strong>GAT</strong> or edge-weighted GCN with confidence-aware edges.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Bad Data Will Kill More AI Projects Than Bad Models</title>
					
					<link>https://atabakkh.github.io/cloud/2024/08/28/bad-data-kills-ai-projects.html</link>

					

					<pubDate>Wed, 28 Aug 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2024/08/28/bad-data-kills-ai-projects</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>By mid-2024 almost every team had a GenAI demo. The ones that stalled when they touched real data did not fail on prompt engineering. They failed on stale tables, fuzzy definitions, and pipelines that had been broken for weeks without anyone noticing.</em></p>

<p><strong>Context:</strong> After a year of ChatGPT-era experiments, many enterprises hit the same wall. Prototypes used curated samples. Production used the warehouse. The gap was almost always data quality and operational truth - not which model you picked.</p>

<p>This is my read on why the model gets blamed, and what had to exist before GenAI or classical ML work survived contact with real data.</p>

<hr />

<h2 id="why-the-model-gets-blamed">Why the model gets blamed</h2>

<p>Models compress patterns in the inputs. They do not know that <code class="language-plaintext highlighter-rouge">customer_status</code> changed meaning in March, that yesterday’s pipeline failed, that marketing and finance use different revenue definitions, or that 3% of rows are duplicates from a replay bug.</p>

<p>A bad model is consistently wrong. Bad data is confidently wrong. That is worse for trust.</p>

<hr />

<h2 id="failures-i-kept-seeing">Failures I kept seeing</h2>

<p><img src="/img/diagrams/data-quality-before-ai.svg" alt="Data quality path before AI consumes data" width="750" /></p>

<p><strong>Freshness lies.</strong> Dashboard says updated hourly. Metadata says six hours ago. Retrieval or scoring treats it as current.</p>

<p>What helped: freshness targets per dataset, monitored separately from “job succeeded”. Put an <code class="language-plaintext highlighter-rouge">as_of</code> timestamp into whatever context the model or copilot sees.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- example freshness SLI view (lab / notes style)</span>
<span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="n">ops</span><span class="p">.</span><span class="n">dataset_freshness</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="s1">'curated.orders'</span> <span class="k">AS</span> <span class="n">dataset</span><span class="p">,</span>
  <span class="n">TIMESTAMP_DIFF</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="k">MAX</span><span class="p">(</span><span class="n">_loaded_at</span><span class="p">),</span> <span class="k">MINUTE</span><span class="p">)</span> <span class="k">AS</span> <span class="n">lag_minutes</span>
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">orders</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Reconciliation gaps.</strong> Training looks fine. Serving does not. Or product analytics disagree with finance.</p>

<p>What helped: automated checks against an authoritative source. Block downstream AI jobs when variance crosses a threshold.</p>

<p><strong>Schema drift.</strong> New enum, nullable column, JSON shape change - feature logic breaks quietly.</p>

<p>What helped: contract tests at ingest, quarantine path, alert owners before consumers break.</p>

<p><strong>Ownership vacuum.</strong> Pipeline green, data wrong. Nobody owns the definition.</p>

<p>What helped: named business and technical owners on datasets that feed AI or critical reporting.</p>

<p><strong>Scope and consent.</strong> Training or RAG pulling data that should not have been in scope.</p>

<p>What helped: access tags and filters in the infrastructure - not only “please be careful” in the prompt. By late 2024 RAG was already common in enterprise pilots; this mattered.</p>

<hr />

<h2 id="a-minimum-bar-that-actually-helped">A minimum bar that actually helped</h2>

<table>
  <thead>
    <tr>
      <th>Control</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Freshness</td>
      <td><code class="language-plaintext highlighter-rouge">orders_curated</code> lag ≤ 15 min p95</td>
    </tr>
    <tr>
      <td>Completeness</td>
      <td>&lt; 0.1% null on business keys</td>
    </tr>
    <tr>
      <td>Uniqueness</td>
      <td><code class="language-plaintext highlighter-rouge">order_id</code> unique per partition</td>
    </tr>
    <tr>
      <td>Referential integrity</td>
      <td>every <code class="language-plaintext highlighter-rouge">product_id</code> in catalog</td>
    </tr>
    <tr>
      <td>Reconciliation</td>
      <td>daily revenue ±0.5% vs finance</td>
    </tr>
    <tr>
      <td>Lineage</td>
      <td>source -&gt; transform -&gt; feature or index</td>
    </tr>
  </tbody>
</table>

<p>Keep evaluation sets versioned separately from production feeds so you notice drift before users do.</p>

<hr />

<h2 id="contracts-beat-tribal-knowledge">Contracts beat tribal knowledge</h2>

<p>A short published agreement - schema, freshness, quality rules, owners, change policy - stopped AI and BI teams from rebuilding trust from scratch every quarter.</p>

<p>Prompt text like “only use recent data” does nothing if the platform cannot define recent or verified.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>In 2024 the edge was not who had a model. It was who could trust inputs at scale. Freshness, definitions, reconciliation, and ownership had to move before prompt tuning or model swaps.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Protein Language Models for GO: A Hands-on Starter</title>
					
					<link>https://atabakkh.github.io/computationalbiology/2024/08/10/plm-go-embeddings.html</link>

					

					<pubDate>Sat, 10 Aug 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/computationalbiology/2024/08/10/plm-go-embeddings</guid>

					
						<category><![CDATA[ ComputationalBiology ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Takeaway:</strong> Frozen PLM embeddings + linear classifier = <strong>strong, fast baseline</strong> for GO prediction.</p>

<h2 id="workflow">Workflow</h2>
<p>1) Embed proteins (batchable; GPU helpful but not required).
2) Train <strong>one-vs-rest</strong> Logistic Regression (balanced).
3) Calibrate; <strong>close</strong> predictions under GO ancestors.
4) Threshold per class for <strong>Fmax</strong>.</p>

<h3 id="sketch">Sketch</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Load embeddings and labels
</span><span class="n">X_tr</span><span class="p">,</span> <span class="n">Y_tr</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># [N_tr, D], [N_tr, C]
</span><span class="n">X_val</span><span class="p">,</span> <span class="n">Y_val</span> <span class="o">=</span> <span class="p">...</span>

<span class="kn">from</span> <span class="nn">sklearn.linear_model</span> <span class="kn">import</span> <span class="n">LogisticRegression</span>
<span class="kn">from</span> <span class="nn">sklearn.multiclass</span> <span class="kn">import</span> <span class="n">OneVsRestClassifier</span>
<span class="n">clf</span> <span class="o">=</span> <span class="n">OneVsRestClassifier</span><span class="p">(</span><span class="n">LogisticRegression</span><span class="p">(</span><span class="n">max_iter</span><span class="o">=</span><span class="mi">4000</span><span class="p">,</span> <span class="n">class_weight</span><span class="o">=</span><span class="s">"balanced"</span><span class="p">))</span>
<span class="n">clf</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_tr</span><span class="p">,</span> <span class="n">Y_tr</span><span class="p">)</span>

<span class="n">P</span> <span class="o">=</span> <span class="n">clf</span><span class="p">.</span><span class="n">predict_proba</span><span class="p">(</span><span class="n">X_val</span><span class="p">)</span>
<span class="n">P</span> <span class="o">=</span> <span class="n">close_under_ancestors</span><span class="p">(</span><span class="n">P</span><span class="p">,</span> <span class="n">go_dag</span><span class="p">)</span>        <span class="c1"># hierarchy consistency
</span><span class="n">th</span> <span class="o">=</span> <span class="n">tune_thresholds</span><span class="p">(</span><span class="n">P</span><span class="p">,</span> <span class="n">Y_val</span><span class="p">,</span> <span class="n">metric</span><span class="o">=</span><span class="s">"Fmax"</span><span class="p">)</span>
<span class="n">Y_hat</span> <span class="o">=</span> <span class="p">(</span><span class="n">P</span> <span class="o">&gt;=</span> <span class="n">th</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="hyper-parameters">Hyper-parameters</h2>

<ul>
  <li><strong>Dimensionality:</strong> if D&gt;1024, try PCA-&gt;512 for speed.</li>
  <li><strong>Regularization:</strong> <code class="language-plaintext highlighter-rouge">C=1.0</code> (grid: 0.1-10), early stop on val Fmax.</li>
  <li><strong>Class imbalance:</strong> <code class="language-plaintext highlighter-rouge">class_weight="balanced"</code>; consider focal loss for MLP.</li>
</ul>

<h2 id="diagnostics">Diagnostics</h2>

<ul>
  <li>Per-class PR curves (spot long-tail collapse).</li>
  <li>Reliability plot + <strong>ECE</strong>; calibrate if &gt;0.05.</li>
  <li>Ancestor coverage % (should be 100% after closure).</li>
</ul>

<h2 id="when-to-go-beyond-linear">When to go beyond linear</h2>

<ul>
  <li>Plateaued Fmax and clear network-driven biology -&gt; add <strong>PPI smoothing</strong> or a <strong>GNN</strong>.</li>
  <li>Heterogeneous evidence (sequence + structure + text) -&gt; <strong>late-fusion</strong> of logits.</li>
</ul>

<p><strong>Tip:</strong> Keep PLM frozen at first. Fine-tune only with careful regularization and time-split eval.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Privacy-First Cloud Audits in the EU - No PII Needed</title>
					
					<link>https://atabakkh.github.io/cloud/2024/06/15/privacy-first-cloud-audits.html</link>

					

					<pubDate>Sat, 15 Jun 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2024/06/15/privacy-first-cloud-audits</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><strong>Takeaway:</strong> You can cut <strong>cost</strong> and improve <strong>reliability/observability</strong> without any access to PII or raw logs. Here’s the artifact-only method I use.</p>

<h2 id="why-this-works">Why this works</h2>
<p>Most performance/cost failures live in <strong>patterns &amp; policies</strong> (autoscaling, retries, retention) not in user data. So inspection needed for <strong>signals and structure</strong>, not payloads.</p>

<h2 id="what-to-check-as-inputs-no-pii">What to check as inputs (no PII)</h2>
<ul>
  <li><strong>Billing exports</strong> (GCP BigQuery export / AWS CUR)</li>
  <li><strong>IaC</strong> (Terraform modules), <strong>autoscaling &amp; alert rules</strong></li>
  <li>Logging/metrics <strong>schemas</strong> (+ retention), <strong>aggregated</strong> charts: p95/p99, 5xx, queue lag</li>
  <li><strong>Architecture diagrams</strong>, runbooks, incident summaries (redacted)</li>
</ul>

<h2 id="what-can-be-analyzed-examples">What can be analyzed (examples)</h2>
<ul>
  <li><strong>Reliability:</strong> retry storms, queue lag vs consumers, DLQ policy, alert fatigue</li>
  <li><strong>Latency:</strong> p95/p99 tails, cold starts/GC signatures, saturation, query latency logs</li>
  <li><strong>Observability:</strong> missing SLIs/SLOs, noisy alerts, unbounded logs</li>
  <li><strong>Cost:</strong> wrong SKUs, mis-sizing, egress hotspots, over-retention</li>
</ul>

<h2 id="example-aggregated-latency-schema">Example: aggregated latency schema</h2>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>service, endpoint, date, count, p50_ms, p95_ms, p99_ms, error_rate
</code></pre></div></div>

<h3 id="optional-one-possible-way-to-get-rough-p95-from-a-sample-table-in-bigquery-no-payloads">Optional: one possible way to get rough p95 from a sample table in BigQuery (no payloads)</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">SELECT</span>
  <span class="n">service</span><span class="p">,</span> <span class="n">endpoint</span><span class="p">,</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="k">AS</span> <span class="n">d</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">n</span><span class="p">,</span>
  <span class="n">APPROX_QUANTILES</span><span class="p">(</span><span class="n">latency_ms</span><span class="p">,</span> <span class="mi">100</span><span class="p">)[</span><span class="k">OFFSET</span><span class="p">(</span><span class="mi">95</span><span class="p">)]</span> <span class="k">AS</span> <span class="n">p95_ms</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="k">CASE</span> <span class="k">WHEN</span> <span class="n">status</span><span class="o">&gt;=</span><span class="mi">500</span> <span class="k">THEN</span> <span class="mi">1</span> <span class="k">ELSE</span> <span class="mi">0</span> <span class="k">END</span><span class="p">)</span><span class="o">/</span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">error_rate</span>
<span class="k">FROM</span> <span class="n">telemetry_samples</span>
<span class="k">WHERE</span> <span class="n">ts</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">service</span><span class="p">,</span> <span class="n">endpoint</span><span class="p">,</span> <span class="n">d</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="how-we-can-help-you-for-that">How we can help you for that</h2>

<h3 id="security--gdpr-stance">Security &amp; GDPR stance</h3>

<ul>
  <li>Default: <strong>no PII</strong>, <strong>no tenant access</strong>, <strong>no extraction</strong></li>
  <li>EU-only processing, NDA; DPA only if you later add tenant-only read-only</li>
  <li>Notes auto-deleted &lt;= <strong>30 days</strong></li>
</ul>

<h3 id="deliverables-you-get-in-2-weeks">Deliverables you get in 2 weeks</h3>

<ul>
  <li><strong>Top 10 findings</strong> with screenshots/tables (aggregates only)</li>
  <li><strong>3 Day-7 Quick Wins</strong> your team can do immediately</li>
  <li><strong>90-day roadmap</strong> (owner, effort, impact, risk)</li>
  <li>Optional <strong>SLO baseline</strong> + alert/runbook templates</li>
</ul>

<p><strong>Want a 1-page checklist to run this audit internally?</strong> Email me and I’ll send it.</p>

<hr />
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Architecture Decisions Under Pressure - Notes From the Trenches</title>
					
					<link>https://atabakkh.github.io/agile/2024/06/10/architecture-decisions-under-pressure.html</link>

					

					<pubDate>Mon, 10 Jun 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/agile/2024/06/10/architecture-decisions-under-pressure</guid>

					
						<category><![CDATA[ Agile ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>Deadlines do not remove trade-offs. They hide them until production. Under pressure I learned to make trade-offs visible, written down, and reversible when I could.</em></p>

<p><strong>Context:</strong> Mid-2024 I was seeing the same squeeze everywhere: cloud cost pressure, early GenAI prototypes, migration backlog still open. Senior engineers were asked to “just decide”. The decisions that held up were not genius calls. They were disciplined ones managers could defend later.</p>

<hr />

<h2 id="pressure-patterns-i-kept-seeing">Pressure patterns I kept seeing</h2>

<ul>
  <li>Date first, design second - “We announced Q3, make it work”</li>
  <li>Six stakeholders, no decider</li>
  <li>“Ask Sarah, she always knows”</li>
  <li>Vendor recommendation treated as a requirement</li>
  <li>Treating every choice as permanent when half of them were reversible</li>
</ul>

<p>What I learned over a decade in leadership roles is that the job is not just about making decisions. It’s about transforming how the organization makes decisions in ways that enable the business to grow.</p>

<hr />

<h2 id="classify-the-decision-first">Classify the decision first</h2>

<p><img src="/img/diagrams/decision-types-under-pressure.svg" alt="Reversible, irreversible, and crisis decisions under pressure" width="750" /></p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Examples</th>
      <th>How I treated it</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Reversible</td>
      <td>Tooling PoC, schedule tweak, non-prod schema</td>
      <td>Decide fast, time-box, measure</td>
    </tr>
    <tr>
      <td>Irreversible</td>
      <td>Core data model, public API, multi-year vendor</td>
      <td>Slow down, write it down, review</td>
    </tr>
    <tr>
      <td>Crisis</td>
      <td>Outage, compliance breach, cost runaway</td>
      <td>Fix first, redesign later - not at 2am</td>
    </tr>
  </tbody>
</table>

<p>Telling stakeholders which type we were in set expectations better than a long architecture debate.</p>

<hr />

<h2 id="write-a-one-page-decision-record">Write a one-page decision record</h2>

<p>Something like:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Decision: Partition curated.events by date, cluster by user_id</span>
Date: 2024-06-10
Status: Accepted
Context: Most dashboard queries filter by date; ~80% also filter by user.
Options:
<span class="p">  1.</span> No partition - rejected (scan cost)
<span class="p">  2.</span> Partition by user - rejected (too many partitions)
<span class="p">  3.</span> Date + cluster user - selected
Trade-offs: Point lookups by user alone may be slower.
Rollback: Re-cluster or add a serving table in 2 sprints.
Owner: data-platform
Review: 2024-09-10
</code></pre></div></div>

<p>Under pressure, the rollback line is what separates a professional decision from a gamble.</p>

<p>Matching DDL from the same decision (practice note):</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">user_id</span>
<span class="k">AS</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">landing</span><span class="p">.</span><span class="n">raw_events</span>
<span class="k">WHERE</span> <span class="n">event_ts</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">180</span> <span class="k">DAY</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="good-enough-for-now-vs-good-forever">“Good enough for now” vs. “good forever”</h2>

<p>I ask:</p>

<ul>
  <li>What breaks if we are wrong?</li>
  <li>What is the cheapest way to learn?</li>
  <li>What can we not compromise (security, compliance, revenue accuracy)?</li>
</ul>

<p>Script I used with leadership:</p>

<blockquote>
  <p>We can ship serving tables for the top three dashboards in four weeks with acceptable cost. Full domain remodel takes six months. I recommend phase 1 with a written debt item and a review in Q3 - not silent shortcuts.</p>
</blockquote>

<p>Managers need phased honesty, not false certainty.</p>

<hr />

<h2 id="pre-mortem-before-big-cutovers">Pre-mortem before big cutovers</h2>

<p>Thirty minutes before a major launch:</p>

<ul>
  <li>Imagine it is three months later and this failed. Why?</li>
  <li>Capture the top five causes</li>
  <li>Mitigate the preventable ones now</li>
</ul>

<p>Cheap under pressure. Cheaper than a failed cutover.</p>

<hr />

<h2 id="when-i-escalate">When I escalate</h2>

<table>
  <thead>
    <tr>
      <th>Escalate when</th>
      <th>Why</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Risk is bigger than the team’s authority</td>
      <td>Regulatory, brand, major revenue</td>
    </tr>
    <tr>
      <td>Stakeholders disagree on what success means</td>
      <td>You cannot design against two definitions of done</td>
    </tr>
    <tr>
      <td>Timeline forces an irreversible compromise</td>
      <td>Needs a named owner of that compromise</td>
    </tr>
    <tr>
      <td>We lack data and waiting costs more than a wrong guess <em>with rollback</em></td>
      <td>Time-box the experiment</td>
    </tr>
  </tbody>
</table>

<p>I do not escalate every decision. I escalate the ones that stick.</p>

<hr />

<h2 id="what-i-needed-from-managers">What I needed from managers</h2>

<p>Time boxes for two-way doors. Short protected reviews for one-way doors. A decision log somewhere searchable. No punishment when a planned rollback happens - that is discipline, not failure.</p>

<hr />

<h2 id="anti-patterns-i-fell-into-or-watched">Anti-patterns I fell into or watched</h2>

<p>Architecture by anxiety - over-engineering because of deadline stress. Silent coupling that binds teams you never met. Disappearing into code to avoid the hard conversation. Winning the argument and losing trust.</p>

<p>Impact is organizational outcomes, not diagram elegance.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Under pressure: classify, document trade-offs and rollback, phase honestly, pre-mortem, escalate the irreversible ones. Managers make that possible by protecting time for hard choices and rewarding visible risk management - not only speed.</p>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Real-Time Pipelines and AI Prediction on Spark</title>
					
					<link>https://atabakkh.github.io/spark/2024/04/18/realtime-pipelines-ai-prediction-spark.html</link>

					

					<pubDate>Thu, 18 Apr 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/spark/2024/04/18/realtime-pipelines-ai-prediction-spark</guid>

					
						<category><![CDATA[ Spark ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>How to design Spark pipelines for real-time derived data and ML scoring; with the right shuffle, skew, and storage choices. This article use real-time campaign segments as the example throughout.</em></p>

<p><strong>Context:</strong> Many pipelines need <strong>derived views</strong> (e.g. aggregates, segments, attributes) in <strong>near real time</strong> from <strong>raw streams</strong> and <strong>reference data</strong>, plus <strong>AI-driven prediction</strong> (propensity scores, next-best-action, risk) on top. The architect’s job is to make both the derivation pipeline and the prediction pipeline <strong>correct</strong>, <strong>scalable</strong>, and <strong>operable</strong>; which means paying attention to shuffle, skew, storage layout, and how the prediction step fits into the same big data stack. This post explains how we think about that end to end, using <strong>real-time campaign segments</strong> as the running example.</p>

<hr />

<h2 id="end-to-end-shape-raw---curated---campaignssegments---prediction---serving">End-to-end shape: raw -&gt; curated -&gt; campaigns/segments -&gt; prediction -&gt; serving</h2>

<p>From a Spark/big data perspective, the pipeline looks like this:</p>

<ol>
  <li>
    <p><strong>Raw</strong><br />
Transactions and product/SKU data land from streaming sources (e.g. Kafka). A Spark Streaming job (or connector) writes to a <strong>raw layer</strong> partitioned by datetime (and optionally hour/source). No heavy shuffle here; just partition-aligned writes.</p>
  </li>
  <li>
    <p><strong>Curated</strong><br />
A Spark batch (or micro-batch) job reads raw by partition, deduplicates, joins to <strong>taxonomy</strong> (SKU -&gt; category, brand, etc.). Taxonomy is typically small enough to <strong>broadcast</strong>; the main shuffle is on the transaction key (e.g. <code class="language-plaintext highlighter-rouge">user_id</code>, <code class="language-plaintext highlighter-rouge">event_id</code>) for dedupe and join. This is where skew first shows up (see below).</p>
  </li>
  <li>
    <p><strong>campaigns and segments</strong><br />
Business rules (e.g. “category affinity = top N categories by spend in last 90 days”) are implemented as Spark aggregations and window functions. Shuffle is by the grouping key (e.g. <code class="language-plaintext highlighter-rouge">user_id</code>). Output is written to a <strong>curated campaign/segment</strong> store (e.g. Parquet by <code class="language-plaintext highlighter-rouge">dt</code> and optionally <code class="language-plaintext highlighter-rouge">user_id</code> or segment id). Again, skew on a few heavy users can dominate runtime.</p>
  </li>
  <li>
    <p><strong>Prediction (AI/ML)</strong><br />
A <strong>scoring job</strong> (Spark batch or Structured Streaming) reads curated data + campaigns, builds features, runs a model (e.g. MLlib, or a loaded PMML/ONNX), and writes <strong>scores</strong> (propensity, churn risk, next-best-action) to a serving layer. Feature building often involves joins and groupBys -&gt; shuffle; scoring itself can be partition-local if features are pre-joined. Storage layout of the <strong>input</strong> to scoring (e.g. one partition per user or per segment) drives how much shuffle you need.</p>
  </li>
  <li>
    <p><strong>Serving</strong><br />
Downstream systems read from tables or APIs. The architect’s concern is that the <strong>write path</strong> from Spark (partitioning, file size, format) matches how serving reads (e.g. by user, by segment, by date).</p>
  </li>
</ol>

<p>Below we focus on <strong>shuffle</strong>, <strong>skew</strong>, and <strong>storage</strong> in the curated -&gt; campaigns -&gt; prediction path, and where AI prediction fits in a Spark-centric design.</p>

<hr />

<h2 id="shuffle-in-the-campaign-and-segment-pipeline">Shuffle in the campaign and segment pipeline</h2>

<p><strong>Where shuffle happens</strong></p>
<ul>
  <li><strong>Join curated to taxonomy</strong>: If taxonomy is small, <strong>broadcast</strong> it so the big table (transactions) is not shuffled to match it. Only the big side is partitioned; no shuffle for the small side.</li>
  <li><strong>Aggregations for campaigns</strong>: e.g. “spend per user per category in last 90 days” -&gt; <code class="language-plaintext highlighter-rouge">groupBy(user_id, category_id)</code>. Shuffle is by <code class="language-plaintext highlighter-rouge">(user_id, category_id)</code>. Size <code class="language-plaintext highlighter-rouge">spark.sql.shuffle.partitions</code> so that each partition gets a few hundred MB to a few GB; repartition by the key you’ll use for downstream (e.g. <code class="language-plaintext highlighter-rouge">user_id</code>) before writing so that the next job (e.g. scoring) can read by user without reshuffling.</li>
  <li><strong>Window functions</strong>: e.g. “rank categories by spend per user” -&gt; <code class="language-plaintext highlighter-rouge">window.partitionBy("user_id").orderBy(desc("spend"))</code>. This shuffles by <code class="language-plaintext highlighter-rouge">user_id</code>. Again, if a few users have huge history, you get skew (see next section).</li>
  <li><strong>Deduplication</strong>: If you dedupe by <code class="language-plaintext highlighter-rouge">(user_id, date)</code> or similar, shuffle is by that key; skew is possible if some users have many more events.</li>
</ul>

<p><strong>Reducing shuffle</strong></p>
<ul>
  <li><strong>Broadcast</strong> taxonomy and any small lookup tables.</li>
  <li><strong>Partition alignment</strong>: If curated data is partitioned by <code class="language-plaintext highlighter-rouge">dt</code>, and campaign output is by <code class="language-plaintext highlighter-rouge">dt</code> and <code class="language-plaintext highlighter-rouge">user_id</code>, design the job so that it reads one partition (e.g. one day) and writes campaign output for that day. Then the next stage (e.g. scoring) can read “campaigns for dt = X” without shuffling on <code class="language-plaintext highlighter-rouge">dt</code>. Shuffle only on the keys that are necessary for the aggregation (e.g. <code class="language-plaintext highlighter-rouge">user_id</code>).</li>
  <li><strong>Single pass where possible</strong>: Compute multiple campaigns in one pass (e.g. one <code class="language-plaintext highlighter-rouge">groupBy(user_id)</code> with multiple aggregations) instead of multiple jobs that each shuffle by <code class="language-plaintext highlighter-rouge">user_id</code>.</li>
</ul>

<hr />

<h2 id="skew-in-campaigns-and-segments">Skew in campaigns and segments</h2>

<p><strong>Why skew appears</strong></p>
<ul>
  <li><strong>User skew</strong>: A small fraction of users (e.g. bots, power users, test accounts) have orders of magnitude more transactions. When you <code class="language-plaintext highlighter-rouge">groupBy(user_id)</code> or <code class="language-plaintext highlighter-rouge">partitionBy(user_id)</code> for windows, a few partitions get huge.</li>
  <li><strong>Category/product skew</strong>: A few categories or products dominate events; joins or groupBys on category can skew.</li>
  <li><strong>Segment skew</strong>: If you write segments by <code class="language-plaintext highlighter-rouge">segment_id</code>, a few segments (e.g. “all users”, “high value”) can be huge.</li>
</ul>

<p><strong>What we do in Spark</strong></p>
<ul>
  <li><strong>AQE skew join</strong> (Spark 3.x): Enable <code class="language-plaintext highlighter-rouge">spark.sql.adaptive.enabled</code> and <code class="language-plaintext highlighter-rouge">spark.sql.adaptive.skewJoin.enabled</code>. For joins, Adaptive Query Execution (AQE) can split skewed partitions at runtime. For aggregations, AQE can optimize join and coalesce; skew join helps when the skew is in a join.</li>
  <li><strong>Salting for groupBy/window</strong>: If a single <code class="language-plaintext highlighter-rouge">groupBy(user_id)</code> is skewed, salt the key: e.g. <code class="language-plaintext highlighter-rouge">concat(user_id, "_", rand(0, N-1))</code>, aggregate, then aggregate again by <code class="language-plaintext highlighter-rouge">user_id</code> to collapse. That spreads the hot keys across N partitions. Cost: two shuffles and more tasks, but balanced load.</li>
  <li><strong>Split hot and cold</strong>: Identify hot keys (e.g. from a pre-pass or from a list of known power users). Process hot keys in a separate job with salting or broadcast; process the rest in the main job; union. This keeps the main job stable and confines complexity to a small subset.</li>
  <li><strong>Broadcast for “small side” of skew</strong>: If the skewed side is actually a small set (e.g. top 10K users), you can filter them out, join/aggregate the rest, then handle the top 10K with a broadcast or a separate small job.</li>
</ul>

<p>From an architect’s view: <strong>measure</strong> task duration distribution in Spark UI; if you see a long tail, enable AQE first, then add salting or split hot/cold for the stages that still skew.</p>

<hr />

<h2 id="storage-optimization-for-campaigns-segments-and-prediction-output">Storage optimization for campaigns, segments, and prediction output</h2>

<p><strong>Partitioning</strong></p>
<ul>
  <li><strong>Curated transactions</strong>: Partition by <code class="language-plaintext highlighter-rouge">dt</code> (and optionally <code class="language-plaintext highlighter-rouge">hour</code>). Align with how you’ll reprocess (e.g. “recompute last 7 days” = read 7 partition dirs).</li>
  <li><strong>campaign/segment tables</strong>: Partition by <code class="language-plaintext highlighter-rouge">dt</code> so that “campaigns as of date X” is one partition. If serving reads by user, you can still store by <code class="language-plaintext highlighter-rouge">dt</code> and have a compact number of files per partition (see file size below). Avoid partitioning by <code class="language-plaintext highlighter-rouge">user_id</code> (millions of partitions) unless you use a bucketing scheme (e.g. bucket by user_id, 256 buckets, partition by dt).</li>
  <li><strong>Prediction scores</strong>: Same idea; partition by <code class="language-plaintext highlighter-rouge">dt</code> (or run_id) so that “scores for run at time T” is a single partition. Serving can then read the latest partition or merge by key.</li>
</ul>

<p><strong>File size and count</strong></p>
<ul>
  <li><strong>Target file size</strong>: Aim for roughly <strong>128 MB-512 MB</strong> per file. Too many small files -&gt; slow listing and many small reads; one huge file per partition -&gt; poor parallelism. Use <code class="language-plaintext highlighter-rouge">repartition(N)</code> or <code class="language-plaintext highlighter-rouge">coalesce(N)</code> before <code class="language-plaintext highlighter-rouge">write</code> so that each partition (e.g. each <code class="language-plaintext highlighter-rouge">dt</code>) has about N files. N can be chosen so that <code class="language-plaintext highlighter-rouge">partition_size / N</code> is in that range.</li>
  <li><strong>Compaction</strong>: If the campaign or score table is appended to every run (e.g. every 15 minutes), you’ll accumulate many small files. Run a <strong>compaction job</strong> (e.g. daily): read the partition(s), coalesce to a target number of files, overwrite. That keeps read performance stable.</li>
  <li><strong>Overwrite by partition</strong>: For idempotent “refresh campaigns for last N days”, overwrite only those partitions (dynamic partition overwrite where supported). That avoids full-table rewrites and keeps storage predictable.</li>
</ul>

<p><strong>Format</strong></p>
<ul>
  <li><strong>Parquet</strong> for curated, campaigns, and scores: columnar, predicate pushdown (e.g. <code class="language-plaintext highlighter-rouge">filter(dt = '...')</code>), good compression. Use Snappy or Zstd. If the serving layer needs row-level lookups by key (e.g. user_id), you can still store as Parquet and use a key-value cache or a separate index that’s fed from the same Spark output; or use a table format (Delta, Iceberg) for upserts if you need to update scores in place.</li>
</ul>

<hr />

<h2 id="where-ai-based-prediction-fits-spark-view">Where AI-based prediction fits (Spark view)</h2>

<p><strong>Typical flow</strong></p>
<ul>
  <li><strong>Input</strong>: Curated transactions + product taxonomy + <strong>computed campaigns and segments</strong> (all in the same data lake or warehouse, often as Parquet).</li>
  <li><strong>Feature build</strong>: Spark job that joins and aggregates to build a feature table (e.g. one row per user with columns = features). This step can have significant shuffle (e.g. by user_id) and skew; same principles apply: broadcast small tables, AQE, salting or split if needed.</li>
  <li><strong>Model</strong>: Trained offline (e.g. with Spark MLlib, or external Python/R). The trained model (e.g. PMML, ONNX, or native Spark model) is stored in object storage or a registry.</li>
  <li><strong>Scoring</strong>: Spark batch job (or micro-batch) loads the model, reads the feature table (partitioned by dt or user_id), and applies the model. If the feature table is already partitioned by user (or by a key that matches the model’s expectation), scoring can be <strong>partition-local</strong> (no shuffle): each task reads a chunk of the feature table and writes a chunk of scores. That scales well.</li>
  <li><strong>Output</strong>: Scores (e.g. <code class="language-plaintext highlighter-rouge">propensity_buy_X</code>, <code class="language-plaintext highlighter-rouge">churn_risk</code>, <code class="language-plaintext highlighter-rouge">next_best_action</code>) are written to a table (e.g. Parquet by <code class="language-plaintext highlighter-rouge">dt</code>) or to a key-value store for low-latency serving. Writing is again optimized by repartitioning by the table partition key and controlling file size.</li>
</ul>

<p><strong>Freshness vs. cost</strong></p>
<ul>
  <li><strong>Micro-batch scoring</strong>: e.g. every 5-15 minutes, build features for “users with new activity or in campaign scope”, score them, update the serving layer. This keeps scores fresh without scoring the full user base every time. Shuffle is limited to the feature-build step; scoring itself can be a narrow read of the feature table and a partition-local transform.</li>
  <li><strong>Full refresh</strong>: Periodically (e.g. nightly), run a full feature build + score for all users so that the serving layer has a consistent snapshot. Same storage and shuffle discipline: partition by dt, control file size, compact if needed.</li>
</ul>

<p><strong>Governance</strong></p>
<ul>
  <li>Track <strong>model version</strong> and <strong>training data window</strong> (e.g. which dt range was used for training). Store them in the score output or in a separate metadata table so that downstream and ops know what they’re using. Document which campaigns are rule-based vs. model outputs so that the pipeline remains auditable.</li>
</ul>

<hr />

<h2 id="late-events-and-reprocessing">Late events and reprocessing</h2>

<p>campaigns and segments often depend on <strong>event-time</strong> (e.g. “last 30 days”). Events can arrive late; taxonomy can be corrected.</p>

<ul>
  <li><strong>Streaming</strong>: Use event-time and watermarking so that late events within the watermark are included; beyond that, drop or send to a side output for batch backfill.</li>
  <li><strong>Batch</strong>: Reprocess raw partitions for the affected window (e.g. last 24-48 hours), recompute curated and campaigns, then <strong>overwrite</strong> the corresponding campaign/segment partitions. Idempotent and consistent if keys and logic are deterministic. Same shuffle/skew/storage considerations as the main pipeline.</li>
</ul>

<hr />

<h2 id="summary-architects-view">Summary: architect’s view</h2>

<p>From a <strong>data solution architect</strong> perspective, a real-time campaign and segment pipeline with AI prediction on Spark rests on:</p>

<ul>
  <li><strong>Clear data layout</strong>: Raw and curated partitioned by <code class="language-plaintext highlighter-rouge">dt</code> (and optional hour/source); campaign and score tables partitioned by <code class="language-plaintext highlighter-rouge">dt</code> (or run_id); taxonomy broadcast so that joins don’t shuffle the small side.</li>
  <li><strong>Shuffle discipline</strong>: Broadcast small tables; size <code class="language-plaintext highlighter-rouge">spark.sql.shuffle.partitions</code>; repartition by the table partition key before write; single-pass aggregations where possible; feature-build job designed so that scoring can be partition-local.</li>
  <li><strong>Skew handling</strong>: AQE skew join and adaptive coalesce; salting or split hot/cold for groupBy/window/join stages that show skew in Spark UI.</li>
  <li><strong>Storage</strong>: Parquet, 128-512 MB target file size, compaction for append-heavy tables, overwrite-by-partition for idempotent reprocessing and merges.</li>
  <li><strong>Prediction</strong>: Feature build as a Spark job (with the same shuffle/skew/storage care); scoring as a partition-local read + model apply + partitioned write; micro-batch for freshness, full refresh for consistency; model and training metadata tracked for governance.</li>
</ul>

<p>Treating the campaign pipeline and the prediction pipeline as <strong>one big data system</strong>; with consistent partitioning, controlled shuffle, explicit skew handling, and deliberate storage layout; is what makes the system scalable and operable at production scale. The same principles apply whether the downstream consumer is a campaign engine, a recommendation API, or an internal dashboard.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>What Really Breaks When You Move a Legacy Data Platform to Cloud</title>
					
					<link>https://atabakkh.github.io/cloud/2024/01/22/legacy-data-platform-to-cloud-what-breaks.html</link>

					

					<pubDate>Mon, 22 Jan 2024 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2024/01/22/legacy-data-platform-to-cloud-what-breaks</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>Migration plans talk about tools and timelines. What broke after go-live, in my experience, was usually schema drift, partitioning assumptions, batch dependencies, cost models, and expectations nobody wrote down.</em></p>

<p><strong>Context:</strong> Early 2024, many enterprises were still moving away from legacy data warehouses and distributed big-data estates because of licensing costs, scarce skills, and growing analytics demand. The technical cutover was rarely the hardest part. The difficult part was changing behavior accumulated over years: physical layouts, implicit dependencies, security conventions, and reporting expectations.</p>

<p>These are generalized lessons from common migration patterns and personal technical study. The examples are illustrative; they do not describe an employer, client, or specific production system.</p>

<p><img src="/img/diagrams/legacy-data-platform-what-breaks.svg" alt="Five things that commonly break after a legacy data-platform migration" width="750" /></p>

<hr />

<h2 id="what-the-plan-usually-missed">What the plan usually missed</h2>

<h3 id="schema-drift-was-tolerated---until-it-wasnt">Schema drift was tolerated - until it wasn’t</h3>

<p>In a tightly coupled legacy estate, a column rename might break one report and get fixed locally. In a cloud platform, the same change can affect pipelines, semantic models, APIs, and ML features at once.</p>

<p>What helped: checks at ingest, quarantine for bad records, versioned curated tables. Not “we’ll notice”.</p>

<h3 id="partitioning-does-not-port-11">Partitioning does not port 1:1</h3>

<p>Filesystem date folders, database indexes, and engine-specific pruning strategies do not automatically become efficient cloud-warehouse layouts.</p>

<p>What helped: redesign partitions around how people actually filter, usually time plus one or two selective dimensions. Benchmark with representative data and queries. Do not assume the old execution plan predicts the new platform’s behavior.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Illustrative warehouse layout; syntax varies by platform</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">curated</span><span class="p">.</span><span class="n">fact_events</span> <span class="p">(</span>
  <span class="n">event_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">user_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_type</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">event_ts</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="n">payload</span> <span class="n">JSON</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">event_type</span><span class="p">;</span>

<span class="c1">-- always include a filter on the partition column</span>
<span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span>
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">fact_events</span>
<span class="k">WHERE</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span> <span class="k">BETWEEN</span> <span class="s1">'2024-01-01'</span> <span class="k">AND</span> <span class="s1">'2024-01-07'</span>
  <span class="k">AND</span> <span class="n">user_id</span> <span class="o">=</span> <span class="s1">'u-123'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="batch-dependencies-lived-in-peoples-heads">Batch dependencies lived in people’s heads</h3>

<p>“The sales mart runs after finance close” was often a human rule, not an explicit dependency. In a new orchestrator, missing that relationship can produce silently stale or incomplete data, especially across time zones and fiscal calendars.</p>

<p>What helped: model calendars and upstream SLAs as real dependencies. Write down latest acceptable start time, not only job duration.</p>

<h3 id="cost-model-flips">Cost model flips</h3>

<p>Fixed-capacity platforms often hid the marginal cost of inefficient scans. Usage-based cloud services make that cost visible. A query that was merely slow before can now be both slow and expensive.</p>

<p>What helped: serving layers, clustering, partition filters, materialize heavy aggregates, query budgets - before cutover, not after the first invoice.</p>

<h3 id="stakeholders-still-expect-2012-behavior">Stakeholders still expect 2012 behavior</h3>

<p>Same report, same number, same time. Migration changes freshness, rounding, nulls, timezones. Without a parity agreement, trust dies even when the new platform is better.</p>

<p>What helped: signed reconciliation on critical metrics during parallel run, with variance thresholds agreed upfront.</p>

<hr />

<h2 id="workload-shapes-that-fight-the-cloud">Workload shapes that fight the cloud</h2>

<table>
  <thead>
    <tr>
      <th>What we brought over</th>
      <th>What it cost us</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Huge wide tables, <code class="language-plaintext highlighter-rouge">SELECT *</code></td>
      <td>Scan cost, slow BI</td>
    </tr>
    <tr>
      <td>Giant star schemas rebuilt every night</td>
      <td>Long fragile critical path</td>
    </tr>
    <tr>
      <td>Heavy UDFs in SQL</td>
      <td>Slot time, hard to test</td>
    </tr>
    <tr>
      <td>Tiny files every micro-batch, no compaction</td>
      <td>Metadata pain</td>
    </tr>
    <tr>
      <td>Shared service account</td>
      <td>Audit and blast radius</td>
    </tr>
  </tbody>
</table>

<p>Moving is not enough. You usually have to change the shape of the work.</p>

<hr />

<h2 id="parallel-run-that-does-not-become-permanent">Parallel run that does not become permanent</h2>

<p>A minimal, platform-neutral reconciliation query for a parallel run might look like this:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">source_data</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="n">order_date</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">rows_old</span><span class="p">,</span> <span class="k">SUM</span><span class="p">(</span><span class="n">revenue</span><span class="p">)</span> <span class="k">AS</span> <span class="n">rev_old</span>
  <span class="k">FROM</span> <span class="n">legacy</span><span class="p">.</span><span class="n">orders_daily</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="mi">1</span>
<span class="p">),</span>
<span class="n">target_data</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="n">order_date</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">rows_new</span><span class="p">,</span> <span class="k">SUM</span><span class="p">(</span><span class="n">revenue</span><span class="p">)</span> <span class="k">AS</span> <span class="n">rev_new</span>
  <span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">orders</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="mi">1</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">COALESCE</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">order_date</span><span class="p">,</span> <span class="n">n</span><span class="p">.</span><span class="n">order_date</span><span class="p">)</span> <span class="k">AS</span> <span class="n">order_date</span><span class="p">,</span>
  <span class="n">rows_old</span><span class="p">,</span> <span class="n">rows_new</span><span class="p">,</span>
  <span class="k">CASE</span>
    <span class="k">WHEN</span> <span class="n">o</span><span class="p">.</span><span class="n">order_date</span> <span class="k">IS</span> <span class="k">NULL</span> <span class="k">OR</span> <span class="n">n</span><span class="p">.</span><span class="n">order_date</span> <span class="k">IS</span> <span class="k">NULL</span> <span class="k">OR</span> <span class="n">rows_old</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">THEN</span> <span class="k">NULL</span>
    <span class="k">ELSE</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">*</span> <span class="p">(</span><span class="n">rows_new</span> <span class="o">-</span> <span class="n">rows_old</span><span class="p">)</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="n">rows_old</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
  <span class="k">END</span> <span class="k">AS</span> <span class="n">row_delta_pct</span><span class="p">,</span>
  <span class="k">CASE</span>
    <span class="k">WHEN</span> <span class="n">o</span><span class="p">.</span><span class="n">order_date</span> <span class="k">IS</span> <span class="k">NULL</span> <span class="k">OR</span> <span class="n">n</span><span class="p">.</span><span class="n">order_date</span> <span class="k">IS</span> <span class="k">NULL</span> <span class="k">OR</span> <span class="n">rev_old</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">THEN</span> <span class="k">NULL</span>
    <span class="k">ELSE</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">*</span> <span class="p">(</span><span class="n">rev_new</span> <span class="o">-</span> <span class="n">rev_old</span><span class="p">)</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="n">rev_old</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
  <span class="k">END</span> <span class="k">AS</span> <span class="n">rev_delta_pct</span>
<span class="k">FROM</span> <span class="n">source_data</span> <span class="n">o</span>
<span class="k">FULL</span> <span class="k">OUTER</span> <span class="k">JOIN</span> <span class="n">target_data</span> <span class="n">n</span> <span class="k">ON</span> <span class="n">o</span><span class="p">.</span><span class="n">order_date</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="n">order_date</span>
<span class="k">WHERE</span> <span class="n">o</span><span class="p">.</span><span class="n">order_date</span> <span class="k">IS</span> <span class="k">NULL</span>
   <span class="k">OR</span> <span class="n">n</span><span class="p">.</span><span class="n">order_date</span> <span class="k">IS</span> <span class="k">NULL</span>
   <span class="k">OR</span> <span class="n">rev_old</span> <span class="o">=</span> <span class="mi">0</span>
   <span class="k">OR</span> <span class="k">ABS</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="o">*</span> <span class="p">(</span><span class="n">rev_new</span> <span class="o">-</span> <span class="n">rev_old</span><span class="p">)</span> <span class="o">/</span> <span class="k">NULLIF</span><span class="p">(</span><span class="n">rev_old</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">.</span><span class="mi">005</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">order_date</span><span class="p">;</span>
</code></pre></div></div>

<p>When we ran old and new together, the things that mattered were:</p>

<ol>
  <li>Reconciliation that looks at distributions, not only totals</li>
  <li>Clear cutover criteria (e.g. 99.5% match on revenue for 30 days)</li>
  <li>A named rollback path</li>
  <li>A decommission date - open-ended parallel run is the expensive failure mode</li>
</ol>

<hr />

<h2 id="the-people-side">The people side</h2>

<p>Database specialists and data engineers can disagree about who owns performance and cost. BI teams may rebuild shadow marts on top of the new warehouse. Delivery partners leave, and nobody knows why the dependency graph is ordered that way.</p>

<p>Naming owners before cutover helped. Training people to interpret query plans, resource contention, and pruning helped more than teaching only how to rerun a job.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Legacy data-platform migrations fail quietly when teams copy old shapes instead of redesigning for access patterns, dependencies, cost, and trust. The breakage is predictable. Plan for that - not only for volume and bandwidth.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Why Most Technical Migrations Fail Before the First Line of Code</title>
					
					<link>https://atabakkh.github.io/agile/2023/12/12/migrations-fail-before-first-line-of-code.html</link>

					

					<pubDate>Tue, 12 Dec 2023 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/agile/2023/12/12/migrations-fail-before-first-line-of-code</guid>

					
						<category><![CDATA[ Agile ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>I’ve seen migrations fail in planning docs and committee meetings, long before anyone started writing the actual code. The first problem is almost never the technology.</em></p>

<p><strong>Context:</strong> End of 2023 was still peak migration-program season. Board dates, vendor SOWs, teams told to “be on cloud by Q2”. The ones that struggled almost never failed because of networking or storage. They failed because the hard decisions were never made before engineering started.</p>

<p>These are notes from that side of the table - when leadership says “make the migration happen” without the preconditions that let engineering succeed.</p>

<hr />

<h2 id="1-nobody-agrees-what-done-means">1. Nobody agrees what “done” means</h2>

<p>IT says migration is complete. Business says the reports are wrong. Finance says cost went up.</p>

<p>“Done” meant different things to different people: infrastructure lifted, metrics matching within a threshold, legacy switched off, operating model updated. Often only the first one was written down.</p>

<p>What helped: a one-page definition agreed by business, finance, and engineering - which workloads in which wave, parity thresholds (for example revenue within ±0.5% for 30 days), decommission dates, who approves cutover. Without that, teams optimize for go-live theater.</p>

<hr />

<h2 id="2-migration-is-treated-as-a-project-not-a-product-handoff">2. Migration is treated as a project, not a product handoff</h2>

<p>The SI delivers. The internal team inherits a black box. One hero burns out keeping it alive.</p>

<p>Budget and timeline focused on cutover. Year-two operations were an afterthought.</p>

<p>What helped: staff for run state before kickoff, not only sprint zero. Put runbooks, ownership, and on-call into acceptance. Plan 20–30% capacity for stabilization after go-live in the roadmap you show upward.</p>

<p>Executives hear “we migrated”. Operators live with “nobody knows why this job runs at 3am”.</p>

<hr />

<h2 id="3-parallel-run-with-no-end-date">3. Parallel run with no end date</h2>

<p>Double infrastructure for 18 months. Two numbers. Endless arguments about which one is “true”.</p>

<p>People are afraid of cutover. Nobody wants to own decommission risk.</p>

<p>What helped: mandatory decommission milestones with named sponsors. Fund reconciliation during parallel run - do not leave it to volunteer overtime. Escalate when parity is “good enough” but decommission keeps slipping. Parallel run is a bridge, not a home.</p>

<hr />

<h2 id="4-the-wrong-people-in-the-room">4. The wrong people in the room</h2>

<p>Vendors or infra-only teams decide the architecture. BI and data consumers find out at UAT.</p>

<p>The charter covered <em>moving</em>. It did not cover <em>using</em>.</p>

<p>What helped: bring consumers, BI, analytics, and security into wave planning from week one. Assign a business owner per domain, not only a technical workstream lead. Do not lock architecture if access patterns are unsigned.</p>

<hr />

<h2 id="5-honesty-gets-punished">5. Honesty gets punished</h2>

<p>Status stays green until go-live week. Then everything is red.</p>

<p>Teams are rewarded for hitting dates and punished for raising risks early. So they hide uncertainty.</p>

<p>What helped: separate status reporting from blame. Use milestone demos on real data, not decks. Protect buffer in the plan you show leadership. The fastest way to miss a date is to pretend the uncertainty is not there.</p>

<hr />

<h2 id="a-short-list-i-used-before-saying-start">A short list I used before saying “start”</h2>

<p><img src="/img/diagrams/migration-preflight.svg" alt="Migration pre-flight checklist diagram" width="750" /></p>

<table>
  <thead>
    <tr>
      <th>Question</th>
      <th>If no</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Is “done” defined and signed?</td>
      <td>Do not start wave 1</td>
    </tr>
    <tr>
      <td>Are owners named for the run state?</td>
      <td>Do not accept vendor handoff</td>
    </tr>
    <tr>
      <td>Is decommission scheduled?</td>
      <td>Do not approve parallel-run budget</td>
    </tr>
    <tr>
      <td>Are consumers in the loop?</td>
      <td>Do not lock architecture</td>
    </tr>
    <tr>
      <td>Is stabilization capacity funded?</td>
      <td>Do not commit a public date</td>
    </tr>
  </tbody>
</table>

<p>That list saved more programs, in my experience, than another proof-of-concept cluster.</p>

<p>I even kept a one-pager template in markdown for steering packs:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Wave 1 - definition of done</span>
Workloads: [list]
Parity: revenue_eur within ±0.5% for 30 consecutive days
Owners (run state): business ___ / technical ___
Decommission date: ____  Sponsor: ____
Stabilization capacity reserved: ___% for __ weeks
Consumers signed (BI / analytics / security): [yes/no]
</code></pre></div></div>

<hr />

<h2 id="how-i-learned-to-talk-to-executives">How I learned to talk to executives</h2>

<p>Bad: “We need six more months for technical debt.”</p>

<p>Better: “We can go live in April on infrastructure. Business cutover in June if parity holds for 30 days. Otherwise we risk wrong reporting and double cost until legacy is off.”</p>

<p>The job is translating risk and trade-offs. It is not being the best debugger in the room.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Most migrations fail before the first line of code because success was never agreed, ownership ends at go-live, and incentives hide problems.</p>

<p>Get those leadership pieces right - definition of done, run-state staffing, courage to decommission, consumers in the room, honest status - and engineering has a chance.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Cloud Migration Is Not Modernization</title>
					
					<link>https://atabakkh.github.io/cloud/2023/11/08/cloud-migration-is-not-modernization.html</link>

					

					<pubDate>Wed, 08 Nov 2023 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2023/11/08/cloud-migration-is-not-modernization</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>I keep seeing programs celebrate “we are on AWS / Azure / Alibaba” while the way people work with data barely changed. Landing a VM or a dump into the cloud is not modernization.</em></p>

<p><strong>Context:</strong> By late 2023 a lot of enterprises had finished their first migration wave. In the rooms I sat in, go-live of the landing zone was treated as the finish line. Batch pipelines stayed nightly because “that is what we always did”. Access still went through tickets. Nobody owned the tables. Cost still belonged only to finance.</p>

<p>This post is about that gap - what I saw when lift-and-shift was sold as modernization, and what actually had to change for analytics and ML to work later.</p>

<hr />

<h2 id="lift-and-shift-vs-what-i-mean-by-modernization">Lift-and-shift vs. what I mean by modernization</h2>

<p><img src="/img/diagrams/lift-shift-vs-modernization.svg" alt="Lift-and-shift versus modernization comparison" width="750" /></p>

<p><em>Sketch from notes I kept after too many “we are on cloud” steering slides.</em></p>

<p>Lift-and-shift usually means: same schemas, same jobs, new host. A legacy export lands in a warehouse as a raw dump. Shared admin accounts remain. You discover the bill at month end.</p>

<p>Modernization, when it actually happened, looked different:</p>

<ul>
  <li>Tables designed around how people query them, not how the source system stored them</li>
  <li>Landing -&gt; curated -&gt; serving layers, with someone responsible for each</li>
  <li>Schedule matched to when the business needs the number, not to an old cron habit</li>
  <li>Least-privilege service accounts</li>
  <li>Cost and retention treated as design choices, not finance surprises</li>
</ul>

<p>Teams that only moved hosts often had the same pain as before: slow delivery, opaque pipelines, rising cloud bills, and no clean base for reporting or ML.</p>

<hr />

<h2 id="five-signs-the-migration-landed-but-nothing-really-changed">Five signs the migration landed but nothing really changed</h2>

<p><strong>1. The “data lake” is still a file dump.</strong><br />
Parquet in object storage without partitioning, compaction, or serving tables is storage with extra steps. It is not a platform.</p>

<p><strong>2. Only one person understands the nightly chain.</strong><br />
If nobody can draw the path from source to dashboard, you have operational debt. Green jobs in an orchestrator do not fix that.</p>

<p><strong>3. Definitions live in people’s heads.</strong><br />
“Active customer” means one thing in finance and another in marketing. Cloud does not fix that. Without owners and written definitions, every team rebuilds the same logic.</p>

<p><strong>4. Security is still perimeter thinking.</strong><br />
VPN plus broad DB roles made some sense on-prem. In cloud, the same broad roles become a much bigger blast radius.</p>

<p><strong>5. FinOps is a monthly meeting.</strong><br />
Partitioning, materialization, how often jobs run, how long you keep data - those are architecture decisions. Treating cost as a post-hoc finance topic almost always ends in overspend.</p>

<hr />

<h2 id="what-actually-changed-when-modernization-worked">What actually changed when modernization worked</h2>

<p><strong>Architecture.</strong> Explicit layers: landing for raw ingest, curated for cleaned entities, serving for how people query. Design for access patterns, not for “copy the source table”.</p>

<p><img src="/img/diagrams/data-platform-layers.svg" alt="Landing curated serving feature layers" width="750" /></p>

<p>A tiny, illustrative example of the serving-layer idea - not a full migration blueprint:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- serving table shaped for the dashboard filter pattern (date + account)</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">serving</span><span class="p">.</span><span class="n">account_daily_kpis</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">event_date</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">account_id</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="n">account_id</span><span class="p">,</span>
  <span class="nb">DATE</span><span class="p">(</span><span class="n">event_ts</span><span class="p">)</span> <span class="k">AS</span> <span class="n">event_date</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">events</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">IF</span><span class="p">(</span><span class="n">event_type</span> <span class="o">=</span> <span class="s1">'purchase'</span><span class="p">,</span> <span class="n">amount</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="k">AS</span> <span class="n">revenue</span>
<span class="k">FROM</span> <span class="n">curated</span><span class="p">.</span><span class="n">events</span>
<span class="k">WHERE</span> <span class="n">event_ts</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">90</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Operating model.</strong> Named business and technical owners. Freshness and quality expectations that someone can be held to. Pipeline failures visible to the people who can fix them.</p>

<p><strong>Engineering habits.</strong> Infrastructure as code, some CI on SQL and DAGs, runbooks, SLOs on the critical paths - the same seriousness you would expect from a product service.</p>

<p><strong>Decommissioning.</strong> Modernization includes turning old paths off. Running legacy and cloud platforms side by side forever doubles cost and destroys trust in the numbers.</p>

<hr />

<h2 id="a-sequence-that-kept-working-when-programs-recovered">A sequence that kept working when programs recovered</h2>

<p><strong>Weeks 1-6 - make the truth visible.</strong> Inventory sources, pipelines, consumers, owners. Measure real query patterns and runtimes. Baseline cost by domain.</p>

<p><strong>Weeks 7-14 - stabilize the path to value.</strong> Publish serving tables for the top business questions. Add freshness metadata. Fix the worst quality and reconciliation gaps.</p>

<p><strong>Weeks 15-24 - retire the old behavior.</strong> Cut duplicate pipelines. Tighten IAM. Set retention. Decommission one legacy domain at a time with signed parity checks.</p>

<p>This principle held across managed warehouses, lakehouses, and mixed platforms. The cloud product was never the modernization. The operating model was.</p>

<hr />

<h2 id="why-i-care-about-this-before-ml-and-analytics-scale">Why I care about this before ML and analytics scale</h2>

<p>Even in 2023, every serious analytics or ML plan assumed trusted, reasonably fresh inputs. A lift-and-shift warehouse rarely gives you that. Teams then bolt more tooling on top of a shaky base and wonder why numbers disagree.</p>

<p>Get the platform operating model right first. Everything you add later - dashboards, scoring jobs, copilots - will amplify what is already there.</p>

<hr />

<h2 id="from-the-leadership-seat">From the leadership seat</h2>

<p>Questions I kept asking when someone said “migration is complete”:</p>

<ol>
  <li>Can we name owners for the top datasets finance and product rely on?</li>
  <li>Is decommission scheduled, or are we paying for two platforms indefinitely?</li>
  <li>Do we have parity evidence, or only infrastructure checklists?</li>
  <li>Can we attribute cost by domain, or is it one opaque bill?</li>
  <li>Can a new engineer change a pipeline safely without calling the hero on Slack?</li>
</ol>

<p>If three or more answers were no, we were not done - landing zone or not.</p>

<p>What worked when talking upward was not “we need more time for technical debt”. It was something like: we are live on cloud infrastructure, but not yet on a cloud operating model; here is a 90-day path, and here are the risks if we skip it.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Cloud migration answers <strong>where</strong> things run. Modernization answers <strong>how</strong> data is layered, owned, operated, secured, and paid for.</p>

<p>When a program was “done” but delivery was still slow, costs opaque, and definitions still argued in meetings - we had changed the invoice address, not the platform.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Large-Scale Lead, View, and Sales Event Data on Spark</title>
					
					<link>https://atabakkh.github.io/spark/2023/09/15/lead-sales-event-data-platform-spark-architecture.html</link>

					

					<pubDate>Fri, 15 Sep 2023 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/spark/2023/09/15/lead-sales-event-data-platform-spark-architecture</guid>

					
						<category><![CDATA[ Spark ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>how to design and tune Spark pipelines for high-volume lead, view, and sales event data; covering shuffle behavior, skew handling, and storage layout so pipelines stay correct and scalable.</em></p>

<p><strong>Context:</strong> Platforms that combine <strong>lead/view events</strong> (e.g. product views, form submissions, campaign touchpoints) with <strong>sales/transaction data</strong> at scale (e.g. multi-TB per day) run on Spark (or similar) for ingestion and batch/streaming processing. The main challenges are not only “what to compute” but <strong>how</strong> Spark executes: shuffle volume, skew, and storage layout directly drive latency, cost, and reliability. This post explains how a big data architect approaches ingestion, partitioning, shuffle, skew, and storage so the platform meets freshness SLAs and stays operable.</p>

<hr />

<h2 id="why-spark-and-what-breaks-at-scale">Why Spark, and what breaks at scale</h2>

<p>At multi-TB daily volume, ingestion and processing must be distributed. Spark gives a single programming model for batch and streaming (Structured Streaming), but <strong>shuffle</strong>, <strong>skew</strong>, and <strong>storage</strong> become the main levers. If you ignore them:</p>

<ul>
  <li><strong>Shuffle</strong>: Wide transformations (joins, aggregations by new keys, <code class="language-plaintext highlighter-rouge">distinct</code>) trigger shuffle. Too many partitions -&gt; many small shuffle writes and slow fetches; too few -&gt; few huge tasks, OOM or stragglers. Wrong partition keys -&gt; unnecessary shuffle or data skew.</li>
  <li><strong>Skew</strong>: A few keys (e.g. power users, hot products) get a disproportionate share of data. One or a few tasks do most of the work; the rest sit idle. Job latency is dictated by the slowest task.</li>
  <li><strong>Storage</strong>: Many small files (e.g. per-microbatch) cause slow listing and many small reads; huge files cause poor parallelism and spill. Partition layout and file size directly affect read performance and downstream jobs.</li>
</ul>

<p>So the architect’s job is to <strong>design the data flow and key layout</strong> so that shuffle is minimized and balanced, skew is handled explicitly, and storage is optimized for the read patterns. The rest of this post is organized around those three themes.</p>

<hr />

<h2 id="pipeline-shape-ingestion-raw-layer-and-spark-processing">Pipeline shape: ingestion, raw layer, and Spark processing</h2>

<p><strong>Ingestion</strong><br />
Events (lead, view, sales) land in a message bus (e.g. Kafka). A Spark Streaming (or connector) job reads from topics and writes to a <strong>raw layer</strong> (e.g. object storage or HDFS). Design choices here:</p>

<ul>
  <li><strong>Partitioning on write</strong>: Partition by <strong>date</strong> (and optionally source/region) so that downstream batch jobs can read by date range and avoid full scans. Use a <strong>single</strong> partition key scheme (e.g. <code class="language-plaintext highlighter-rouge">dt=yyyy-MM-dd</code>) so that reprocessing and backfills are consistent.</li>
  <li><strong>Shuffle at ingestion</strong>: Keep ingestion <strong>narrow</strong>; no joins, no global aggregation. Each batch writes to its partition; that implies no shuffle in the ingestion job if you use the same partition key as the topic or a simple projection. If the connector repartitions by <code class="language-plaintext highlighter-rouge">dt</code>, that’s one controlled shuffle; avoid extra <code class="language-plaintext highlighter-rouge">groupBy</code> or joins here.</li>
</ul>

<p><strong>Raw -&gt; curated (Spark batch)</strong><br />
A batch job (e.g. every 15-30 minutes or hourly) reads the raw layer, deduplicates, handles late arrivals, joins to dimensions, and writes the <strong>curated</strong> layer. This is where shuffle and skew show up:</p>

<ul>
  <li><strong>Deduplication</strong>: Typically <code class="language-plaintext highlighter-rouge">dropDuplicates(event_id)</code> or a window + rank. This is a shuffle by the dedupe key. If the key is well distributed (e.g. UUID), shuffle is even; if the key is something like <code class="language-plaintext highlighter-rouge">(user_id, date)</code>, you may get skew (see below).</li>
  <li><strong>Joins</strong>: Joining events to dimension tables (e.g. product, user) triggers shuffle (or broadcast). Dimension tables that fit in memory should be <strong>broadcast</strong> so you avoid shuffle for that side; large fact-fact or fact-dimension joins need a clear strategy for the partition key and skew.</li>
  <li><strong>Late arrivals</strong>: Reprocessing last N hours of raw data and merging into curated implies reading by partition (e.g. by <code class="language-plaintext highlighter-rouge">dt</code> and hour), so <strong>partition pruning</strong> works. The merge step (e.g. overwrite by partition or merge via a key) should align with the same partition key to avoid full-table shuffle.</li>
</ul>

<p>So from an architect’s view: <strong>define the partition key for raw and curated once</strong> (e.g. <code class="language-plaintext highlighter-rouge">dt</code>, and optionally <code class="language-plaintext highlighter-rouge">hour</code> or <code class="language-plaintext highlighter-rouge">source</code>), and design every Spark job to respect that key so that shuffle is predictable and limited to the minimum necessary.</p>

<hr />

<h2 id="shuffle-how-much-and-how-to-tune-it">Shuffle: how much, and how to tune it</h2>

<p><strong>When shuffle happens</strong><br />
In Spark, shuffle is triggered by wide transformations: <code class="language-plaintext highlighter-rouge">join</code>, <code class="language-plaintext highlighter-rouge">aggregate</code>/<code class="language-plaintext highlighter-rouge">groupBy</code> with a new key, <code class="language-plaintext highlighter-rouge">distinct</code>, <code class="language-plaintext highlighter-rouge">repartition</code>, <code class="language-plaintext highlighter-rouge">sortByKey</code>, etc. Each of these writes a shuffle stage (map side) and reads it (reduce side). Shuffle is expensive: network, disk I/O, and serialization. So the architect’s goals are: (1) <strong>reduce shuffle</strong> where possible, (2) <strong>size partitions so that shuffle is balanced</strong>, and (3) <strong>avoid shuffle where a broadcast or partition-local op will do</strong>.</p>

<p><strong>Reduce shuffle</strong></p>
<ul>
  <li><strong>Broadcast</strong> small dimension tables (e.g. product category lookup). Threshold is driver memory and executor memory; typically tables &lt; 100-200 MB are safe. Use <code class="language-plaintext highlighter-rouge">broadcast()</code> in joins so the large table is not shuffled to match the small one; only the large side is partitioned.</li>
  <li><strong>Partition alignment</strong>: If the raw table is already partitioned by <code class="language-plaintext highlighter-rouge">dt</code>, and the curated table is also by <code class="language-plaintext highlighter-rouge">dt</code>, a “merge by dt” job can read and write by partition without shuffling on <code class="language-plaintext highlighter-rouge">dt</code>. Shuffle only on the keys you need for dedupe or join (e.g. <code class="language-plaintext highlighter-rouge">event_id</code> or <code class="language-plaintext highlighter-rouge">user_id</code>).</li>
  <li><strong>Avoid redundant groupBy</strong>: If you already have a grouped result from a previous step, don’t re-group by the same key in the next stage; chain transformations so that you don’t shuffle twice on the same key.</li>
</ul>

<p><strong>Size shuffle partitions</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">spark.sql.shuffle.partitions</code> (default often 200) controls how many reduce-side tasks (and hence output partitions) you get after a shuffle. Too high -&gt; many small tasks, overhead and small files; too low -&gt; few large tasks, risk of OOM and skew. Rule of thumb: start with something like <code class="language-plaintext highlighter-rouge">2-4 × num_cores</code> across the cluster, then adjust so that each task gets a few hundred MB to a few GB of shuffle read. For very large jobs, 500-2000 is common.</li>
  <li><strong>Repartition before shuffle-heavy writes</strong>: If the next stage is a write to object storage, repartition by the <strong>table partition key</strong> (e.g. <code class="language-plaintext highlighter-rouge">dt</code>, <code class="language-plaintext highlighter-rouge">hour</code>) so that each output partition corresponds to one directory; that avoids a separate shuffle in the next job when reading by partition.</li>
</ul>

<p><strong>Monitor shuffle</strong></p>
<ul>
  <li>In Spark UI: shuffle read/write size per stage, and skew in task duration. If a few tasks take 10× longer than the median, you have skew (see next section). If shuffle read/write is huge relative to input size, look for redundant shuffles or suboptimal join order.</li>
</ul>

<hr />

<h2 id="skew-why-it-happens-and-how-to-handle-it-in-spark">Skew: why it happens and how to handle it in Spark</h2>

<p><strong>Where skew comes from</strong><br />
Skew is uneven distribution of data per partition key. Common causes:</p>

<ul>
  <li><strong>Join key</strong>: A few <code class="language-plaintext highlighter-rouge">user_id</code>s or <code class="language-plaintext highlighter-rouge">product_id</code>s have orders of magnitude more events than the rest. When you join on that key, one partition gets a huge amount of data.</li>
  <li><strong>GroupBy key</strong>: Same idea; e.g. “events per user” when 1% of users have 80% of events.</li>
  <li><strong>Deduplication key</strong>: If the key is correlated with time or source, some partitions can be much larger.</li>
</ul>

<p><strong>Effect</strong><br />
A few tasks run for a long time; others finish quickly. Job latency is dominated by the stragglers. You may also see OOM on those tasks or spill.</p>

<p><strong>Handling skew in Spark</strong></p>

<ol>
  <li>
    <p><strong>Broadcast the skewed side (if small)</strong><br />
If the “large” side of the join is actually a small set of hot keys (e.g. a list of 10K power users), you can filter them out, join the rest normally, and then join the hot keys via broadcast. So: “cold” data joins with even partitions; “hot” data is handled separately with a broadcast.</p>
  </li>
  <li>
    <p><strong>Salting</strong><br />
Add a random salt to the key on the big side so that the hot key is split across many partitions. Example: <code class="language-plaintext highlighter-rouge">concat(user_id, "_", rand(0, 99))</code> so that one <code class="language-plaintext highlighter-rouge">user_id</code> is spread across 100 partitions. The small side is replicated (e.g. 100 copies) so that each salted partition can join. After the join, aggregate back by the original key if needed. This increases shuffle volume (replicated small side) but balances load.</p>
  </li>
  <li>
    <p><strong>AQE skew join (Spark 3.x)</strong><br />
Adaptive Query Execution can detect skew at runtime and split the skewed partition into multiple tasks. Enable <code class="language-plaintext highlighter-rouge">spark.sql.adaptive.enabled</code> and <code class="language-plaintext highlighter-rouge">spark.sql.adaptive.skewJoin.enabled</code>. AQE repartitions the skewed partition so that the work is spread. This is the least invasive: no salting logic in the code, but you need to run Spark 3 and tune AQE (e.g. <code class="language-plaintext highlighter-rouge">skewJoinSkewedPartitionFactor</code>, <code class="language-plaintext highlighter-rouge">skewJoinPartitionFactor</code>).</p>
  </li>
  <li>
    <p><strong>Split hot and cold</strong><br />
Identify hot keys (e.g. from a pre-aggregation or histogram), then: (a) process hot keys in a separate job with broadcast or salting, (b) process the rest in the main job. Union the results. This keeps the main job skew-free and confines special handling to a small subset.</p>
  </li>
</ol>

<p>In practice, an architect will <strong>enable AQE</strong> first, <strong>monitor</strong> task duration distribution; if skew remains, add <strong>salting</strong> or <strong>split hot/cold</strong> for the specific stages that show skew.</p>

<hr />

<h2 id="storage-optimization-partitioning-file-size-and-format">Storage optimization: partitioning, file size, and format</h2>

<p><strong>Partitioning</strong></p>
<ul>
  <li><strong>Raw and curated</strong>: Partition by <strong>date</strong> (<code class="language-plaintext highlighter-rouge">dt=yyyy-MM-dd</code>) at minimum. If downstream often filters by hour or source, add <code class="language-plaintext highlighter-rouge">hour</code> or <code class="language-plaintext highlighter-rouge">source</code> so that listing and reads only touch the needed directories. Avoid over-partitioning (e.g. by minute or by user_id) so you don’t get millions of tiny partitions and slow metadata/list operations.</li>
  <li><strong>Align with Spark reads</strong>: Use <code class="language-plaintext highlighter-rouge">df.write.partitionBy("dt")</code> (and optionally <code class="language-plaintext highlighter-rouge">"hour"</code>) so that <code class="language-plaintext highlighter-rouge">spark.read.parquet(path).filter("dt = '2023-09-15'")</code> only reads that partition. No need to scan the whole table.</li>
</ul>

<p><strong>File size and count</strong></p>
<ul>
  <li><strong>Small files</strong>: Many small files (e.g. one per microbatch or per task) cause slow listing (object storage) and many small reads. Spark’s listing and planning overhead grows with file count. Target roughly <strong>128 MB-512 MB per file</strong> (or per partition) for good throughput. If your write produces many small files, <strong>coalesce</strong> or <strong>repartition</strong> by the partition key before writing (e.g. <code class="language-plaintext highlighter-rouge">df.repartition(N, "dt").write.partitionBy("dt").parquet(...)</code> so that each <code class="language-plaintext highlighter-rouge">dt</code> has about N files).</li>
  <li><strong>Compaction</strong>: Run a separate compaction job (e.g. daily): read a partition (e.g. yesterday’s <code class="language-plaintext highlighter-rouge">dt</code>), coalesce to a target number of files, overwrite that partition. That keeps file count and size in a good range without changing the pipeline logic.</li>
  <li><strong>Repartition on write</strong>: For append or overwrite-by-partition, use <code class="language-plaintext highlighter-rouge">repartition("dt", "hour")</code> (or a fixed number per partition) so that the number of output files is under control. Avoid <code class="language-plaintext highlighter-rouge">coalesce(1)</code> for large partitions; that creates a single huge file and kills parallelism on read.</li>
</ul>

<p><strong>Format and compression</strong></p>
<ul>
  <li><strong>Parquet</strong> is the default choice: columnar, good compression, predicate pushdown (so <code class="language-plaintext highlighter-rouge">filter(dt = ...)</code> is pushed to the reader and only needed columns are read). Use <strong>Snappy</strong> (faster) or <strong>Zstd</strong> (better ratio, slightly more CPU). Schema evolution is supported if you use a table format (e.g. Delta, Iceberg) or strict schema discipline.</li>
  <li><strong>Block size</strong>: Parquet row group size affects read parallelism and pushdown. Defaults are often fine; for very wide tables, tuning row group size can help.</li>
</ul>

<p><strong>Idempotent overwrite</strong><br />
For “merge” or “reprocess last N hours,” use <strong>overwrite by partition</strong> (e.g. <code class="language-plaintext highlighter-rouge">df.write.mode("overwrite").partitionBy("dt").save(path)</code> with dynamic partition overwrite if your Spark/source supports it) so that you don’t leave duplicate or partial data. That way, storage stays consistent and reprocessing is safe.</p>

<hr />

<h2 id="late-arrivals-and-deduplication-spark-view">Late arrivals and deduplication (Spark view)</h2>

<p><strong>Late arrivals</strong></p>
<ul>
  <li><strong>Streaming</strong>: Use event-time and watermarking so that late events within the watermark window are included; beyond that, they’re dropped (or sent to a side output for a batch backfill).</li>
  <li><strong>Batch</strong>: Reprocess a window (e.g. last 24-48 hours) by reading the raw partitions for that window, deduplicating and joining, then <strong>overwriting</strong> the corresponding curated partitions. That’s idempotent if the keys and logic are deterministic.</li>
</ul>

<p><strong>Deduplication</strong></p>
<ul>
  <li>Use a stable key (e.g. <code class="language-plaintext highlighter-rouge">event_id</code>). <code class="language-plaintext highlighter-rouge">dropDuplicates("event_id")</code> triggers a shuffle by <code class="language-plaintext highlighter-rouge">event_id</code>. If <code class="language-plaintext highlighter-rouge">event_id</code> is uniform, shuffle is even. If you need “last event wins” per (user, date), that’s a window + rank; partition by (user, date), order by timestamp, take rank 1. Again, shuffle is by (user, date); watch for skew on high-activity users and apply the same skew tactics (AQE, salting, or split).</li>
</ul>

<hr />

<h2 id="summary-how-a-big-data-architect-sees-it">Summary: how a big data architect sees it</h2>

<p>A large-scale lead/view/sales event platform on Spark rests on:</p>

<ul>
  <li><strong>Clear partition strategy</strong> for raw and curated (e.g. <code class="language-plaintext highlighter-rouge">dt</code>, optional <code class="language-plaintext highlighter-rouge">hour</code>/<code class="language-plaintext highlighter-rouge">source</code>) so that jobs can read and write by partition, reprocess safely, and avoid unnecessary full scans.</li>
  <li><strong>Shuffle discipline</strong>: broadcast small dimensions; set <code class="language-plaintext highlighter-rouge">spark.sql.shuffle.partitions</code> and repartition by the table partition key before writing; avoid redundant shuffles; monitor shuffle read/write and task duration in Spark UI.</li>
  <li><strong>Skew handling</strong>: enable AQE skew join; where needed, use salting or split hot/cold so that no single key dominates a partition.</li>
  <li><strong>Storage optimization</strong>: partition by date (and optional hour/source); target 128-512 MB per file; use coalesce/repartition on write and compaction jobs to control file count; use Parquet with sensible compression; use overwrite-by-partition for idempotent merges.</li>
  <li><strong>Late arrivals and deduplication</strong>: event-time and watermark in streaming; batch reprocess with overwrite-by-partition and deterministic keys so that correctness and operability are maintained.</li>
</ul>

<p>Getting these right; shuffle, skew, and storage; is what makes the difference between a pipeline that “works on a sample” and one that meets freshness SLAs and stays reliable at multi-TB scale. The same patterns apply to any Spark-based event and transaction platform.</p>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>4 common architecture solutions</title>
					
					<link>https://atabakkh.github.io/software/2021/12/05/commonarchitecture.html</link>

					

					<pubDate>Sun, 05 Dec 2021 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/software/2021/12/05/commonarchitecture</guid>

					
						<category><![CDATA[ Software ]]></category>
					

					<description><![CDATA[ From variaty of architectures and dependencies on requirement, let's have a short discussion on 4 basic architectures. ]]></description>
					<content:encoded><![CDATA[ <h4>A short overview on 4 common architecture solutions</h4><h2>Basic ideal solutions</h2>
<p><br /></p>

<p><img src="https://atabakkh.github.io/img/idealsolution.png" width="400" height="130" /></p>

<p><br />
<br />
Usually most of the architecture and restructuring of the applications and finding a proper architect for the entire solution would require enough POC and testing. However, due to shortage of time, cost and also dependencies to other services, the solution architects will prepare the easiest workable one and then improve gradually, and later will face a lot of bottleneck and deadlock. The main target of this article is to compare and represent some foundation and common architectures based on 4 main application categories: resilient, high-performance, secure and cost effective.
<br />
<br /></p>
<h3>1. Resilient Architectures</h3>
<p>Target of this architecture is to achieve an effective architecture using fundamental services. Best practices are important to know when building these architecture. Therefore, focusing on following items is required:</p>

<h4>a. Design a multi-tier architecture solution</h4>
<p>Placing all the resources on the same machine creates availability and security risk. If your server got down, the application would be down, and it would not communicate with the database. In case of any attack on the server, and if you do not have a replica then you are at greater risk of data loss. So, Multi-tier architecture solves these problems by splitting data access across more than one server. By distributing all the resources spread into different servers your deployment performance would be boosted. Moreover, having different layers for different resources implies adding an extra security layer by separating data from code. If you have replication, the database can be replicated across more than one server which prevents the loss of data in case of cluster failure.</p>

<h4>b. Design highly available and/or fault-tolerant architectures</h4>
<p>High availability means that a system will almost always maintain uptime. Though sometimes it could be in a degraded status. Five nines is when your system has high availability at 99.999% uptime. It means that the system would be down only for a mere five minutes and fifteen seconds a year. System would be in a high availability status when it has fault tolerance. So, fault tolerance as high availability’s brother means that a system will almost always maintain uptime and users will not notice anything different during a primary system outage.</p>

<h4>c. Design decoupling mechanisms</h4>
<p>Loose coupling isolates the layers and components of your application so that each component interacts with the others independently. This is necessary if you want to enable scalability and want your system to become stateless. Therefore, by using a load balancer or a queuing system you can distribute components of your applications that perform different tasks.</p>

<h4>d. Choose appropriate resilient storage</h4>
<p>The Resilient Storage enables a shared storage or clustered file system to access the same storage device over a network through a pool of data that is available to each server in the group.</p>

<p>Nowadays all cloud providers have specific services for users to have a Resilient Architectures in an easiest way, but it would be possible through an on-prem environment by using different tools and services easily. For example one of the easiest ways is to use queuing and autoscaling services.</p>

<p><img src="https://atabakkh.github.io/img/resilient_architectures.jpg" width="650" height="220" /></p>

<p>Rather than using cloud or on-premises environments, preparing most decoupling architecture is the main target when you need a resilient solution. If you already integrated into cloud and wanted to rollout such solution into AWS for example, utilizing below services are very recommended:
EC2 Storage types, Elastic File System (EFS), Amazon Simple Storage Service (S3), Simple Queue Service (SQS), Elastic Load Balancer (ELB), Auto Scaling, etc.
<br />
<br /></p>
<h3>2. High-Performing Architectures</h3>
<p>When your application reaches to an enterprise level, considering a high-performance architecture is highly required. Therefore, certain metrics need to be followed:</p>

<h4>a. Identify elastic and scalable compute solutions for a workload</h4>
<p>Elasticity is to match resource allocation with actual amount of resources needed at any given point in time; and scalability handles the changing needs of an application within the confines of the infrastructure via statically adding or removing resources to meet applications demands if needed.</p>

<h4>b. Select high-performing and scalable storage solutions for a workload</h4>
<p>Scalable storage enables the architecture to increase the data capacity using a single repository rather than multiple sentinel servers. True scale-out storage does not only refer to the capacity of the systems to store more data. It should also provide the capability to sort out and find the data. Ultimately, the scalable storage solution should ensure flexibility, security, functionality, and reliability. As an example, you can use a POSIX (Portable Operating System Interface) object-based files, to manage the workloads. Moreover, Scaling data-intensive workloads on-premises typically involves purchasing more hardware.</p>

<h4>c. Select high-performing networking solutions for a workload</h4>
<p>High-performance networks (HPNs) play a role in real-time data processing requirements. For example, activities such as datacenter replication, datacenter disaster recovery, and high-performance distributed computing require high volume data transfer and low network latency. HPNs with dynamic connection capabilities make high-performance network resources more accessible and manageable.</p>

<h4>d. Choose high-performing database solutions for a workload</h4>
<p>It has always been a challenge for organizations with high-volume and complex data-management needs to find the database that offers the best fit. As enterprises increasingly mix and match on-premises and multi-cloud environments on stateless cloud native platforms, the performance bar that is required of databases becomes that much higher. In addition to improving data access performance, a good design achieves other benefits, such as maintaining data consistency, accuracy, reliability and reducing storage space by eliminating redundancies. Another benefit of good design is that the database is easier to use and maintain.</p>

<p><img src="https://atabakkh.github.io/img/high-performance_architectures.jpg" width="650" height="220" /></p>

<p>Many tools and services are available to give you a better opportunity to make your desired architecture. Therefore having API gateways, Load balancer, using a variety of web-service tools is possible easily. Moreover, in a cloud environment such as AWS utilizing the following services are very recommended: RDS, DynamoDB, Elasticache, CloudFront, APIGateway, etc.
<br />
<br /></p>
<h3>3. Secure Applications and Architectures</h3>
<p>Security must be implied within the architecture itself. It means that considering a secure architecture and design at the initial point of your planning would be required.</p>

<h4>a. Design secure access to resources</h4>
<p>To achieve security on resource access we need to consider many corners, but mainly focusing on the basic parameters such as follows is required:</p>

<ul>
<li>All services accessing a resource must be authenticated, authorized and audited.</li>
<li>Any secrets should never be kept with the service and must be easy to rotate.</li>
<li>All policies should be centralized and allow dynamic updates.</li>
<li>Escalated privileges should be granted on-demand and for a specified time.</li>
<li>All of the above should be managed as code and every action should be auditable.</li>
</ul>

<h4>b. Design secure application tiers</h4>
<p>This ensures that all business needs and performance goals of the application are achieved without any security incident when deployed within the production environment. It means that by considering following items you need to make sure you are using multi-tire (commonly 3 tiers) application architecture to handle the security metrics:</p>

<ul>
<li>user authentication</li>
<li>controlling user access</li>
<li>auditing user actions</li>
<li>protecting data</li>
<li>security between tiers</li>
<li>limiting privilege of the middle tier</li>
<li>managing identities across tiers</li>
<li>building scalable systems.</li>
</ul>

<h4>c. Select appropriate data security options</h4>
<p>Data security is the process of protecting corporate data and preventing data loss through unauthorized access. This includes protecting your data from attacks that can encrypt or destroy data, such as ransomware, as well as attacks that can modify or corrupt your data. Data security also ensures data is available to anyone in the organization who has access to it.
It includes data encryption, hashing, tokenization, and key management practices that protect data across all applications and platforms. In addition, it’s a concept that encompasses every aspect of information security from the physical security of hardware and storage devices to administrative and access controls, as well as the logical security of software applications.</p>

<p>Cloud environments mostly prepare many tools to help you have all security metrics in once. For example in AWS you can use: Identity and Access Management (IAM), Key Management Service (KMS), Customer Managed Key (CMK), CloudHSM, etc.
<br />
<br /></p>
<h3>4. Cost-Optimized Architectures</h3>
<p>Of course you might have wider options in clouds but having a proper architecture which is most cost-effective based on your application and usage is so important from a business point of view. The other word, cost is one of primary and critical items, which might stop your architecture from being developed.</p>

<h4>a. Identify cost-effective storage solutions</h4>
<p>With data volumes growing exponentially, many companies are looking to save money on their storage costs. You should know how to take cost into consideration when building your architectures.</p>

<h4>b. Identify cost-effective compute and database services</h4>
<p>You might store your data in a relational database to ease development and management tasks. When you launch your application, the database is manageable at first, but it grows to hundreds of gigabytes per week. Data storage and retrieval alone are consuming 20 percent of IOPS and CPU in your relational database instance. In addition, applications are storing XML, JSON, and binary documents in database tables along with transactional data. Historical data continues growing every month. Your traditional on-premises database licensing and infrastructure costs are increasing, and scaling the database has become a big challenge.
The data store that your application uses depends on its access pattern. It also depends on the scale at which you anticipate its data to grow and how readily you require access to the data.
Therefore, it is very important to have a proper database, not only to save the storage but also to save money, and reduce the cost.</p>

<h4>c. Design cost-optimized network architectures</h4>
<p>Most unplanned and hidden cost of an architecture especially in a cloud environment is from network loads, therefore having a proper network architecture would cover a big gap within the whole infrastructure costs.
A cost optimization assessment provides a snapshot of the existing network infrastructure, strengths and areas for improvements, which helps identify opportunities for planning and recognizes immediate cost savings between 5% to 30%. On average, best practices can generate additional productivity and efficiency savings of 5% to 20% throughout the entire network infrastructure environment.
<br />
<br />
In Conclusion, selecting and then designing a proper architecture has a direct impact on your cost, performance, security and many more metrics. Therefore, picking the basic foundation and then building on top of it can help on your platform to be consistent and highly available.
<br />
<br /></p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Cloud Migration in a simple way</title>
					
					<link>https://atabakkh.github.io/cloud/2021/11/13/cloudmigration.html</link>

					

					<pubDate>Sat, 13 Nov 2021 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/cloud/2021/11/13/cloudmigration</guid>

					
						<category><![CDATA[ Cloud ]]></category>
					

					<description><![CDATA[ How cloud migration would be in placed in a simple roadmap more clear underestanding. ]]></description>
					<content:encoded><![CDATA[ <h4>How to itemize cloud migration phases.</h4><h2>Cloud Migration in a simple way</h2>

<p>This page would bring the challenges, architecture and then proposed target to achieve through collaborative innovation migration, and build truly customer-centric businesses on cloud simultaneously. It includes preparation, focus, transform and consulting roadmap along with cloud-based services.
This would help hands-on thinking to engineer ideas that solve business problems, and enables the migration to rapidly plan, develop, design, architect, scale projects and resources including security and compliance with regulation that deliver immediate impact and sustainable growth.</p>

<p>The context  is a space for cross-functional executive teams. The architecture will bring together IT Operations and leaders, Infrastructure architectures, Information security architecture and all decision makers from the business facing challenges at different points of the cloud transformation journey.</p>

<p><b>Objectives</b></p>
<ul>
<li>Migrate an application into AWS Cloud</li>
<li>RTO and RPO in placed</li>
<li>Integrated with legacy SSO</li>
<li>Database migration</li>
<li>Reduction of Cost</li>
<li>Apply security, compliance and regulation</li>
</ul>

<h3>Cloud Migration</h3>

<p>The first phases of cloud migration starts with investigation of the existing platform across on-premise environments and also cloud services required for this migration. Proper planning and strategy would be in-placed as the next step of the roadmap in high level and details. Workload migration and implementation in an agility way, then Continuously Monitor, optimize and operate on infrastructure will be the next steps.
<br />
<br />
<img src="https://atabakkh.github.io/img/cloud_migration.png" width="750" height="250" /></p>

<p>Therefore the planning phases of the migration would be divided a bit more on 6 steps which the timeline also would be prepared based on below phases.
<br />
<br />
<img src="https://atabakkh.github.io/img/migration_phases.png" width="750" height="350" /></p>

<p>Moreover, the journey of this migration will be divided into more details staged as belows:
<br />
<br />
<img src="https://atabakkh.github.io/img/migration_journey.png" width="750" height="250" />
<br />
As stated earlier there are 4 main migration stages within the approach. A continuously improvement and development would be in placed until the target is achieved properly (CI/CD)
<br />
<br />
<img src="https://atabakkh.github.io/img/migration_keys.png" width="750" height="350" /></p>

<p>And the key Steps to Cloud Migration Process:
<br />
<br />
<img src="https://atabakkh.github.io/img/migration_process.png" width="750" height="300" /></p>

<h3>System Requirement</h3>
<p><img src="https://atabakkh.github.io/img/CLOUD.png" width="100" height="100" />
<br />
For a FinTech application like a bank or digital bank or payment gateways, a consideration of following points are critically important:</p>
<ul>
<li>Security</li>
<li>Performance</li>
<li>SLA</li>
<li>RTO &amp; RPO</li>
<li>Cost Effective</li>
<li>Fault tolerance and HA</li>
</ul>

<p>Based on the experiences with Fintech and IoT projects, there are many compliance and regulation for banking systems and payment gateways. Therefore, making sure all regulations are in place and certifications are satisfied, not only consideration of the items one by one is important, but also a third party tool to scan the entire system would be required.
Having said that, cloud migration should seamlessly have a main focus on cloud security and adjustment, high performance architecture, make sure RTO/RPO are in place and can rapidly handle load, traffic, failover and ad hoc requests.</p>

<h3>Cloud Security and Compliance</h3>
<p>To improve the security of the system and reduce the vulnerabilities, consideration of a wide range of items within each corner of the system and cloud is required:</p>
<ul>
<li><b>Reduce Attack Surface:</b> Security group, direct connect, Jumper &amp; VPN , WAF,  Anti-DDoS, etc</li>
<li><b>Reduce visibility and tracking:</b> Cloudfront, API Gateway mapping, KMS, etc.</li>
<li><b>DevSecOps and Automation:</b> CI/CD, Infra as Code, Change management, Code Review, etc.</li>
<li><b>Granular Privilege and Key Management:</b> IAM, KMS, CloudTrail, etc.</li>
<li><b>Compliance and Governance:</b> PCIDSS compliance, PCI 3.2, SSL/TLS Handshake, GDPR, etc.</li>
</ul>

<p>As the entire environment should be under VPC and security of it might be handled by security groups assigned to it, connecting to that VPC would be possible through VPN and a direct connect service between on-prem and cloud environments.
DNS and APIs should be covered under cloudfront and API Gateway. 
Infra as code should be widely used to make sure every single changes is manageable and reviewed by compliance team before deployment
SSL/TLS handshaking is used across api calls on top of application layers.
Using KMS to encrypt user and credit card tokens, KMS Allows to encrypt data within the application using the keys that you create and control.</p>

<h3>Performance</h3>
<p>Having a proper vcpu and memory for application is very important, especially when the system is not containerized yet and running in a windows server for example.</p>
<ul>
<li><b>Select appropriate instances:</b> vCPU &amp; memory, EBS, EFS, S3, ...</li>
<li><b>Autoscaling:</b> Enterprise ad hoc effort and on demand resources, ...</li>
<li><b>Caching service:</b> ElastiCache, Redis, ...</li>
<li><b>Adopt to microservice architecture:</b> Individually deploy, operate and scale, ...</li>
<li><b>Event-driven architecture:</b> Serverless computing, Lambda, long-time operation</li>
<li><b>Monitoring and notification:</b> Cloudwatch, APM, SNS, ...</li>
</ul>

<h3>SLA and SLO</h3>
<p><img src="https://atabakkh.github.io/img/sla.png" width="100" height="100" /><br />
SLO is tighter than the SLA. The SLOs are generally used for internal only, and the SLAs are for external. Initial target commonly starts from 99.5% availability.</p>
<ul>
<li><b>Service-Level Objective (SLO):</b> goal that service provider wants to reach. 
<ul><li>Monitoring, Tracking, CICD, Alerting</li></ul></li>
<li><b>Service-Level Agreement (SLA):</b> contract that the service provider promises customers.
<ul><li>Alerting, Monitoring</li></ul></li>
</ul>
<p>The matter here is the operation team must always be beyond the SLA level, that is why it is recommended to follow SLO for operations on cloud. Therefore monitoring and alerting systems would be triggered before hitting the threshold. It will help HA and RTO to always hit within the target.</p>

<h3>RTO and RPO</h3>
<p><img src="https://atabakkh.github.io/img/rto_rpo.png" width="100" height="100" /><br />
Response time and recovery time are 2 main key and advantage of using cloud when the architect is proper by using below services and targets:</p>
<ul>
<li><b>Recovery Time Objective (RTO):</b>
<ul><li>Backup, DR, Multi-zone application and database.</li></ul></li>
<li><b>Recovery Point Objective (RPO):</b>
<ul><li>DR, Multi-zone application and database, autoscaling.</li></ul></li>
</ul>

<p>The matter of recovery is to have redundancy for every critical service within the platform and architecture. For example to hit RTO within 1 day and RPO in 15 minutes, we need to make sure:</p>
<ul>
<li>We have a proper architecture across multi-zones,</li>
<li>We do have database backup more frequent,</li>
<li>We are using shared storage and disc to be usable by other instances.</li>
<li>Autoscaling and load balancing is in-placed </li>
<li>Monitoring is everywhere as much as possible </li>
</ul>

<h3>Cost Effective</h3>
<p><img src="https://atabakkh.github.io/img/Cost-cutting-min.png" width="100" height="100" /><br />
Even though it’s a time-consuming process, the cloud can provide extensive financial benefits like budget estimation and planning, budget savings and increased workplace productivity. 
In fact, companies can save an average of 15 percent on all IT costs by migrating to the cloud, and getting benefits like:</p>
<ul>
<li>On-demand services</li>
<li>Autoscaling</li>
<li>Reduces the necessary amount of hardware (CAPEX)</li>
<li>Less demanding labor and maintenance (OPEX)</li>
<li>Higher productivity </li>
<li>Pay as you go ( lower initial capital investment)</li>
<li>etc.</li>
</ul>

<p>Cloud price calculators will help to estimate the yearly or quarterly cost, therefore, by adjusting the services and finding proper on-demand usage, managing the cost would be easily achievable. 
It is very important to plan the migration strategy properly, then manage the cost. Importantly, cloud solutions are available in a pay-as-you-go pricing model. This format provides savings and flexibility in several ways</p>

<h3>High Availability</h3>
<p><br />
Using a clustered architecture within the cloud is one of the solutions you can achieve High Availability. A high availability cluster is a group of servers that act as a single server to provide continuous uptime. These servers will have access to the same shared storage for data, so if a server is unavailable, the other servers pick up the load. A high availability cluster can be anything from two to dozens of servers. As well as providing failover, high availability clusters also allow auto-scaling and load balancing of workloads so that any server within the cluster will not get overloaded and you can provide more consistent performance. So the basic elements of high availability are as follows:</p>
<ul>
<li><b>Redundancy:</b> ensuring that any elements critical to system operations have an additional, redundant component.</li>
<li><b>Monitoring:</b> collecting data from a running system and detecting when a component fails or stops responding.</li>
<li><b>Failover:</b> a mechanism that can switch automatically from the currently active component to a redundant component</li>
</ul>

<h4>Moreover, components enabling high availability are as follows:<h4>
<ul>
<li>Data backup and recovery</li>
<li>Load balancing</li>
<li>Clustering</li>
</ul>

<h4>Define availability metrics</h4>
<li>Percentage of Uptime</li>
<li>Mean Time to Recovery (MTTR)</li>
<li>Mean Time between Failures (MTBR)</li>
<li>Recovery Time Objective (RTO)</li>
<li>Recovery Point Objective (RPO)</li>
&lt;/ul&gt;

Most of the systems need a High-Performing Architecture, which can satisfy Security, High Availability and fast Failover. This will help to reduce RTO and RPO as main requirements. For example below items are required in the first phase to have high availability in the beginning:
<br />
<img src="https://atabakkh.github.io/img/cloud-computing-ha.png" width="100" height="100" />
<ul>
<li>Shared storage</li>
<li>Load-balancing</li>
<li>Autoscaling</li>
<li>Multi-zone database</li>
<li>API Gateway</li>
<li>Security services</li>
<li>Monitoring services</li>
</ul>

If it was IoT for example, better to use queue and some DB like DynamoDB, but for Fintech, performance and false tolerance are the main items as HA, therefore, using RDS would be a more suitable choice. At the same time changing the architecture into a more reliable and cost effective way and containerizing them then utilizing EKS and ECR would help more on target achievement.

<h3>Cloud Operations</h3>
<br />
The key is to get use of a proper DevOps culture for operation of new architecture in the cloud to achieve a correct agility. This would be possible by integrating Technology, Process, People and Environment.
<br />
<img src="https://atabakkh.github.io/img/cloud_operation.png" width="350" height="350" />
<ul>
<li>Adaptation of Cloud technologies</li>
<li>Break up application architecture into more service-based architectures</li>
<li>Find evolving orchestration that works in our organization</li>
<li>Deployment</li>
<li>Reliable</li>
<li>Quickly Validated</li>
<li>Reversible</li>
<li>Replace an approval with a notification</li>
<li>Technology vendors</li>
<li>Service Providers</li>
</ul>
<br />
<img src="https://atabakkh.github.io/img/cloud-operations-detail.png" width="100" height="100" />
<br />
With a combination of cloud operation services and some extra tools (open source or subscription based) we can make seamless cloud operations. 

<ul>
<li><b>CI/CD in placed and easier in cloud</b>
<ul><li>CodeDeploy, Jenkins, etc.</li></ul></li>
<li><b>Monitoring</b>
<ul><li>CloudWatch, Grafana, Zabbix, APM, etc.</li></ul></li>
<li><b>Infra as code</b>
<ul><li>Terraform, Ansible, etc. </li></ul></li>
<li><b>Change management</b>
<ul><li>Jira, Word, etc.</li></ul></li>
<li><b>Alerting</b>
<ul><li>SNS, CloudTrail</li></ul></li>
</ul>

<h3>Cloud Operations</h3>
<br />
Pricing is calculated based on Compute, Storage, and Data transfer, therefore budget estimation and planning, budget savings is possible based on cloud cost calculator. For this architecture focusing on belows are recommended: 

<ul><li>When to use reserved instances over on-demand or spot instances. </li>
<li>Redundancy </li>
<li>TPS</li>
<li>Database size</li>
<li>For a pilot project using pay-as-you-go is also recommended</li>
</ul>
As stated earlier, calculating and budget saving is possible through managing service utilization, based on using exactly what you need, and not reserve any useless services. 

<h3>Migration Checklist</h3>
<br />
After confirmation on the architecture a kick-off migration checklist is required with can be extracted from a high level migration checklist accordingly
<br />
<img src="https://atabakkh.github.io/img/migration_checklist.png" width="750" height="400" />
<br />
<br /><br />
<h3>Build Cloud Strategy and Benefits</h3>
As a very quick overview, we can see the benefits of cloud across proposed architecture 
<br />
<br />
<img src="https://atabakkh.github.io/img/migration_strategy_benefit.png" width="750" height="250" />
<br />
<br />
<h3>Migration Triggers</h3>
<br />
In another way, the migration process which trigger for a platform would be summarized as follows:
<br />
<br />
<img src="https://atabakkh.github.io/img/migration_triggers.png" width="750" height="350" />
<br />

<h3>Cloud Migration Timeline</h3>
<br />
<br />
In general a sample timeline for such migration would be itemized as follows:
<br />
<br />
<img src="https://atabakkh.github.io/img/migration_timeline.png" width="850" height="250" />
<br />
<br />

In conclusion, below is a simple example for a general optimized architecture in AWS cloud: 
<br />
<img src="https://atabakkh.github.io/img/cloud_arch_sample.png" width="850" height="450" />
<br />
<br />
Eventually, this migration would be itemized in a few phases, which would help the migration to be concluded in a more proper architecture as it needs. It would help to cover many gaps within your requirement documents. To make a proper clustered environment using EKS, and Container services hosted in ECR:
<ul>
<li>It will help to improve HA even higher that previous one</li>
<li>Easily doing continuous improvement and delivery</li>
<li>In case of deployment and rollback, it would be happen within a couple of minutes when you have container versions in ECR</li>
<li>Pods, and Microservices can work as stand alone and scalability would be per services, and not only helping the scalability and HA but also reduce the cost</li>
<li>Same container and package can be tested in sandbox or staging environment before reaching to the production</li>
<li>Resources can be managed easier per services requirement</li>
<li>Security would be higher by reducing the attack surfaces, visibility and increase automation.</li>
</ul>
</h4></h4>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Digital Transformation with Agile + DevOps Culture</title>
					
					<link>https://atabakkh.github.io/agile/2021/05/07/digitalTransformation.html</link>

					

					<pubDate>Fri, 07 May 2021 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/agile/2021/05/07/digitalTransformation</guid>

					
						<category><![CDATA[ Agile ]]></category>
					

					<description><![CDATA[ How hybrid Agile + DevOps impose End-to-End responsibility and foster collaborative team. ]]></description>
					<content:encoded><![CDATA[ <h4>How Foster a Collaborative Environment.</h4><h2>Digital Transformation with Agile + DevOps Culture</h2>

<p>Waterfall is dead, and traditional software development lifecycle (SDLC) won’t support rapid development. Clients expect quick turnaround on products specially within enterprise level and technologies like cloud changed expectations. Agile speed up the delivery rate of the product and more releases that are smaller in size. Each release has multiple short sprints, then you will receive quick information from client feedback.</p>

<p><b>Agile is a good start, </b> Getting close, but we can get quicker, and need to focus on nondevelopment activities accordingly.</p>

<p><b>Devops brings groups together, </b> No more separated teams, and it Will typically include an agile-type approach to development. Therefore, not only does development move quickly, so the entire release; plus, empower teams to closely define, develop, and release the product.
<br />
<img src="https://atabakkh.github.io/img/devopsall2.png" width="550" height="350" /></p>

<p>DevOps is a large culture change. It requires a skilled team, not all tester can code; not all coders can admin Linux. Everything needs to happen quickly. Automation in an existing environment is usually a lacking and security is not immune to the effects of DevOps</p>

<p><img src="https://atabakkh.github.io/img/allinone2.png" width="250" height="250" /></p>

<p><b>Enterprise DevOps: </b> 
At enterprise scale, everything becomes more difficult. Changing a culture which has been created unconsciously over the years would be the most critical part.</p>

<p><img src="https://atabakkh.github.io/img/enterprisesolution.png" width="250" height="250" /></p>

<p>Many vendors and stakeholders are involved, and they are incentive to worry about, different vendor contracts. All in different timeline and value add. Therefore, we should learn that, to change the culture need to work in many elements, and it is not possible just by announcing a new principles and values like cost, communication or new agreements. Indeed, it needs to work on process and behaviors as well.</p>

<p><img src="https://atabakkh.github.io/img/timeline.png" width="600" height="190" /></p>

<p><b>Laloux’s Model</b>
If you have a look on Laloux’s model you will find 5 colors or development styles:</p>
<ul>
<li>Red: it powered by individual </li>
<li>Amber: it is the typical command and control hierarchies (similar to the militaries) </li>
<li>Orange: competition of success is the underlive driver </li>
<li>Green: focusing on the empowerment of the employees, a focus shift from shareholders to stakeholders (family business) </li>
<li>Teal: A culture of full served organization, very much like a living organism </li>
</ul>

<p>Most of the organizations have people and organization that it is more closed to Amber and Orange culture. Migrating from amber to orange would be main issue across team, so it is best practice to focus on master orange and then automate everything (like deployment process) rather than achieve to green directly. And then empowering the team members to decide how to deploy, when to deploy and how improve the process continuously in a GREEN culture.</p>

<p>The culture is the significant key of the journey. So, it drives us in a try and error until found own culture and roadmap. Indeed, to be successful in DevOps enterprise level we need to think about 3 main dimensions such as:</p>
<ul>
<li>Technology</li>
<li>People and Process</li>
<li>Ecosystem</li>
</ul>

<h3>Technology dimensions</h3>
<ul>
<li>Cover Test automation</li>
<li>Adaptation of Cloud technologies</li>
<li>Break up application architecture into more service-based architectures</li>
<li><b>Speed is dependent on agility</b></li>
</ul>

<h3>People and Process Dimensions</h3>
<ul>
<li>Find evolving orchestration that works in our organization</li>
<li>Reliable Deployment</li>
<li>Quickly Validated</li>
<li>Reversible</li>
<li>Replace an approval with a notification</li>
</ul>

<h3>Ecosystem Dimension</h3>
<ul>
<li>Technology vendors</li>
<li>Service Providers</li>
</ul>

<p>But it was always main challenge to define the best Key indicators in an enterprise level. Many vendors and stakeholders will always make it important to find the bottlenecks and define the KPI based on efficient Cost and Times. Use a KPI to evaluate the progress:</p>
<ul>
<li>Mean time to recover</li>
<li>Deployment cycle time</li>
<li>Deployment failure rate</li>
</ul>

<p><b>Transforming is a continual process</b>, As long as the business results is improved and getting better, we are on track.</p>

<h4>Continuous Improvement (Checklist Activity) </h4>
<ul>
<li>Identify</li>
<li>Track</li>
<li>Plan: Define right measures</li>
<li>Act: Discipline and Flexibility</li>
</ul>
<p><b>Tip: We need to rethink about the process to decrease defects. </b> Each improvement is an experiment that we hope will positively influence the capabilities, but it might not always work out in our favor.</p>

<p><b>Improvement Cycle Template</b></p>
<ul>
<li>We believe "this capability"</li>
<li>Will result in "that outcome"</li>
<li>We will know we have succeeded when " we see a measurable signal"</li>
</ul>

<p>In conclusion, by merging DevOps into Agile technology, we will not only gain the benefits internally, but our customers would achieve significant metrics:</p>

<p><img src="https://atabakkh.github.io/img/customerbenefitcycle.png" width="250" height="250" /></p>

<h4>Speed:</h4>
<ul>
<li>Innovate for customers faster</li>
<li>Adapt to changing markets better</li>
<li>Grow more efficient at driving business results. </li>
</ul>
<p>The DevOps model enables your developers and operations teams to achieve these results. For example, microservices architecture and continuous delivery let teams take ownership of services and then release updates quicker and frequently.</p>

<h4>Rapid Delivery:</h4>
<ul>
<li>Innovate and improve the product faster</li>
<li>The quicker we can release new features and fix bugs, the faster we can respond to your customers’ needs and build competitive advantage</li>
</ul>
<p>Continuous integration and continuous delivery are practices that automate the software release process, from build to deploy.</p>

<h4>Reliability:</h4>
<ul>
<li>Ensure the quality of application updates and infrastructure changes</li>
<li>maintaining a positive experience for end users. </li>
</ul>
<p>Use practices like continuous integration and continuous delivery to test that each change is functional and safe. Monitoring and logging practices help you stay informed of performance in real-time.</p>

<h4>Improved Collaboration:</h4>
<ul>
<li>Build more effective teams under a DevOps cultural model</li>
<li>Emphasizes values such as ownership and accountability. </li>
<li>Developers and operations teams collaborate closely, share many responsibilities, and combine their workflows. </li>
</ul>
<p>This reduces inefficiencies and saves time (e.g., reduced handover periods between developers and operations, writing code that takes into environment account in which it is run).</p>

<h4>Security:</h4>
<ul>
<li>Move quickly while retaining control and preserving compliance</li>
</ul>
<p>Adopt a DevOps model without sacrificing security by using automated compliance policies, fine-grained controls, and configuration management techniques. For example, using infrastructure as code and policy as code, you can define and then track compliance at scale.</p>

<p><br /><br /></p>
<h3>Summary of Overcome Challenges</h3>
<ul>
<li>Foster a Collaborative Environment</li>
<li>Impose End-to-End Responsibility</li>
<li>Encourage Continuous Improvement</li>
<li>Automate (Almost) Everything</li>
<li>Focus on the Customer’s Needs</li>
<li>Embrace Failure, and Learn From it</li>
<li>Unite Teams - and Expertise</li>
</ul>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Cloud Cost Is an Architecture Problem, Not a Finance Problem</title>
					
					<link>https://atabakkh.github.io/finops/2021/03/20/cloud-cost-is-architecture-problem.html</link>

					

					<pubDate>Sat, 20 Mar 2021 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/finops/2021/03/20/cloud-cost-is-architecture-problem</guid>

					
						<category><![CDATA[ FinOps ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>Finance sees the invoice. Architecture creates it. The durable cost improvements I saw did not come from better spreadsheets - they came from changing how data was partitioned, scheduled, and consumed.</em></p>

<p><strong>Context:</strong> By early 2021, accelerated cloud adoption had left many organizations with bills that finance could not explain by product or domain. Cost-management tools improved visibility, but visibility alone did not create sustainable savings. Pipelines, warehouses, retention policies, and scaling behavior still had to change.</p>

<hr />

<h2 id="why-finance-only-finops-stalls">Why finance-only FinOps stalls</h2>

<p>Monthly reviews can flag anomalies, allocate spend roughly, and enforce budgets after the fact. They cannot fix a nightly job that scans 200 TB because nobody added a partition filter. They cannot merge three duplicate pipelines owned by three teams. They cannot decide that a daily decision does not need an hourly job.</p>

<p>Those are architecture decisions with a cost attached.</p>

<hr />

<h2 id="where-the-money-usually-goes-in-data-platforms">Where the money usually goes in data platforms</h2>

<p><img src="/img/diagrams/cloud-cost-levers.svg" alt="Cloud data cost levers diagram" width="750" /></p>

<table>
  <thead>
    <tr>
      <th>Driver</th>
      <th>Lever that actually moved the number</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Warehouse scan volume</td>
      <td>Partitioning, clustering, serving tables</td>
    </tr>
    <tr>
      <td>Job frequency</td>
      <td>Match schedule to how fresh the decision needs to be</td>
    </tr>
    <tr>
      <td>Storage growth</td>
      <td>Retention, compaction, lifecycle</td>
    </tr>
    <tr>
      <td>Orchestration fan-out</td>
      <td>Fewer duplicate extracts, cleaner DAGs</td>
    </tr>
    <tr>
      <td>Over-provisioned compute</td>
      <td>Right-size; scale on p95, not on peak panic</td>
    </tr>
    <tr>
      <td>Egress / cross-region</td>
      <td>Locality by design</td>
    </tr>
  </tbody>
</table>

<p>Practical example - cap a BigQuery job at 10 GB from the command line:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bq query <span class="se">\</span>
  <span class="nt">--use_legacy_sql</span><span class="o">=</span><span class="nb">false</span> <span class="se">\</span>
  <span class="nt">--maximum_bytes_billed</span><span class="o">=</span>10000000000 <span class="se">\</span>
  <span class="nt">--parameter</span><span class="o">=</span><span class="s1">'start:DATE:2021-03-01'</span> <span class="se">\</span>
  <span class="nt">--parameter</span><span class="o">=</span><span class="s1">'end:DATE:2021-03-31'</span> <span class="se">\</span>
  <span class="s1">'SELECT *
   FROM `curated.fact_events`
   WHERE event_date BETWEEN @start AND @end'</span>
</code></pre></div></div>

<hr />

<h2 id="what-i-started-putting-into-architecture-reviews">What I started putting into architecture reviews</h2>

<ol>
  <li><strong>Expected bytes scanned per run</strong> - estimate before deployment</li>
  <li><strong>Cost-to-serve per domain</strong> - attribute pipelines to a product or business unit</li>
  <li><strong>Freshness versus schedule</strong> - hourly processing for a daily decision is waste</li>
  <li><strong>Materialize versus recompute</strong> - calculate repeated heavy joins once</li>
  <li><strong>Decommission plan</strong> - parallel legacy and cloud systems double spend</li>
</ol>

<p>Engineers should see cost estimates the way they see latency estimates.</p>

<hr />

<h2 id="guardrails-that-stuck">Guardrails that stuck</h2>

<p>Architecture alone is not enough. We also needed:</p>

<ul>
  <li>Per-query byte caps on production service accounts</li>
  <li>Budget alerts at 50%, 90%, and 100%, with domain tags</li>
  <li>Terraform limits on expensive resource types</li>
  <li>Reservations for steady baseline after right-sizing</li>
</ul>

<p>Guardrails stop the bleeding. Redesign stops the wound.</p>

<hr />

<h2 id="habits-that-helped-without-turning-into-blame">Habits that helped without turning into blame</h2>

<p>Show teams the marginal cost of a new pipeline. Celebrate deletion and retention wins, not only launches. Pair FinOps practitioners with platform architects each month. Put cost next to freshness SLOs on the same dashboards.</p>

<p>Finance owns the budget. Engineering owns the shape of the spend.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>Cloud cost is an architecture problem because scan patterns, schedules, layers, and retention are design choices - not billing surprises. Architecture has to move first. FinOps keeps it honest.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>The Technical Leader's Trap: Solving Everything Yourself</title>
					
					<link>https://atabakkh.github.io/agile/2020/11/05/technical-leaders-trap-solving-everything-yourself.html</link>

					

					<pubDate>Thu, 05 Nov 2020 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/agile/2020/11/05/technical-leaders-trap-solving-everything-yourself</guid>

					
						<category><![CDATA[ Agile ]]></category>
					

					<description><![CDATA[  ]]></description>
					<content:encoded><![CDATA[ <h4></h4><p><em>I became a lead because I could unblock anything under my responsibility. Then I became the bottleneck for everything while the team waited.</em></p>

<p><strong>Context:</strong> In 2020, many senior engineers were working remotely for the first time while teams depended heavily on chat, calls, and a few people who held critical knowledge. I was in an awkward middle: officially a lead, but still the default solver for migrations, pipelines, and incidents. The organization called it technical leadership. It often felt like hero mode with a new title.</p>

<p>This trap is common. Working harder does not fix it.</p>

<hr />

<h2 id="signs-i-was-in-it">Signs I was in it</h2>

<p>Slack DMs were my workflow system. I was in every architecture review “just in case”. Tickets stalled until I commented. I debugged at night; the team waited by day. I told myself delegation takes longer - for the 400th time. Burnout felt like responsibility.</p>

<p>If the team cannot ship when you are on vacation, you are not leading. You are load-bearing in the wrong place.</p>

<hr />

<h2 id="why-orgs-push-you-there">Why orgs push you there</h2>

<p>I had rewarded the behavior: every fast firefight taught people to route the next fire to me. Success metrics were vague (“keep the platform running”), ownership was never distributed, and managers confused senior individual-contributor output with lead output. Honestly, technical problems were clearer than people problems. That preference had a career cost.</p>

<hr />

<h2 id="solver-vs-leader">Solver vs leader</h2>

<p><img src="/img/diagrams/solver-vs-leader.svg" alt="Solver versus leader: personal reliability versus team reliability" width="750" /></p>

<table>
  <thead>
    <tr>
      <th>Solver</th>
      <th>Leader</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fastest fix wins</td>
      <td>Repeatable fix wins</td>
    </tr>
    <tr>
      <td>“I’ll take this”</td>
      <td>“Who owns this class of problem?”</td>
    </tr>
    <tr>
      <td>Answer the question</td>
      <td>Leave a way for others to answer next time</td>
    </tr>
    <tr>
      <td>Be right in the meeting</td>
      <td>Leave with a decider and an owner</td>
    </tr>
    <tr>
      <td>Depth on everything</td>
      <td>Depth on risks; breadth on ownership</td>
    </tr>
    <tr>
      <td>Personal reliability</td>
      <td>Team reliability</td>
    </tr>
  </tbody>
</table>

<p>Output shifts from tasks completed to capacity created.</p>

<hr />

<h2 id="what-i-did-over-about-30-days">What I did over about 30 days</h2>

<table>
  <thead>
    <tr>
      <th>Week</th>
      <th>What I did</th>
      <th>What it revealed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Tracked every interrupt for five days</td>
      <td>Most “decisions” were missing standards</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Published 3 docs and linked instead of DM replies</td>
      <td>Recurring answers were undocumented</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Delegated with context, constraint, checkpoint, authority</td>
      <td>“Look at this when you have time” was not delegation</td>
    </tr>
    <tr>
      <td>4</td>
      <td>Redesigned the calendar for systemic work</td>
      <td>Interrupts were signals about missing systems</td>
    </tr>
  </tbody>
</table>

<p><strong>Week 1.</strong> Buckets: decision, information, execution, support. Most “decisions” were missing standards.</p>

<p><strong>Week 2.</strong> Replaced three recurring answers with docs - runbook section, architecture checklist, how we choose freshness. Linked instead of replied.</p>

<p><strong>Week 3.</strong> Delegated with context, constraint, checkpoint, authority:</p>

<blockquote>
  <p>You own the reconciliation fix. Budget 3 days. Must pass finance parity. Ping me only if the variance model changes.</p>
</blockquote>

<p>Not: “look at this when you have time.”</p>

<p><strong>Week 4.</strong> Redesigned the calendar for systemic work: ownership gaps, SLOs, hiring, and cross-team agreements. I started treating interrupts as signals about missing systems.</p>

<hr />

<h2 id="what-i-needed-from-my-manager">What I needed from my manager</h2>

<p>Cover for the short velocity dip while I delegated. Back me when the team pushes back (“ask the runbook first”). Measure lead success differently: team throughput, repeat incidents, bus factor. Staff the platform - one lead cannot replace missing headcount forever.</p>

<p>If the manager only celebrates personal saves, they are incentivizing the trap.</p>

<hr />

<h2 id="hard-conversations">Hard conversations</h2>

<p>With manager: <em>I am the bottleneck on X. I need to transfer Y and Z this quarter. Here is the risk if we do not.</em></p>

<p>With team: <em>I will not be first responder on this class of issue anymore. Here is the escalation path.</em></p>

<p>With stakeholders: <em>I can join this review if we assign an owner. Otherwise we are repeating last month.</em></p>

<p>Uncomfortable - because you were promoted for the opposite behavior.</p>

<hr />

<h2 id="closing">Closing</h2>

<p>The trap is solving everything yourself until the team and platform depend on you personally. What helped: standards, delegation with authority, measuring team capacity, accepting that the job is to make yourself less necessary for daily execution - not more heroic.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Accountability on Resilience Engineering</title>
					
					<link>https://atabakkh.github.io/agile/2020/04/25/accountabilityonresilienceengineering.html</link>

					

					<pubDate>Sat, 25 Apr 2020 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/agile/2020/04/25/accountabilityonresilienceengineering</guid>

					
						<category><![CDATA[ Agile ]]></category>
					

					<description><![CDATA[ How accountability cross engineers would be consider in a better Resilience Engineering approaches. ]]></description>
					<content:encoded><![CDATA[ <h4>How accountability will be proposed in Resilience Engineering approach.</h4><h5>Accountability on Resilience Engineering</h5>

<p>Resilience Engineering defines safety as the capability to be successful under varying conditions. That is not possible only by studying things that go right as things that go wrong. It is to understand the normal functioning of a technical system for knowing how it fails and how it succeeds. It is easier and effective to increase safety enhancement of the number of things that go right, than by reducing the number of things that go wrong.</p>

<p>Resilience has more concern on 4 ability elements:</p>

<p><b>1- Responding: </b> Ability to address the Actual and knowing what to do, that is, how to respond to regular and irregular disruptions and disturbances either by setting up a proper set of responses or by adjusting normal functioning.</p>

<p><b>2- Monitoring: </b> Ability to address the Critical and knowing what to look for, that is, how to effectively monitor each corner and what is or can become a threat in the near term? The monitoring capability must show both that which happens in the environment and that which happens in the system itself, that is, its own performance.</p>

<p><b>3- Anticipating: </b> Ability to address the Potential  and knowing what to expect, that is, how to anticipate developments, threats, and opportunities further into the future. This has a main focus on potential changes, disruptions, pressures, and their consequences.</p>

<p><b>4- Learning: </b> Ability to address the Factual and knowing what has happened, that is, how to learn from experience, in particular how to learn the right lessons from the right experience either successes or failures.</p>

<p><img src="https://atabakkh.github.io/img/4concernsofresilience.png" width="370" height="370" /></p>

<p><b>Traditionally, the main focus was on which has gone wrong or could go wrong. </b></p>

<p>This sentence comes from the main definition of safety as “freedom from unacceptable risk” and the risk matrix only looks at things that can go wrong. However, it is clear that things can go right as well as wrong. Obviously things go right when they have planned or intended and that is unusual for the thing to go wrong. Therefore, safety efforts traditionally focused on unwanted or negative outcomes, and have relatively have been limited to low probability outcomes, such as accidents or incidents.</p>

<p>Normally, unwanted negative outcomes with high probability have been eliminated, otherwise the system would not be able to maintain its main functioning. If we assume that there is a simple relation between events and outcomes, characterizing several subsets of outcomes should be a must:</p>

<table class="table">
  <thead>
    <tr>
      <th scope="col">#</th>
      <th scope="col">Things that go right (wanted outcomes)</th>
      <th scope="col">Things that go wrong (unwanted outcomes)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Outcomes with high predictability</th>
      <td>Set of outcomes that represent the normal functioning of a safe system.</td>
      <td>The minor unwanted outcomes are usually tolerated and serious outcomes are normally eliminated</td>
    </tr>
    <tr>
      <th scope="row">Outcomes with low predictability</th>
      <td>Not normally considered in system management, but should obviously be facilitated.</td>
      <td>The focus of traditional safety efforts and the subject of risk assessment, prevention, and protection.</td>
    </tr>
  </tbody>
</table>

<p><b>Therefore, Resilience Engineering sees the “outcomes that go wrong” as the flip side of the “outcomes that go right” and assumes that they are a result of the same underlying processes. In consequence of that, “outcomes that go right” and “outcomes that go wrong” should be explained in basically the same way. </b></p>

<p>However, it makes sense to understand why things go right as to understand why they go wrong, because there are many more things that go right than things that go wrong. If for instance, the probability of failure is 1/10,000, then humans are usually blamed for 80-90 percent of the one case out of 10,000 when things go wrong. In the same way, humans should be praised for a similar 80-90 percent of 9.999 cases where nothing goes wrong. <b> In both cases humans should actually be seen as accountable for the full 100 percent.</b></p>

<p>In conclusion, Resilience Engineering proposes that we should try to understand a general view of a system’s performance, rather than limit ourselves to the things that go wrong, that is, try to understand all the outcomes rather than only the negative ones with the possible exception of “good luck”. <b> In one word Resilience Engineering recognises this paradox and argues that safety should deal with safe performance as well as unsafe performance with things that go right as well as things that go wrong. </b></p>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Team structure</title>
					
					<link>https://atabakkh.github.io/agile/2020/04/11/microteamasaservice.html</link>

					

					<pubDate>Sat, 11 Apr 2020 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/agile/2020/04/11/microteamasaservice</guid>

					
						<category><![CDATA[ Agile ]]></category>
					

					<description><![CDATA[ How team structure would help development team and business to grow faster and achive the target rapidly. ]]></description>
					<content:encoded><![CDATA[ <h4>What might be the best structure for a product development team?</h4><h5>Agility on product</h5>

<p>To formally define Agile through product development, Google describe as: ‘relating to a method of product management, used especially for software development, that is characterized by the division of tasks into short phases of work and frequent reassessment and adaptation of plans.’</p>

<p>Structure of development team would help the product development to know the size of phases and adaptation. It would be a key area on agility development and this structure would be depend on few points:</p>

<p><b>1- Product Maturity / Market -</b> Sometimes the product being developed is a variant, then even with complex architecture a simple team structure might suffice. However the size of the team is based on size of product and modules inside it.</p>

<p><b>2- Complexity of the product architecture -</b> Complexcity of the product architucture has a direct link to the team structure. The more complex the product in terms of its architecture the more complicated the team structure would be. The architecture does not just mean the physical nature, but also the logic behind the services, the modes of interaction and information transfer between different sub systems. The complexity in terms of reliability requires you to have teams and departments within depth specialty skills, This means using multifunctional team means that a team comprises of a varity of required skills individually. These teams may be ancillary and may be active in very specific time period of product development cycle.</p>

<p><b>3- Maturity of the Organization -</b> Generally the structure of a mature organization (which has seen multiple product development cycles) closely mimics product architecture and mostly exploring the duality between organization and product architectures (similar with a test on mirroring hypothesis). Therefore it is likely that the team has experts in each of the subsystem and the various modes of interaction. Plus, there should be a central integration team that ensure that all modules and interaction being developed are as per specification, and deviations are managed and communicated to the right individuals. The important of this middleware team it is not only on application architecture but also in solution architecture, specially on distributed systems that called a DevOps culture.
However, for an extremely novel or a startup or maybe disruptive product this duality wont hold good as the elements of product itself are evolving and different members might actually work on more than one area of product. This duality or better to use reusability cross products would help to bring the cost down and allow for better resource management over time.</p>

<p><b>Agile software development methology help on team structure and suitable product architucture. </b></p>

<p>As much as the teams are close to eachother and have better and comfortable comminucation the agility is observing better. As much as the product architecture and integration is more simple and clear, the product would develop faster.</p>

<p>Software development management is about revolutionizing the way business is conducted and proofing your organization for continuously changing internal factors, new technologies, external competitors and industry trends. An agile approach is to rolling out new initiatives across the business direction, and ensure all teams involved can quickly adapt and deliver values often.</p>

<p>As the architecture of the product would be a microservice architecture and services rely on each others, each segment of the team structure also would rely on accountability cross skillset resources. The proven microservice design and architecture nowadays help product and development team to understand eachother better. However, make sure your microservices working in the same room, and then scale up based on traffic on that particular services (backend, fronend, devops, etc.).</p>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Containerize dotnet project</title>
					
					<link>https://atabakkh.github.io/devops/2019/10/27/containerizedotnet.html</link>

					

					<pubDate>Sun, 27 Oct 2019 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/devops/2019/10/27/containerizedotnet</guid>

					
						<category><![CDATA[ Devops ]]></category>
					

					<description><![CDATA[ containerize and configuration of ASP dotnet core microservices using docker ]]></description>
					<content:encoded><![CDATA[ <h4>Config and build dotnet core project on docker container</h4><p>Most of the time, dotnet developers setup and configure a windows server and then occupy the entire server for a single project or a micro service project using web services. Then handling and maintaining the server and IIS inside the server would cost a lot of load on server maintainance.</p>

<p>Contianerize environment would help devops culure and development to push CI/CD in an easiest and fastest way.
dotnet developers are one of the development team which will go through package managers and container based architecture as last option.</p>

<p>Here I tried to share some playing on docker containers to setup a dotnet project with out gui, and then another environment who has a dashboard using <code class="language-plaintext highlighter-rouge">angularjs</code>.</p>

<p>we assume you have already installed docker and you are familiar with docker concepts.
As an example, We have a project contain 2 sub-projects (service, dashboard).</p>

<p>We will start by creating 2 containers for each sub-projects using different port and then run together.</p>

<h3>Configure Dockerfile</h3>

<p><b>configure dockerfile for services</b></p>

<p>let’s create a <code class="language-plaintext highlighter-rouge">Dockerfile</code> inside the project folder and name as <code class="language-plaintext highlighter-rouge">DockerfileService</code>.
Since the Dockerfile might be big and you only need to update the code with in the container, so we prepare a dockerfile using stage based:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build-env
WORKDIR /app
COPY . ./
WORKDIR /app/services
RUN dotnet restore
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS runtime
WORKDIR /app/services
COPY --from=build-env /app/services/out .
RUN mkdir -p /app/services/out/logs
ENV ASPNETCORE_ENVIRONMENT=Staging
ENV ASPNETCORE_URLS http://*:8888
EXPOSE 8888
ENTRYPOINT ["dotnet", "services.dll"]
</code></pre></div></div>

<p>As you can see the first image will prepare an image using dotnetcore sdk only then will restore and publish the project to prepare the DLLs and runnable files.</p>

<p>second image will prepare an aspnet:2.2 as webservice and then host the components and files build via previous image. The container would assign 2 variables and then expose the port on 8888.</p>

<p>Note: you can create Dockerfile inside each sub-project and then remove <code class="language-plaintext highlighter-rouge">services</code> from the path.</p>

<p>Now, we can build and image by running below command:</p>

<p><code class="language-plaintext highlighter-rouge">docker build -t myservice -f DockerfileService .</code></p>

<p>It will generate a docker image called <code class="language-plaintext highlighter-rouge">myservice</code>.</p>

<p><b>configure dockerfile for dashboard</b></p>

<p>Making a docker file for dashboard is a bit different since we might need to run different modules like nodejs to prepare the build and entire environment for presenting GUI.</p>

<p>Assume we have a portal using angular js build using gulp, bower and node. 
Let’s create another <code class="language-plaintext highlighter-rouge">Dockerfile</code> and name as <code class="language-plaintext highlighter-rouge">DockerfileDashboard</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM microsoft/dotnet:sdk AS dashboard
WORKDIR /app
COPY . ./
WORKDIR /app/Dashboard
RUN dotnet restore
RUN dotnet publish -c Release -o out
COPY ./Dashboard/ ./out/

ENV NODE_VERSION 8.9.4
ENV NODE_DOWNLOAD_URL https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz
ENV NODE_DOWNLOAD_SHA 21fb4690e349f82d708ae766def01d7fec1b085ce1f5ab30d9bda8ee126ca8fc

RUN curl -SL "$NODE_DOWNLOAD_URL" --output nodejs.tar.gz \
    &amp;&amp; echo "$NODE_DOWNLOAD_SHA nodejs.tar.gz" | sha256sum -c - \
    &amp;&amp; mkdir -p /usr/local/opt/nodejs8 \
    &amp;&amp; tar -xzf "nodejs.tar.gz" -C /usr/local/opt/nodejs8 --strip-components=1 \
    &amp;&amp; rm nodejs.tar.gz \
    &amp;&amp; ln -f -s /usr/local/opt/nodejs8/bin/node /usr/local/bin/node \
    &amp;&amp; ln -f -s /usr/local/opt/nodejs8/bin/node /usr/local/bin/nodejs

WORKDIR /app/Dashboard
COPY --from=dashboard /app/Dashboard/out .
RUN mkdir -p /app/Dashboard/logs

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get install -y nodejs build-essential bzip2 libkrb5-dev git

ENV ASPNETCORE_ENVIRONMENT=Staging
ENV ASPNETCORE_URLS http://*:80

RUN echo '{ "allow_root": true }' &gt; /root/.bowerrc
RUN rm -rf node_modules &amp;&amp; npm cache clean --force \
 &amp;&amp; npm config set prefix /usr/local \
 &amp;&amp; npm install -g bower gulp 
RUN npm install
RUN npm install --save-dev gulp
RUN npm install --save-dev bower
RUN bower install --allow-root 
RUN bower install moment --allow-root
RUN gulp clean
RUN gulp build
RUN npm link gulp

EXPOSE 3000 3001 80
CMD ["gulp", "serve"]
</code></pre></div></div>

<p>Here we are using an image from dotnetsdk, then we install our requirments (Node, bower and gulp) and then at the endwe expose port 80 and serve gulp on that port.</p>

<p>Now, we can build the second image for dashboard running below command:</p>

<p><code class="language-plaintext highlighter-rouge">docker build -t mydashboard -f DockerfileDashboard .</code></p>

<p>It will generate a docker image called <code class="language-plaintext highlighter-rouge">mydashboard</code>.</p>

<p>2 Dockerfiles and built images are ready to be used in any package managers like Kubernetes or docker-compose.</p>

<p>As mentioned earlier we can put each docker file inside the folders and then route build separately or give the route to package manager.</p>

<p>For example let’s have a simple <code class="language-plaintext highlighter-rouge">docker-compose</code> to run this 2 images plus a mysql container once:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>version: '3'
services:
  myservice:
    build:
      context: ./
      dockerfile: DockerfileService
    ports:
      - "8888:8888"
    volumes:
      - ./:/var/www
    container_name: myservice
    environment:
      - "ASPNETCORE_ENVIRONMENT=Staging"
    links:
      - mysqldb
    restart: on-failure
    depends_on:
      - mysqldb
  mydashboard:
    build:
      context: ./
      dockerfile: DockerfileDashboard
    ports:
      - "80:80"
    volumes:
      - ./:/var/www
    container_name: myservice
    environment:
      - "ASPNETCORE_ENVIRONMENT=Staging"
    links:
      - mysqldb
    restart: on-failure
    depends_on:
      - mysqldb
  mysqldb:
    image: mysql:5.7.27
    hostname: "mysqldb"
    container_name: mysqldb
    restart: unless-stopped
    ports:
      - 3306:3306
    command: --init-file /data/application/init.sql --max_allowed_packet=2G
    environment:
      MYSQL_ROOT_USER: root
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: mysql
      MYSQL_USER: devuser
      MYSQL_PASSWORD: devpass
      TZ: UTC
      MYSQL_ROOT_HOST: "%"
    volumes:
      - ./dbdata/mysql/initdb.d/init.sql:/data/application/init.sql
      - ./dbdata/mysql/conf.d:/etc/mysql/conf.d:ro
volumes:
    dbdata:
</code></pre></div></div>

<p><b>Hope this would help to run your project faster and setup CI/CD easier</b>
<b>Good Luck</b></p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Clusterized Spark</title>
					
					<link>https://atabakkh.github.io/spark/2019/09/16/sparkcluster.html</link>

					

					<pubDate>Mon, 16 Sep 2019 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/spark/2019/09/16/sparkcluster</guid>

					
						<category><![CDATA[ Spark ]]></category>
					

					<description><![CDATA[ Build a cluster of spark engines to run ML algorithm ]]></description>
					<content:encoded><![CDATA[ <h4>Build a cluster of spark engines</h4><p>Apache Spark is an engine for Big Data processing. One can run Spark on distributed mode on the cluster. In the cluster, there is master and n number of workers. It schedules and divides resource in the host machine which forms the cluster. The prime work of the cluster manager is to divide resources across applications. It works as an external service for acquiring resources on the cluster.
The following description is just a experience of using simple docker-compose for a clusterized spark environment.</p>

<p>Base on following discussion, we would create a base docker image called pysparkbase which we can use for our master node as well as workers. Then we create and build master node and follows by few workers as needed. at the end we can execute a simple python code to be run through entire cluster.</p>

<p><b>create docker file including pyspark modules</b></p>

<p>To create a docker file for pyspark base we can consider a light version of jdk and then add spark and pyspark to it. Therefore we can create a folder called pyspark_cluster, then create a <code class="language-plaintext highlighter-rouge">Dockerfile</code> inside it based on following scripts:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM openjdk:8-jdk-alpine

ENV DAEMON_RUN=true

ENV HADOOP_VERSION 2.7.3
ENV HADOOP_HOME /usr/hadoop-$HADOOP_VERSION
ENV HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop

ENV SPARK_VERSION 2.4.4
ENV SPARK_PACKAGE spark-${SPARK_VERSION}-bin-without-hadoop
ENV SPARK_HOME /usr/spark-${SPARK_VERSION}
ENV SPARK_DIST_CLASSPATH="$HADOOP_HOME/etc/hadoop/*:$HADOOP_HOME/share/hadoop/common/lib/*:$HADOOP_HOME/share/hadoop/common/*:$HADOOP_HOME/share/hadoop/hdfs/*:$HADOOP_HOME/share/hadoop/hdfs/lib/*:$HADOOP_HOME/share/hadoop/hdfs/*:$HADOOP_HOME/share/hadoop/yarn/lib/*:$HADOOP_HOME/share/hadoop/yarn/*:$HADOOP_HOME/share/hadoop/mapreduce/lib/*:$HADOOP_HOME/share/hadoop/mapreduce/*:$HADOOP_HOME/share/hadoop/tools/lib/*"

ENV PYSPARK_PYTHON python3
ENV PYSPARK_DRIVER_PYTHON=python3

ENV PATH $PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$SPARK_HOME/bin

# Conda envs
ENV CONDA_DIR=/opt/conda CONDA_VER=4.3.14
ENV PATH=$CONDA_DIR/bin:$PATH SHELL=/bin/bash LANG=C.UTF-8

# Conda
RUN set -ex \
  &amp;&amp; apk add --no-cache bash curl \
  &amp;&amp; apk add --virtual .fetch-deps --no-cache ca-certificates wget \
  &amp;&amp; wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub \
  &amp;&amp; wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk \
  &amp;&amp; apk add --virtual .conda-deps glibc-2.28-r0.apk \
  &amp;&amp; mkdir -p $CONDA_DIR  \
  &amp;&amp; echo export PATH=$CONDA_DIR/bin:'$PATH' &gt; /etc/profile.d/conda.sh \
  &amp;&amp; wget https://repo.continuum.io/miniconda/Miniconda3-${CONDA_VER}-Linux-x86_64.sh -O miniconda.sh \
  &amp;&amp; bash miniconda.sh -f -b -p $CONDA_DIR \
  &amp;&amp; rm miniconda.sh \
  &amp;&amp; conda update conda \
  # hadoop
  &amp;&amp; curl -sL --retry 3 "http://archive.apache.org/dist/hadoop/common/hadoop-$HADOOP_VERSION/hadoop-$HADOOP_VERSION.tar.gz" \
  | gunzip | tar -x -C /usr/ \
  &amp;&amp; rm -rf $HADOOP_HOME/share/doc \
  &amp;&amp; chown -R root:root $HADOOP_HOME \
  # spark
  &amp;&amp; curl -sL --retry 3 "https://www.apache.org/dyn/mirrors/mirrors.cgi?action=download&amp;filename=spark/spark-${SPARK_VERSION}/${SPARK_PACKAGE}.tgz" \
  | gunzip | tar x -C /usr/ \
  &amp;&amp; mv /usr/$SPARK_PACKAGE $SPARK_HOME \
  &amp;&amp; chown -R root:root $SPARK_HOME \
  &amp;&amp; apk add --update curl gcc g++ \
  &amp;&amp; apk add --no-cache python3-dev libstdc++ \
  &amp;&amp; rm -rf /var/cache/apk/* \
  &amp;&amp; ln -s /usr/include/locale.h /usr/include/xlocale.h \
  &amp;&amp; pip install bottle numpy cython pandas \
  &amp;&amp; pip3 install numpy \
  &amp;&amp; pip3 install pandas \
  # cleanup
  &amp;&amp; apk del .fetch-deps
  # &amp;&amp; apk del .conda-deps

COPY config ${SPARK_HOME}/conf

CMD ["/bin/bash"]
</code></pre></div></div>

<p>As you can see, the environment need config files as well, so let’s create a config folder and vim to file here:
<code class="language-plaintext highlighter-rouge">log4j.properties</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>log4j.rootCategory=ERROR, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
log4j.logger.org.apache.spark.repl.Main=WARN

log4j.logger.org.spark_project.jetty=WARN
log4j.logger.org.spark_project.jetty.util.component.AbstractLifeCycle=ERROR
log4j.logger.org.apache.spark.repl.SparkIMain$exprTyper=INFO
log4j.logger.org.apache.spark.repl.SparkILoop$SparkILoopInterpreter=INFO
log4j.logger.org.apache.parquet=ERROR
log4j.logger.parquet=ERROR

log4j.logger.org.apache.hadoop.hive.metastore.RetryingHMSHandler=FATAL
log4j.logger.org.apache.hadoop.hive.ql.exec.FunctionRegistry=ERROR
</code></pre></div></div>
<p>you can modify it based on your requirements.
then next file is :
<code class="language-plaintext highlighter-rouge">spark-defaults.conf</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Example:
# spark.master                     spark://master:7077
# spark.eventLog.enabled           true
# spark.eventLog.dir               hdfs://namenode:8021/directory
# spark.serializer                 org.apache.spark.serializer.KryoSerializer
# spark.driver.memory              5g
# spark.executor.extraJavaOptions  -XX:+PrintGCDetails -Dkey=value -Dnumbers="one two three"

# spark.driver.memory                40g
# spark.executer.memory              40g
# spark.driver.maxResultSize         20g
</code></pre></div></div>

<p>Now it is turn to build <code class="language-plaintext highlighter-rouge">pysperkbase</code> image:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build -t pysparkbase:1.0.1 .
</code></pre></div></div>

<p>after built, we are ready to configure and build our master node, Therefore we will create a folder named <code class="language-plaintext highlighter-rouge">pyspark-master</code> and will create a Dockerfile with below contains:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM pysparkbase:1.0.1

ENV SPARK_MASTER_PORT 7077
ENV SPARK_MASTER_WEBUI_PORT 8080
ENV SPARK_MASTER_LOG /spark/logs

COPY  runmaster.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/runmaster.sh
RUN ln -s /usr/local/bin/runmaster.sh / # backwards compat

EXPOSE 8080 7077 6066
</code></pre></div></div>

<p>let’s build the container for master node:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build -t pysparkmaster:1.0.1 ./pyspark-master
</code></pre></div></div>

<p>Same goes to worker container, so we will create a folder called <code class="language-plaintext highlighter-rouge">pyspark-worker</code> and create a <code class="language-plaintext highlighter-rouge">Dockerfile</code> inside:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM pysparkbase:1.0.1

ENV SPARK_WORKER_WEBUI_PORT 8081
ENV SPARK_WORKER_LOG /spark/logs
ENV SPARK_MASTER "spark://pysparkmaster:7077"


COPY  runworker.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/runworker.sh
RUN ln -s /usr/local/bin/runworker.sh / # backwards compat

EXPOSE 8081
</code></pre></div></div>

<p>Containers are ready, so let’s use <code class="language-plaintext highlighter-rouge">docker-compose</code> to build and run the entire package once:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>version: "3.7"
services:
  pysparkmaster:
    image: pysparkmaster:1.0.1
    #      build:
    #        context: .
    container_name: pysparkmaster
    hostname: pysparkmaster
    tty: true
    stdin_open: true
    ports:
      - "8080:8080"
      - "7077:7077"
    volumes:
      - ./pyspark-apps:/code/pyspark-apps
      - ./pyspark-data:/code/pyspark-data
    environment:
      - "SPARK_LOCAL_IP=pysparkmaster"
    networks:
      pysparknetwork:
         ipv4_address: 172.26.0.2
  pysparkworker1:
    image: pysparkworker:1.0.1
         #      build:
         #        context: .
    container_name: pysparkworker1
    hostname: pysparkworker1
    tty: true
    stdin_open: true
    ports:
      - "8081:8081"
    volumes:
      - ./pyspark-apps:/code/pyspark-apps
      - ./pyspark-data:/code/pyspark-data
    environment:
      - "SPARK_LOCAL_IP=pysparkworker1"
    depends_on:
      - pysparkmaster
    networks:
      pysparknetwork:
        ipv4_address: 172.26.0.3
  pysparkworker2:
    image: pysparkworker:1.0.1
         #      build:
         #        context: .
    container_name: pysparkworker2
    hostname: pysparkworker2
    tty: true
    stdin_open: true
    ports:
      - "8082:8081"
    volumes:
      - ./pyspark-apps:/code/pyspark-apps
      - ./pyspark-data:/code/pyspark-data
    environment:
      - "SPARK_LOCAL_IP=pysparkworker2"
    depends_on:
      - pysparkmaster
    networks:
      pysparknetwork:
        ipv4_address: 172.26.0.4
  pysparkworker3:
    image: pysparkworker:1.0.1
         #      build:
         #        context: .
    container_name: pysparkworker3
    hostname: pysparkworker3
    tty: true
    stdin_open: true
    ports:
      - "8083:8081"
    volumes:
      - ./pyspark-apps:/code/pyspark-apps
      - ./pyspark-data:/code/pyspark-data
    environment:
      - "SPARK_LOCAL_IP=pysparkworker3"
    depends_on:
      - pysparkmaster
    networks:
      pysparknetwork:
        ipv4_address: 172.26.0.5

networks:
  pysparknetwork:
    driver: bridge
    name: pysparknetwork
    ipam:
      driver: default
      config:
        - subnet: 172.26.0.0/16
</code></pre></div></div>

<p>By using <code class="language-plaintext highlighter-rouge">docker-compoer up --build -d</code> the whole package would be up and running.</p>

<p>now all of the workers are running and it’s time to link workers to the name node by doing:</p>

<p><code class="language-plaintext highlighter-rouge">master</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spark-class org.apache.spark.deploy.master.Master --ip `hostname` --port 7077 --webui-port 8080
</code></pre></div></div>
<p>each <code class="language-plaintext highlighter-rouge">workers</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spark-class org.apache.spark.deploy.worker.Worker  --webui-port 8080 spark://pysparkmaster:7077 
</code></pre></div></div>

<p><b>hint</b></p>

<p>you can also using some bash script or Makefile to build the entire thing easier and faster, for example before make docker-compose up you can easily have a <code class="language-plaintext highlighter-rouge">build.sh</code> file to build all images once:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash

set -e

docker build -t pysparkbase:1.0.1 .
docker build -t pysparkmaster:1.0.1 ./pyspark-master
docker build -t pysparkworker:1.0.1 ./pyspark-worker
</code></pre></div></div>

<p><b>submit a task</b></p>

<p>During build and up the package we shared some volumes like:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>volumes:
    - ./pyspark-apps:/code/pyspark-apps
    - ./pyspark-data:/code/pyspark-data
</code></pre></div></div>
<p>Therefore, we can put our pyspark code inside <code class="language-plaintext highlighter-rouge">pyspark-app</code> directory and the data would be under <code class="language-plaintext highlighter-rouge">pyspark-data</code> .</p>

<p>It would help you sbmit the task easily and track the data easier as well.</p>

<p>to submit a task we can use below command to master node:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run --rm -v $(CURDIR):/code --network pysparknetwork pysparkbase:1.0.1 spark-submit --master spark://pysparkmaster:7077 /code/pyspark-apps/test.py
</code></pre></div></div>

<p><b>Hope you setup this env easily and enjoy with it. You may use this env in your end to handle your bigdata tasks, streaming data or Machine learning data analysis</b>
<b>good luck</b></p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Kubernetes in an easy way</title>
					
					<link>https://atabakkh.github.io/devops/2018/10/17/buildk8s.html</link>

					

					<pubDate>Wed, 17 Oct 2018 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/devops/2018/10/17/buildk8s</guid>

					
						<category><![CDATA[ Devops ]]></category>
					

					<description><![CDATA[ Setup and configuration of an 2 layer application - frontend and backend ]]></description>
					<content:encoded><![CDATA[ <h4>Config and build a K8S environment</h4><p>Before going thorugh configuration and build of kubernetes packages, we should install minikube or Kubernetes.
My primary goal was to try Kubernetes on VMs the simplest way. I have chosen minimal version of k8s called minikube and used a Virtualbox VM with Ubuntu 18.04. minikube creates a single-node cluster.</p>

<p>There is a problem with minikube on VirtualBox VM: minikube uses VirtualBox hypervisor for virtualization and thus requires VT-X support on host. However VirtualBox doesn’t support VT-X/AMD-v. That could be solved by using vm-driver=none option and Docker on host.</p>

<p><b>Install Minikube</b></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 &amp;&amp; chmod +x minikube
$ sudo mkdir -p /usr/local/bin/
$ sudo install minikube /usr/local/bin/
$ minikube start
$ minikube status
</code></pre></div></div>

<p>you can follow the official link as well:
https://kubernetes.io/docs/tasks/tools/install-minikube/</p>

<p>To start minikube in none driver mode to solve hypervisor issue you cal force to start it with below command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo minikube start --vm-driver=none
</code></pre></div></div>

<p>Assume we have an application compose of 2 layer of frontend and backend. Therefore, before make the entire application into a single package, we will start to containarize the each layer by creating proper Dockerfile for each frontend and backend.</p>

<p>Then your environment would be like this:</p>

<p><img src="https://atabakkh.github.io/img/applicationtree.png" width="500" height="200" /></p>

<p>No it is turn to build your images and push to proper hub.
if you are a bash user you can make both in once user this command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/usr/bin/env bash
set -e
OWNER="&lt;Yor Hub's path&gt;"
BASE_NAME="&lt;Application name&gt;"
PACKAGES=( "backoffice" "frontend" )
version=&lt;Specify tag or leave latest&gt;
package=
usage() {
    cat &lt;&lt;EOF
usage: ./build.sh [--version &lt;version&gt; | -v &lt;version&gt;] [--package &lt;package&gt; | -p &lt;package&gt;] [--help | -h]
Build &lt;Application name&gt; project
Options:
  -v, --version string     Set the build version for the Docker images
  -p, --package string     Build only single package. If this option is not provided, build all packages
  -h, --help               Print help message
EOF
}
build() {
    echo   "---&gt; Start building ${OWNER}/${BASE_NAME}-${package}:${version}"
    docker build "${package}" -t "${OWNER}/${BASE_NAME}-${package}:${version}" || {
        echo "---&gt; Failed building ${OWNER}/${BASE_NAME}-${package}:${version}"
        exit 1
    }
    echo   "---&gt; Done building ${OWNER}/${BASE_NAME}-${package}:${version}"
}
while [ "$1" != "" ]; do
    case $1 in
        -v | --version )        shift
                                version=$1
                                ;;
        -p | --package )        shift
                                package=$1
                                ;;
        -h | --help )           usage
                                exit
                                ;;
        * )                     usage
                                exit 1
    esac
    shift
done
if [ -z "${package}" ]; then
    for package in "${PACKAGES[@]}"
    do
        build
    done
else
    build
fi
</code></pre></div></div>

<p>This bash script will build the image and store it locally, you need o push it in your container repository hub.
If you are using aws registry account easily follow belows:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>eval $(aws ecr get-login --no-include-email) &amp;&amp; \
docker push &lt;Yor registry's path&gt;/backoffice:&lt;tag&gt; &amp;&amp; \
docker push &lt;Yor registry's path&gt;/frontend:&lt;tag&gt; # &amp;&amp; \
</code></pre></div></div>

<p>Since the containers are ready, let’s create and run kubernetes deployment and services:
Before that make sure your minikube is running properly:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>minikube status
</code></pre></div></div>

<p>We need to prepare deployment config file and run it for each layer separately, So we start from frontend:
You run below command to generate the deployment file for frontend automaticaly and easy:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl run frontend --image=&lt;Yor registry's path&gt;/frontend:&lt;tag&gt; --image-pull-policy=IfNotPresent -o yaml &gt; manifests/frontend-deployment.yaml
</code></pre></div></div>
<p>Now we need to run services and generate config file as well (make sure the port defined based on dockerfile):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl expose deployment frontend --port=8080 --type=NodePort -o yaml &gt; manifests/frontend-service.yaml
</code></pre></div></div>

<p>if you run ( <code class="language-plaintext highlighter-rouge">kubectl get pod &amp;&amp; kubectl get service</code> ) you can see the pod and service for frontend are ready and running and by checking the config files in manifests folder ( <code class="language-plaintext highlighter-rouge">ls manifests</code> ) the configuration files also generated as well.</p>

<p>So it it time to prapre backend environment, therefore we need to do the same for that:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl run backend --image=&lt;Yor registry's path&gt;/backend:&lt;tag&gt; --image-pull-policy=IfNotPresent -o yaml &gt; manifests/backend-deployment.yaml
</code></pre></div></div>
<p>Now we need to run services and generate config file as well (make sure the port defined based on dockerfile):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl expose deployment backend --port=5000 --type=NodePort -o yaml &gt; manifests/backend-service.yaml
</code></pre></div></div>

<p>if you run ( <code class="language-plaintext highlighter-rouge">kubectl get pod &amp;&amp; kubectl get service</code> ) you can see the pod and service for backend are ready and running and by checking the config files in manifests folder ( <code class="language-plaintext highlighter-rouge">ls manifests</code> ) the configuration files also generated as well.</p>

<p><b>Your k8s environment is ready to use, you can easily work on config files and folders to maachive what exactly you are looking for.</b></p>

<p><b>good luck</b></p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Docker Container Networking</title>
					
					<link>https://atabakkh.github.io/devops/2018/08/10/dockernetwork.html</link>

					

					<pubDate>Fri, 10 Aug 2018 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/devops/2018/08/10/dockernetwork</guid>

					
						<category><![CDATA[ Devops ]]></category>
					

					<description><![CDATA[ One of the reasons Docker containers and services are so powerful is that you can connect them together, or connect them to non-Docker workloads. ]]></description>
					<content:encoded><![CDATA[ <h4>Multicontainer Docker - Networking and Compose</h4><p>Docker containers and services do not even need to be aware that they are deployed on Docker, or whether their peers are also Docker workloads or not. Whether your Docker hosts run Linux, Windows, or a mix of the two, you can use Docker to manage them in a platform-agnostic way.</p>

<p><b>User-defined bridge networks</b> are best when you need multiple containers to communicate on the same Docker host.</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:100px;width: 100%;">
$ docker network create -d bridge mybridge
$ docker run -d --net mybridge --name db redis
$ docker run -d --net mybridge -e DB=db -p 8000:5000 --name web myapp/web
$ docker network inspect mybridge
</textarea>

<p><img src="https://atabakkh.github.io/img/networkmybridge.png" width="500" height="370" /></p>

<p><b>Overlay networks</b> are best when you need containers running on different Docker hosts to communicate.</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:70px;width: 100%;">
$ docker network create -d overlay --opt encrypted mynet-overlay
$ docker service create --network mynet-overlay --name db redis
$ docker service create --network mynet-overlay -p 8000:5000 -e DB=db --name web myapp/web
</textarea>

<p><img src="https://atabakkh.github.io/img/overlaynetwork.png" width="500" height="370" /></p>

<p><b>Macvlan networks</b> are best when you are migrating from a VM setup or need your containers to look like physical hosts on your network, each with a unique MAC address.
The macvlan driver uses the concept of a parent interface. This interface can be a host interface such as eth0, a sub-interface, or even a bonded host adaptor which bundles Ethernet interfaces into a single logical interface.</p>

<p><img src="https://atabakkh.github.io/img/macvlan.png" width="500" height="370" /></p>

<p><b>Create Docker Container Network</b></p>

<textarea id="myTextArea" style="background-color: black;color: green; height:70px;width: 100%;">
$ docker container ls  #(which is same as docker ps) 
$ docker network ls
$ #The bridge network is the network in which containers are run by default (not secure). 

$ docker network inspect bridge
$ docker network create tertiary-test
</textarea>

 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Docker Compose vs Swarm</title>
					
					<link>https://atabakkh.github.io/devops/2018/08/10/dockercomposevsswarm.html</link>

					

					<pubDate>Fri, 10 Aug 2018 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/devops/2018/08/10/dockercomposevsswarm</guid>

					
						<category><![CDATA[ Devops ]]></category>
					

					<description><![CDATA[ How to use docker compose and docker swarm with best network ]]></description>
					<content:encoded><![CDATA[ <h4>Multicontainer Docker - Docker compose vs Docker swarm</h4><h2>What is Docker Compose</h2>

<p>It is unlikely that your application will only require a single container. Usually you will have several containers for other services like a database, web service, background tasks, etc. For this we use the docker-compose command.
docker-compose uses a very simple YAML file to build multiple containers. Each container can have its own Dockerfile that customises the individual container but docker-compose will build all the containers and put them into the same virtual network.
Docker containers and services do not even need to be aware that they are deployed on Docker, or whether their peers are also Docker workloads or not. Whether your Docker hosts run Linux, Windows, or a mix of the two, you can use Docker to manage them in a platform-agnostic way.</p>

<p><img src="https://atabakkh.github.io/img/dockercompose.png" width="500" height="370" /></p>

<p><b>YAML</b></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># YAML</span>
<span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3'</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">web</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">ports</span><span class="pi">:</span>
     <span class="pi">-</span> <span class="s2">"</span><span class="s">4000:80"</span>
    <span class="na">volumes</span><span class="pi">:</span>
     <span class="pi">-</span> <span class="s">.:/code</span>
</code></pre></div></div>

<p><b>Compose Two Container</b></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># YAML</span>
<span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3'</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">web</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">wordpress</span>
    <span class="na">links</span><span class="pi">:</span>
       <span class="pi">-</span> <span class="s">mysql</span>
    <span class="na">environment</span><span class="pi">:</span>
       <span class="pi">-</span> <span class="s">WORDPRESS_DB_PASSWORD=sample</span>
    <span class="na">ports</span><span class="pi">:</span>
       <span class="pi">-</span> <span class="s2">"</span><span class="s">127.0.0.3:8080:80"</span>
  <span class="na">mysql</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mysql:latest</span>
    <span class="na">environment</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">MYSQL_ROOT_PASSWORD=sample</span>
        <span class="pi">-</span> <span class="s">MYSQL_DATABASE=wordpress</span>
</code></pre></div></div>

<p><b>Docker Compose in Depth - Volumes and Networks</b> (MultiContainer Componse)</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:200px;width: 100%;">
$ mkdir mycompose
$ cd mycompose
$ vim app.py 
$ vim requirements.txt &lt;
  - flask
  - Redis
$ vim Dockerfile
$ vim docker-compose.yml
$ docker-compose up
$ #Hit http://0.0.0.0:5000/
$ docker-compose pull
$ docker-compose build 
$ docker-compose create
</textarea>

<ul>
<li><b>--force-recreate:</b> This recreates the container even if there is no need to as nothing within the configuration has changed</li>
<li><b>--no-recreate:</b> This doesn't recreate a container if it already exists; this flag cannot be used with the preceding flag</li>
<li><b>--no-build:</b> This doesn't build the images, even if an image that needs to be built is missing</li>
<li><b>--build:</b> This builds the images before creating the contain</li>
</ul>

<textarea id="myTextArea" style="background-color: black;color: green; height:70px;width: 100%;">
$ docker-compose logs
$ docker-compose top db
$ docker-compose config
</textarea>

<p><b>Docker compose</b></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>volumes:
     - .:/code
vim app.py
docker-compose down 

Build:
Context: .docker
Dockerfile: myapp.docker
Container_name: myapp
</code></pre></div></div>

<p><b>Compose networking</b></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>networks:
  default:
    external:
      name: existing-network 
</code></pre></div></div>

<p><b>Compose networking</b></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># you need to use the latest version of docker-compose yml
version: '3.5'
services:
  app:
    image: nginx
    networks:
      - my-network-name

networks:
  my-network-name:
   name: my-global-net
</code></pre></div></div>

<h2>Docker Swarm</h2>

<p><img src="https://atabakkh.github.io/img/dockerswarm.png" width="500" height="370" /></p>

<p><b>Create a Cluster and add worker to Cluster</b></p>

<textarea id="myTextArea" style="background-color: black;color: green; height:150px;width: 100%;">
$ docker-machine create -d virtualbox swarm-manager
$ docker-machine ls
$ docker-machine env swarm-manager
$ eval $(docker-machine env swarm-manager)
$ docker-machine ls
$ docker-machine create -d virtualbox swarm-worker01 
$ docker-machine create -d virtualbox swarm-worker02
$ docker-machine ls
</textarea>

<p><b>Initiate swarm manager </b></p>
<textarea id="myTextArea" style="background-color: black;color: green; height:70px;width: 100%;">
$ docker $(docker-machine config swarm-manager) swarm init \\
  --advertise-addr $(docker-machine ip swarm-manager):2377 \\
  --listen-addr $(docker-machine ip swarm-manager):2377
</textarea>

<p><b>Joining workers</b></p>
<textarea id="myTextArea" style="background-color: black;color: green; height:100px;width: 100%;">
$ docker $(docker-machine config swarm-worker01) swarm join \\
  $(docker-machine ip swarm-manager):2377 \\
  --token {token received from previous step}
$ docker $(docker-machine config swarm-worker02) swarm join \\
  $(docker-machine ip swarm-manager):2377 \\
  --token {token received from previous step}
</textarea>

<p><b>Cluster Nodes</b></p>
<textarea id="myTextArea" style="background-color: black;color: green; height:100px;width: 100%;">
$ docker-machine ls
$ eval $(docker-machine env swarm-manager)
$ docker node ls
$ docker info
</textarea>

<p><img src="https://atabakkh.github.io/img/swarmnetwork.png" width="500" height="370" /></p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Dotnet Core on LXD and Kubernetes</title>
					
					<link>https://atabakkh.github.io/devops/2018/06/06/dotnetcore-lxd.html</link>

					

					<pubDate>Wed, 06 Jun 2018 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/devops/2018/06/06/dotnetcore-lxd</guid>

					
						<category><![CDATA[ Devops ]]></category>
					

					<description><![CDATA[ Setup and configuration of .net code project on kubernetes using lxd ]]></description>
					<content:encoded><![CDATA[ <h4>Automated Deployment of .NET Core micro-services on LXD via Kubernetes</h4><p>This document shares an experience on setting up Kubernetes and then configuring your .NET Core application into your cluster. It will also help automate your deployment 
using a Kubernetes cluster instead of Visual Studio publish tools.</p>

<p>This experiment case contains: a) private cloud for Kubernetes deployment, and b) automated deployment of your .NET Core services through a basic CI/CD pipeline based on Jenkins.</p>

<p><b>Requirement</b></p>
<ul>
<li>Ubuntu 16.04 machine</li>
<li>git command-line client (installed by sudo apt install git)</li>
<li>Internet access</li>
</ul>

<p><b>Quick Outline</b></p>
<ul>
<li>Create or restore your .NET Core application on the Ubuntu machine and push the code to GitHub</li>
<li>Create and build a Docker container and upload it to Docker Hub</li>
<li>Set up a Kubernetes cluster using the Canonical distribution</li>
<li>Deploy your application on the Kubernetes cluster and expose it</li>
<li>Deploy Jenkins alongside Kubernetes and automate the deployment of your service</li>
</ul>

<p><b>Dotnet Core service on Linux</b>
<br /><br />
Installing dotnet core on Ubuntu 16.04 :</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:170px;width: 100%;">
$ curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor &gt; microsoft.gpg 
$ sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
$ sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main" &gt; /etc/apt/sources.list.d/dotnetdev.list'
$ sudo apt-get install apt-transport-https 
$ sudo apt-get update 
$ sudo apt-get install dotnet-sdk-2.1.4
</textarea>

<p>We can now copy our application into directory and then run <code class="language-plaintext highlighter-rouge">dotnet run</code> inside directory.
Then open browser and hit localhost. (if you specified any port for your project, you have to add port after localhost: <code class="language-plaintext highlighter-rouge">https://localhost:6600</code>)</p>

<p><b>Dockerize application</b></p>

<p>Since you are able to run your application via command, now it is time to dockerize it and create container for application and its dependent services.
If you do not have docker installed, please follow the steps:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:200px;width: 100%;">
$ sudo apt-get install apt-transport-https ca-certificates curl \
    software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce
$ sudo usermod -a -G docker $USER
$ newgrp docker 
</textarea>

<p>You might create a dockerhub repository and push your dotnet docker image over there for easy pull and run.
Therefore, go to https://hub.docker.com and create an account. After logging in to Docker Hub click on the “Create Repository” button and create a new public repository. That is where we will be pushing our docker images.</p>

<p>Now, try to run dotnet publish to make sure your project can be compile properly before moving to docker container.</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:20px;width: 100%;">
$ dotnet publish -c Release
</textarea>

<p>Now, we tring to package everything under <code class="language-plaintext highlighter-rouge">bin/Release/netcoreapp2.0/publish/.</code>
We create a <code class="language-plaintext highlighter-rouge">Dockerfile</code> on the root of our project with the following contents:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:150px;width: 100%;">
FROM microsoft/dotnet:2.0-sdk
ADD ./bin/Release/netcoreapp2.0/publish/ /app
ENV ASPNETCORE_URLS=http://*:${PORT}
WORKDIR /app
ENTRYPOINT [ "dotnet", "{projectname}.dll"]
</textarea>

<p>Building the container and testing that it works:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:70px;width: 100%;">
$ docker build -t {dockerhub-username}/{repository-name}:v1 .
$ docker run -p 8080:8080  {dockerhub-username}/{repository-name}:v1
&lt;\textarea&gt;

Now your out put is ready at http://localhost:8080 .

It is time to push the first version to Docker Hub:

<textarea id="myTextArea" style="background-color: black;color: green; height:70px;width: 100%;">
$ docker login
$ docker push {dockerhub-username}/{repository-name}:v1
</textarea>

The Dockerfile should be part of the code so we commit it and push it to the git repository:

<textarea id="myTextArea" style="background-color: black;color: green; height:70px;width: 100%;">
$ git add Dockerfile
$ git commit -m "Adding dockerfile"
$ git push origin master
</textarea>

<br /><br />

<b>Kubernetes Cluster</b>


To utilize Kubernetes deployment we will go with Canonical’s solution. The reason being that Canonical offers a seamless and effortless transition from small deployment in localhost  to a full blown production grade Kubernetes deployed on private, public clouds or even on bare metal.
Therefore, we continue with deploying Kubernetes and Jenkins on localhost (just make sure your laptop/desktop have at least 8GB of RAM. 

<h6>LXD</h6>
We first need to have LXD running. LXD is a really powerful type of container based on the similar technologies as Docker. Contrary to Docker, LXD containers resemble more to virtual machines (VMs). Lets simplify things and assume from now on that LXD containers are VMs that boot instantly and have no performance overhead!

<br />

For this deployment we are going to use LXD. While initialising ( /snap/bin/lxd init) make sure you go with the defaults but you do not enable ipv6. When asked “What IPv6 address should be used (CIDR subnet notation, “auto” or “none”) [default=auto]?” Reply with “none”.

<textarea id="myTextArea" style="background-color: black;color: green; height:100px;width: 100%;">
$ sudo snap install lxd
$ sudo usermod -a -G lxd $USER 
$ newgrp lxd
$ /snap/bin/lxd init
</textarea>

At this point we can use either Juju or Conjure-up to deploy Kubernetes. Conjure-up is essentially a wizard sitting on-top of Juju.
To install Kubernetes with conjure-up you can simply use follow steps:

<textarea id="myTextArea" style="background-color: black;color: green; height:50px;width: 100%;">
$ sudo snap install conjure-up --classic
$ conjure-up
</textarea>


<h6>Canonical</h6>

Canonical Kubernetes comes in two flavours:

kubernetes-core is a cut down version installed in two machines, in our case two LXD containers running on our localhost.
canonical-kubernetes is the full production grade deployment with features such as HA and monitoring.
We will go for kubernetes-core; on the next screen select localhost as the cloud provider. Follow the wizard’s steps till the end and wait for the deployment to finish. For a headless deployment you can do a `conjure-up kubernetes-core localhost` .


To review the status of deployment have a look at:
<textarea id="myTextArea" style="background-color: black;color: green; height:20px;width: 100%;">
$ juju status
</textarea>

Getting into one of the LXD containers/machines we use juju ssh, for exmple:
<textarea id="myTextArea" style="background-color: black;color: green; height:20px;width: 100%;">
$ juju ssh kubernetes-master/0
</textarea>

Inside kubernetes-master under /home/ubuntu you will find a config file you can use for accessing your cluster. 
We can fetch that file with:
<textarea id="myTextArea" style="background-color: black;color: green; height:20px;width: 100%;">
$ juju scp kubernetes-master/0:config .
</textarea>

Conjure-up has already copied the Kubernetes config locally and installed kubectl for us. 

<b>CI/CD</b>

To have an automated deployment process, you can use jenkins machine running beside other LXD which we just setup.
Follows will be showing a few Jenkins jobs to automate the process of building, packaging and deploying our application. The intention here is to show everything that happens under the hood and not hide behind a flashy UI.

You can simply run jenkins by following command:
<textarea id="myTextArea" style="background-color: black;color: green; height:20px;width: 100%;">
juju deploy jenkins
</textarea>

We deploy Jenkins next to our Kubernetes cluster. This will take some time, you can check the progress of the deployment with `juju status`.
Next we need to set a password to Jenkins and expose it so we can access its UI on port 8080. Exposing Jenkins is not needed in the localhost deployment which uses LXD containers but we show it here for completeness.

<textarea id="myTextArea" style="background-color: black;color: green; height:40px;width: 100%;">
$ juju config jenkins password='your_secure_password'
$ juju expose jenkins
</textarea>

Before we start crafting our jobs we need to configure Jenkins a bit more. I can tell you beforehand our jobs need to sudo run without asking for a password. The easiest way to do that is to edit /etc/sudoers in the Jenkins machine. Here is how we append a line to the sudoers file with Juju:

<textarea id="myTextArea" style="background-color: black;color: green; height:30px;width: 100%;">
$ juju run --unit jenkins/0 -- 'sudo echo "jenkins ALL=(ALL) NOPASSWD: ALL" &gt;&gt; /etc/sudoers'
</textarea>

We also know that our Jenkins jobs need to talk to the Kubernetes. To this end Jenkins will need the kubeconfig file. We take the file from the kubernetes-master and place it in our Jenkins machine under /var/tmp:
<textarea id="myTextArea" style="background-color: black;color: green; height:40px;width: 100%;">
$ juju scp kubernetes-master/0:config .
$ juju scp config  jenkins/0:/var/tmp/
</textarea>

Last part of the configuration, I promise! We know our application will be exposed using Kubernetes NodePort on port 31576. We need to make sure there is no firewall blocking that port and requests can reach it:
<textarea id="myTextArea" style="background-color: black;color: green; height:40px;width: 100%;">
$ juju run --application kubernetes-worker -- open-port 31576
</textarea>
Jenkins is ready in `localhost:8080`. Then we can start creating our jobs.

The first Jenkins jobs is the “Install dependencies”. This job is just a shell script installing all software packages needed to a) talk to kubernetes, b) build our .NET core service and c) package everything into a docker container. Place the following in a shell script job and run it once:

<textarea id="myTextArea" style="background-color: black;color: green; height:400px;width: 100%;">
echo "Installing kubectl"
sudo snap install kubectl --classic
echo "Installing dotnet"
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor &gt; microsoft.gpg 
sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main" &gt; /etc/apt/sources.list.d/dotnetdev.list'
sudo apt-get install apt-transport-https -y 
sudo apt-get update
sudo apt-get install dotnet-sdk-2.1.4 -y
echo "Installing docker"
sudo apt-get install apt-transport-https ca-certificates  curl \
    software-properties-common -y
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
sudo apt-get update
sudo apt-get install docker-ce -y
</textarea>

The second Jenkins job bootstraps the service in Kubernetes. It creates a deployment using the v1 image we created above and it makes sure all operations are recorded ( - record param). This deployment is exposed using NodePort 31576. Place the following in a Jenkins jobs and run it once, remember to update the docker user name:
<textarea id="myTextArea" style="background-color: black;color: green; height:320px;width: 100%;">
sudo /snap/bin/kubectl --kubeconfig=/var/tmp/config run {repository-name} \
    --image={duckerhub-username}/{repositoryname}:v1 --port=8080 --record
echo "apiVersion: v1
kind: Service
metadata:
  name: {repositoryname}
spec:
  type: NodePort
  ports:
    - port: 8080
      nodePort: 31576
      name: http
  selector:
    run: {repositoryname}" &gt; /tmp/expose.yaml
sudo /snap/bin/kubectl --kubeconfig=/var/tmp/config apply -f /tmp/expose.yaml
</textarea>

Use juju status to find the IP of a kubernetes worker and open a browser at `http://<kubernetes-worker-ip>:31576` . Your application is served from Kubernetes! But we are not done yet.

Lets create a third job (“Build and release”) that pulls your code from GitHub, compiles it, puts it in a container and deploys that container. Replace the repository and the docker username in the following snippet. Then create a Jenkins job:
<textarea id="myTextArea" style="background-color: black;color: green; height:250px;width: 100%;">
rm -rf {projectdirectory}
git clone https://github.com/{github-username}/{project-repository}.git
cd {project-repository}
dotnet publish -c Release
sudo docker login -u {dockerhub-username} -p ${DOCKER_PASS}
sudo docker build -t {dockerhub-username}/{repository-name}:${DOCKER_TAG} .
sudo docker push {dockerhub-username}/{repository-name}:${DOCKER_TAG}
sudo /snap/bin/kubectl --kubeconfig=/var/tmp/config set image deployment/{repository-name} {repository-name}={dockerhub-username}/{repository-name}:${DOCKER_TAG}
</textarea>

Two parameters are needed: ${DOCKER_TAG} is a string, and ${DOCKER_PASS} holds the docker password of the user. You have to tick the checkbox indicating this is a parametrized job and add the two parameters. We are ready! Trigger the job and wait for it to finish. Your code should find its way to our Kubernetes cluster.
<br /><br />
<h5>Enjoy!</h5>
<br />
</kubernetes-worker-ip></textarea>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Agile with Mosquito concern</title>
					
					<link>https://atabakkh.github.io/agile/2017/09/17/AgileWithMosquito.html</link>

					

					<pubDate>Sun, 17 Sep 2017 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/agile/2017/09/17/AgileWithMosquito</guid>

					
						<category><![CDATA[ Agile ]]></category>
					

					<description><![CDATA[ Many benefit of using agile made it robustness to be one of the best practices in project and process management. ]]></description>
					<content:encoded><![CDATA[ <h4>Do agile but don't forget the main MOSQUITO criteria</h4><p><b>Why Agile?</b>
<br /></p>

<p>To formally define Agile through software development, Google describe as: ‘relating to a method of project management, used especially for software development, that is characterized by the division of tasks into short phases of work and frequent reassessment and adaptation of plans.’</p>

<p>Software development management is about revolutionizing the way business is conducted and proofing your organization for continuously changing internal factors, new technologies, external competitors and industry trends. An agile approach is to rolling out new initiatives across the business direction, and ensure all teams involved can quickly adapt and deliver values often.</p>

<h5>Agile Advantages:</h5>

<h6>Progress Flexibility:</h6>
<p>One of the offer of agile is adaptability. The lithe direction, procedure, nonetheless and technology require alternate arrangement and it gives better outcomes to particular difficulties.</p>

<h6>Always Business value comes first:</h6>
<p>For advanced movement and changes, agile is not solely innovation. When the management characterized business difficulties, target and directions, any new activities will start to guarantee how whole approach is aligned into business.</p>

<h6>Align with CI/CD:</h6>
<p>The team is always collaborating, learning and adjusting through iterations and reviewing on improvement and delivering. Team members always expanding their knowledge and identifying the movement. Productive sprints means that features are delivered incrementally as the project evolves. Working in an agile manner ensures valuable outcomes are delivered in smaller chunks, more frequently and overwhelming to the business.</p>

<h6>Cost management:</h6>
<p>Sprint is representing smaller chunk of business direction and therefore the cost for each sprint allows budget refinements changes and improvement.</p>

<h6>Team communication an engagement:</h6>
<p>Agile will help the teams to collaborating and solving the solution clearly. Agile encourage regular communication between stakeholder management and feedback sessions. 
<br /><br />
Moreover, agile approach would help other corners of business, such as risk management and quality assurance. Agile is a set of methodology, principles and philosophy.</p>

<p><b>Why MOSQUITO?</b></p>

<p>Every sprint has its specific MOSQUITO attributes to help the productivities:</p>

<h5><b>M</b>oney</h5>
<p>One of the significant items in agile is estimating short and long term cost. How much it will take to deliver a new software? The answer to this question is not straightforward?
Indeed, Money is a product of time and team members. Agile is focusing on principles believe in fixing time and cost cross team member, deliverables and their seniorities.
Product deliverable is promises and the need of its customer. Therefore, stakeholder confidence, is one of the important item through cost management.
Agile is trying to focus on:
<br /><code class="language-plaintext highlighter-rouge">work packages:</code> <br />
every work packages required cost re-estimation based on necessary and un-necessary backlog.
<br />
<code class="language-plaintext highlighter-rouge">early estimations:</code><br />
Agile would help to terminate redundant backlog. It also help to early terminate low priority items from sprint.
<br /><code class="language-plaintext highlighter-rouge">Flexible changes:</code><br />
Agile is a pace to re-arrange the backlog and deliverables based on direction roadmap and deliverable on certain period of time.</p>
<h5><b>S</b>kill</h5>
<p>Developers are the core of agile development practice. They work in small , cross-functional teams and can quickly create working and tested code which delivers value into sprint. Every developer is empowered by principles to enterprise and manage including self-organize to accomplish their own work.
Seniority is main concern into agile, the agile team members must have ability to do cross-functioning. Developers with T-Shaped skill cross team responsibilities, comprising of 	Technical skills (in respective platforms) and Behavioral skills.</p>
<h5><b>Q</b>uality</h5>
<p>How is the owner of quality in agile development. Product owner will prepare the product backlog and then based on that small chunk of development within sprint, suddenly developer is also facing some required refactoring within the code and raise a product backlog into the list. This was unexpected for the team, and then the assumption from technical team is that this issue impact on very wide rage of features. Now it is job of team lead and product owner to prioritize the product backlog.</p>

<p>Quality Assurance team is specialist for functional report, Product owner would sign off on design document. Of course, performance on non-functional requirement must be approved by product owner. All defects would be approved by product owner and then add into backlog. Indeed, the scope of pre-production testing would be approved by the Product Owner.</p>

<p>Therefore, although ever single member of the team is responsible for quality of his task, including functional and non-functional, but the entire quality process would conduct by product owner</p>
<h5><b>I</b>nformation</h5>
<p>Enough information and story are the primary artifact used to define system behavior in Agile. They’re not requirements. Instead, they’re short, simple descriptions of functionality usually told from the team member’s perspective and written from different point of view. Each information and story is intended to enable the implementation of a small, vertical slice of system behavior that supports incremental development. Stories provide just enough information for the intent to be understood by both business and technical people. Details are deferred until the story is ready to be implemented. Through acceptance criteria, stories get more specific, helping to ensure system quality. Stories deliver functionality directly to the end user. Enabler stories bring visibility to the work items needed to support exploration, architecture, infrastructure, and compliance.</p>
<h5><b>T</b>ime</h5>
<p>The total time that elapses from the moment when the work is started on a task until its completion. Lead time and cycle time are two significant metroc to measure agile process. Cycle time is a measure of the elapsed time when work starts on an item (story, task, bug etc.) until it’s ready for delivery. Cycle time tells how long (in calendar time) it takes to complete a task. 
Lead time clock starts when the request is made and ends at delivery. Cycle time clock starts when work begins on the request and ends when the item is ready for delivery. Knowing your cycle time is one thing but many teams are also actively trying to drive down their cycle times as part of their continuous improvement efforts. 
The faster your cycle time, the quicker you can introduce new features to your end users.</p>
<h5><b>O</b>rganization</h5>
<p>The whole organization and team would help on successful delivery. Product owner, team lead, every team developer would align into direction and understanding the story of tickets. Communication is a significant point of high quality deliveries. Across industries and regions, most survey participants agree that the world around them is changing, and quickly. Business environments are increasingly complex and volatile, with two-thirds of respondents saying their sectors are characterized by rapid change. In such environments, the need for companies to demonstrate agility is top of mind: the more unstable that respondents say their environments are, the more likely they are to say their companies have begun agile transformations.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Hadoop Spark via docker</title>
					
					<link>https://atabakkh.github.io/spark/2017/08/05/spark-instalation.html</link>

					

					<pubDate>Sat, 05 Aug 2017 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/spark/2017/08/05/spark-instalation</guid>

					
						<category><![CDATA[ Spark ]]></category>
					

					<description><![CDATA[ how to setup hadoop master-slave and spark containers to handle resilient steaming data. ]]></description>
					<content:encoded><![CDATA[ <h4>preparing environment for Hadoop, Spark, Scala and pyspark.</h4><p>This document would share an experience on setting up dockerized master-slave hadoop and spark on top of them. 
Then config your environment and listen to steaming data. It will also help to automate your environment setup for development and production stages.</p>

<p>This experiment case is contain; a) private a master container, and b) provide 2 slave hadoop containers and c) prepare spark on top of hadoop infrustructure.</p>

<p><b>Requirement</b></p>
<ul>
<li>Ubuntu</li>
<li>Internet access</li>
</ul>

<p><b>Quick outline</b></p>
<ul>
<li>create a hadoop master container. </li>
<li>create slave containers. </li>
<li>create spark container pointed to hadoop. </li>
<li>create spark container pointed to hadoop. </li>
<li>prepare docker environment with docker-compose</li>
</ul>

<p><b>prepare a docker for hadoop main node</b>
<br /><br />
Creating docker file for hadoop main node:</p>

<p>Create a directory by name <code class="language-plaintext highlighter-rouge">hadoop-master</code> and then create a Dockerfile inside that contain following info:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:170px;width: 100%;">
FROM atabak/hadoop-base:latest
MAINTAINER Atabak

ADD bootstrap.sh /etc/bootstrap.sh
RUN chown root:root /etc/bootstrap.sh
RUN chmod 700 /etc/bootstrap.sh

ENV BOOTSTRAP /etc/bootstrap.sh

CMD ["/etc/bootstrap.sh", "-d"]
</textarea>

<p>As you can see in Dockerfile, customized bootstrap configuration file is must be located beside <code class="language-plaintext highlighter-rouge">Dockerfile</code>.
Therefore, inside same directory, create a bash file named <code class="language-plaintext highlighter-rouge">bootstrap.sh</code> and then enter the following script within that:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:270px;width: 100%;">
#!/bin/bash

: ${HADOOP_PREFIX:=/usr/local/hadoop}

$HADOOP_PREFIX/etc/hadoop/hadoop-env.sh

rm /tmp/*.pid

# installing libraries if any - (resource urls added comma separated to the ACP system variable)
cd $HADOOP_PREFIX/share/hadoop/common ; for cp in ${ACP//,/ }; do  echo == $cp; curl -LO $cp ; done; cd -

# altering the core-site configuration
#sed s/NAMENODE/$HOSTNAME/ /usr/local/hadoop/etc/hadoop/core-site.xml.template &gt; /usr/local/hadoop/etc/hadoop/core-site.xml
#sed s/NAMENODE/$HOSTNAME/ /usr/local/hadoop/etc/hadoop/hdfs-site.xml.template &gt; /usr/local/hadoop/etc/hadoop/hdfs-site.xml
#sed s/RESOURCEMANAGER/$HOSTNAME/ /usr/local/hadoop/etc/hadoop/yarn-site.xml.template.template &gt; /usr/local/hadoop/etc/hadoop/yarn-site.xml.template

service ssh start
nohup $HADOOP_PREFIX/bin/hdfs namenode &amp;
nohup $HADOOP_PREFIX/bin/yarn resourcemanager &amp;
nohup $HADOOP_PREFIX/bin/yarn timelineserver &amp;
nohup $HADOOP_PREFIX/bin/mapred historyserver &amp;

if [[ $1 == "-d" ]]; then
    while true; do sleep 1000; done
fi

if [[ $1 == "-bash" ]]; then
    /bin/bash
fi
</textarea>

<p><b>prepare a docker for hadoop slave nodes</b>
<br /><br />
Creating docker file for hadoop main node:</p>

<p>Create a directory by name <code class="language-plaintext highlighter-rouge">hadoop-slave</code> and then create a Dockerfile inside that contain following info:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:170px;width: 100%;">
FROM atabak/hadoop-base:latest
MAINTAINER Atabak

ADD bootstrap.sh /etc/bootstrap.sh
RUN chown root:root /etc/bootstrap.sh
RUN chmod 700 /etc/bootstrap.sh

ENV BOOTSTRAP /etc/bootstrap.sh

CMD ["/etc/bootstrap.sh", "-d"]
</textarea>

<p>As you can see in Dockerfile, slave containers also required customized bootstrap configuration file is must be located beside <code class="language-plaintext highlighter-rouge">Dockerfile</code>.
Therefore, inside same directory, create a bash file named <code class="language-plaintext highlighter-rouge">bootstrap.sh</code> and then enter the following script within that:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:270px;width: 100%;">

#!/bin/bash

: ${HADOOP_PREFIX:=/usr/local/hadoop}

$HADOOP_PREFIX/etc/hadoop/hadoop-env.sh

rm /tmp/*.pid

# installing libraries if any - (resource urls added comma separated to the ACP system variable)
cd $HADOOP_PREFIX/share/hadoop/common ; for cp in ${ACP//,/ }; do  echo == $cp; curl -LO $cp ; done; cd -

# altering the core-site configuration
#sed s/NAMENODE/$HOSTNAME/ /usr/local/hadoop/etc/hadoop/core-site.xml.template &gt; /usr/local/hadoop/etc/hadoop/core-site.xml
#sed s/NAMENODE/$HOSTNAME/ /usr/local/hadoop/etc/hadoop/hdfs-site.xml.template &gt; /usr/local/hadoop/etc/hadoop/hdfs-site.xml
#sed s/RESOURCEMANAGER/$HOSTNAME/ /usr/local/hadoop/etc/hadoop/yarn-site.xml.template.template &gt; /usr/local/hadoop/etc/hadoop/yarn-site.xml.template

service ssh start
nohup $HADOOP_PREFIX/bin/hdfs datanode 2&gt;&gt; /var/log/hadoop/datanode.err &gt;&gt; /var/log/hadoop/datanode.out &amp;
nohup $HADOOP_PREFIX/bin/yarn nodemanager 2&gt;&gt; /var/log/hadoop/nodemanager.err &gt;&gt; /var/log/hadoop/nodemanager.out &amp;

if [[ $1 == "-d" ]]; then
    while true; do sleep 1000; done
fi

if [[ $1 == "-bash" ]]; then
    /bin/bash
fi
</textarea>

<p><b>prepare a docker for spark and config the nodes</b>
<br /><br />
Now it is turn to config and create a container for spark including it’s dependencies:</p>

<p>Create a directory by name <code class="language-plaintext highlighter-rouge">spark</code> and then create a Dockerfile inside that contain following info:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:170px;width: 100%;">
FROM atabak/hadoop-base:latest
MAINTAINER Atabak

# Update the APT cache 
RUN apt-get update 
RUN apt-get upgrade -y 

# Install and setup project dependencies 
RUN apt-get install -y curl wget git 

#prepare for Java download 
RUN apt-get install -y python-software-properties 
RUN apt-get install -y software-properties-common

#java
RUN apt-get install -y openjdk-8-jdk &amp;&amp; \ 
    apt-get install -y ant &amp;&amp; \ 
    apt-get clean &amp;&amp; \ 
    rm -rf /var/lib/apt/lists/* &amp;&amp; \ 
    rm -rf /var/cache/oracle-jdk8-installer;

RUN mkdir -p /usr/java/default
RUN sudo cp -r /usr/lib/jvm/java-8-openjdk-amd64/* /usr/java/default/

ENV JAVA_HOME /usr/java/default
ENV PATH $PATH:$JAVA_HOME/bin


#install spark
ENV SPARK_HOME /usr/local/spark
ENV SPARK_VERSION 2.1.1
ENV SPARK_BIN_VERSION $SPARK_VERSION-bin-hadoop2.7

RUN curl -s https://www.apache.org/dist/spark/spark-$SPARK_VERSION/spark-$SPARK_BIN_VERSION.tgz | tar xz -C /usr/local
RUN ln -s /usr/local/spark-$SPARK_BIN_VERSION $SPARK_HOME 
#&amp;&amp; \
#    rm /spark-$SPARK_BIN_VERSION.tgz


#RUN mkdir $SPARK_HOME/yarn-remote-client
#ADD yarn-remote-client $SPARK_HOME/yarn-remote-client

#RUN $BOOTSTRAP &amp;&amp; $HADOOP_PREFIX/bin/hadoop dfsadmin -safemode leave &amp;&amp; $HADOOP_PREFIX/bin/hdfs dfs -put $SPARK_HOME-2.1.1-bin-hadoop2.7/lib /spark

#ENV YARN_CONF_DIR $HADOOP_PREFIX/etc/hadoop
#ENV PATH $PATH:$SPARK_HOME/bin:$HADOOP_PREFIX/bin

# update boot script
ADD bootstrap.sh /etc/bootstrap.sh
#RUN chown root.root /etc/bootstrap.sh
#RUN chmod 700 /etc/bootstrap.sh


#install scala
ENV SCALA_VERSION 2.12.2 
ENV SCALA_HOME /usr/local/scala 
ENV PATH $PATH:$SPARK_HOME/bin:$SCALA_HOME/bin 

RUN wget https://downloads.lightbend.com/scala/$SCALA_VERSION/scala-$SCALA_VERSION.tgz &amp;&amp; \ 
    tar -zxf /scala-$SCALA_VERSION.tgz -C /usr/local/ &amp;&amp; \ 
    ln -s /usr/local/scala-$SCALA_VERSION $SCALA_HOME &amp;&amp; \ 
    rm /scala-$SCALA_VERSION.tgz 

ADD start-master.sh /start-master.sh 
ADD start-worker /start-worker.sh 
ADD spark-shell.sh /spark-shell.sh 
ADD spark-defaults.conf /spark-defaults.conf 
ADD remove_alias.sh /remove_alias.sh 

ENV SPARK_MASTER_OPTS="-Dspark.driver.port=7001 -Dspark.fileserver.port=7002 -Dspark.broadcast.port=7003 -Dspark.replClassServer.port=7004 -Dspark.blockManager.port=7005 -Dspark.executor.port=7006 -Dspark.ui.port=4040 -Dspark.broadcast.factory=org.apache.spark.broadcast.HttpBroadcastFactory" 
ENV SPARK_WORKER_OPTS="-Dspark.driver.port=7001 -Dspark.fileserver.port=7002 -Dspark.broadcast.port=7003 -Dspark.replClassServer.port=7004 -Dspark.blockManager.port=7005 -Dspark.executor.port=7006 -Dspark.ui.port=4040 -Dspark.broadcast.factory=org.apache.spark.broadcast.HttpBroadcastFactory" 
ENV SPARK_MASTER_PORT 7077 
ENV SPARK_MASTER_WEBUI_PORT 8080 
ENV SPARK_WORKER_PORT 8888 
ENV SPARK_WORKER_WEBUI_PORT 8081 

EXPOSE 8080 7077 8888 8081 4040 7001 7002 7003 7004 7005 7006
</textarea>

<p>As you can see within docker file few dependencies is there which need to be satisfied including spark configuration and node manager:
Therefore we go with creating one by one:</p>

<h6>bootstrap</h6>

<p>we need to customize the bootstrap, and for that reason we are going to update the current bootstrap file with the file.
create a bash file <code class="language-plaintext highlighter-rouge">bootstrap.sh</code> contain following content and place beside the dockerfile:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:270px;width: 100%;">
#!/bin/bash

: ${HADOOP_PREFIX:=/usr/local/hadoop}

$HADOOP_PREFIX/etc/hadoop/hadoop-env.sh

#rm /tmp/*.pid

# installing libraries if any - (resource urls added comma separated to the ACP system variable)
cd $HADOOP_PREFIX/share/hadoop/common ; for cp in ${ACP//,/ }; do  echo == $cp; curl -LO $cp ; done; cd -

# altering the core-site configuration
sed s/HOSTNAME/$HOSTNAME/ /usr/local/hadoop/etc/hadoop/core-site.xml.template &gt; /usr/local/hadoop/etc/hadoop/core-site.xml


service sshd start
$HADOOP_PREFIX/sbin/start-dfs.sh
$HADOOP_PREFIX/sbin/start-yarn.sh
$HADOOP_PREFIX/sbin/mr-jobhistory-daemon.sh start historyserver

if [[ $1 == "-d" ]]; then
  while true; do sleep 1000; done
fi

if [[ $1 == "-bash" ]]; then
  /bin/bash
fi
</textarea>

<h6>Spark Master</h6>

<p>create another bash file <code class="language-plaintext highlighter-rouge">start-master.sh</code> to start master node:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:170px;width: 100%;">

#!/usr/bin/env bash
export SPARK_MASTER_IP="spark_master"
export SPARK_LOCAL_IP="spark_master"
/usr/local/spark/sbin/start-master.sh --properties-file /spark-defaults.conf -i $SPARK_LOCAL_IP "$@"
/bin/bash

</textarea>

<h6>Workers</h6>

<p>create another bash file <code class="language-plaintext highlighter-rouge">start-worker</code> to start workers:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:270px;width: 100%;">

#!/usr/bin/env bash
cd /usr/local/spark
SPARK_MASTER_PORT_7077_TCP_ADDR=`getent hosts spark_master | awk '{ print $1 }'`
export SPARK_LOCAL_IP=`hostname -I | awk '{print $1}'`
./bin/spark-class org.apache.spark.deploy.worker.Worker \
	spark://${SPARK_MASTER_PORT_7077_TCP_ADDR}:${SPARK_MASTER_ENV_SPARK_MASTER_PORT:-7077}  \
	--properties-file /spark-defaults.conf \
	-i $SPARK_LOCAL_IP \
	"$@"
	
</textarea>

<h6>spark shell</h6>

<p>create another bash <code class="language-plaintext highlighter-rouge">spark-shell.sh</code> file to start spark :</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:270px;width: 100%;">

#!/usr/bin/env bash
SPARK_MASTER_PORT_7077_TCP_ADDR=`getent hosts spark_master | awk '{ print $1 }'`
export SPARK_LOCAL_IP=`hostname -I | awk '{print $1}'`
/remove_alias.sh # problems with hostname alias, see https://issues.apache.org/jira/browse/SPARK-6680
cd /usr/local/spark
./bin/spark-shell \
	--master spark://${SPARK_MASTER_PORT_7077_TCP_ADDR:-spark_master}:${SPARK_MASTER_ENV_SPARK_MASTER_PORT:-7077}  \
	-i ${SPARK_LOCAL_IP} \
	--properties-file /spark-defaults.conf \
	"$@" 	
</textarea>

<h6>spark configuration</h6>

<p>one of the important files we need is to customize configuration by injecting a config file  <code class="language-plaintext highlighter-rouge">spark-defaults.conf</code>:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:170px;width: 100%;">

spark.driver.port	7001
spark.fileserver.port	7002
spark.broadcast.port	7003
spark.replClassServer.port	7004
spark.blockManager.port 7005
spark.executor.port 7006
spark.ui.port 4040
spark.broadcast.factory org.apache.spark.broadcast.HttpBroadcastFactory
 	
</textarea>

<h6>clean up</h6>

<p>and finaly clean up the host alias and remove the redundancies <code class="language-plaintext highlighter-rouge">remove_alias.sh</code>:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:100px;width: 100%;">
#!/usr/bin/env bash
sed '1d' /etc/hosts &gt; tmpHosts
cat tmpHosts &gt; /etc/hosts
rm tmpHosts
</textarea>

<p><b>prepare a docker compose file to manage the package under same network</b>
<br /><br />
Creating docker compose file for whole package:</p>

<p>Within root directory, create a <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> file. The file must be contain 4 sections, master service, 2 slaves and finaly spark container.
Therefore, the file would be comprised of:</p>

<textarea id="myTextArea" style="background-color: black;color: green; height:270px;width: 100%;">
version: "2"

services:
  master:
    build: ./hadoop-master
    ports:
      - "9870:9870"
      - "8088:8088"
      - "19888:19888"
      - "8188:8188"
    container_name: "master"
  slave1:
    build: ./hadoop-slave
    container_name: "slave1"
    ports:
      - "9991:9864"
      - "8041:8042"
  slave2:
    build: ./hadoop-slave
    container_name: "slave2"
    ports:
      - "9992:9864"
      - "8042:8042"
  spark:
    build: ./spark
    container_name: "spark"
</textarea>

<p>run <code class="language-plaintext highlighter-rouge">docker-compose up --build -d</code></p>

<p>enjoy your environment.</p>
 ]]></content:encoded>
				</item>
			
				<item>
					
						<title>Protein function prediction</title>
					
					<link>https://atabakkh.github.io/computationalbiology/2017/04/17/ProteinFunctionPrediction.html</link>

					

					<pubDate>Mon, 17 Apr 2017 00:00:00 UTC</pubDate>
					<guid>https://atabakkh.github.io/computationalbiology/2017/04/17/ProteinFunctionPrediction</guid>

					
						<category><![CDATA[ ComputationalBiology ]]></category>
					

					<description><![CDATA[ Since the Functionality of protein is what effect in behaviour, disease, etc. This talk is about to describe what exactly protein function means. ]]></description>
					<content:encoded><![CDATA[ <h4>Automate detection of functionality of protein via genome sequence</h4><p><b>WHAT IS PROTEIN FUNCTION?</b>
<br /></p>
<p>
The meaning of organic capacity is vague, and the correct significance of the term fluctuates in view of the setting in which it is utilized. Clearly the natural capacity of a protein has in excess of one viewpoint. Take for instance a protein kinase; in the biochemical viewpoint, a kinase's capacity would be the phosphorylation of a hydroxyl gathering of a particular substrate. The extent of intrigue suggested by this definition does not require any more than an 'immaterial' protein performing alone in vitro. Be that as it may, proteins play out their capacity inside a life form, and this has outcomes going from the subcellular to the entire creature level. In a physiological perspective, a similar kinase might be a piece of a flagging pathway, where a protein both phosphorylates, and is phosphorylated by, communicating accomplices. A transformation in this kinase may cause an infection, so yet another angle is a phenotypic or therapeutic one. Along these lines, when discussing a protein's capacity, we should dependably determine the viewpoint or parts of the utilitarian depiction. When embarking to utilize a capacity expectation apparatus, let alone to create one, we should remember which useful angle or perspectives we are attempting to foresee, and utilize the suitable vocabulary. </p>
<p><br /><br />
<b>DESCRIBING FUNCTION</b></p>
<p>
Having characterized the practical viewpoint or parts of intrigue, by what method should work be depicted in a computationally amiable manner? Protein grouping and structure data are effortlessly rendered machine readable. Protein groupings are spoken to as character strings that are suited for some undertakings: pairwise and numerous arrangements, theme discovering, database seeking, and a huge number of different assignments went for extricating organic data from the succession. The capacity to express arrangement data as a character string managable for computational handling dovetails with calculations ready to break down this data. With respect to structure, despite the fact that the portrayal is more unpredictable, the PDB and mmCIF record designs do as such for most reasonable purposes. There is a restricted, all around characterized punctuation engaged with the two cases. </p>
<p>
As opposed to grouping and structure data, the explanation of a protein is composed in human dialect, which passes on the nuances and complexities of its capacity and also the exploratory confirmation which underpins it, its examination history, and different attributes. As is acknowledged in human dialect, especially in science where the vocabulary is designed and reevaluated day by day, numerous terms are synonymous. This synonymy is befuddling to people and significantly more so to machines. Different variables add to the issue, for example, stirring up or precluding utilitarian parts of a protein. At long last, what constitutes practical data and what does not? </p>
<p>
To make utilitarian explanation open to computational preparing, there is a need to pass on it in a controlled and very much characterized design. The requirement for a controlled vocabulary and all around characterized connections in portraying capacity was first perceived by natural chemists and appeared as the Enzyme Commission Classification, or EC. EC characterizes responses in a four-level progressive system, and those are noted by a four-position identifier, going from the general 'Lyase' in the primary position through the more particular 'Nitrogen lyase' on to 'Smelling salts lyases' to the at last particular 'Histidine-alkali lyase' in the fourth position. EC answers the necessities both for a controlled vocabulary and for a very much characterized connection between terms. Notwithstanding, there are different capacities other than the enzymatic and other practical angles other than the biochemical that are needed in explanation. In 1993, the main genomic plan to order quality items was proposed for Escherichia coli. The consistent theme among those plans is the foundation of a controlled vocabulary and much of the time an arrangement that returns from the general to the particular.</p>
<p>
The Gene Ontology (GO) at present fills in as the overwhelming methodology for machine-clear utilitarian comment. GO is a structure comprising of controlled vocabularies depicting three parts of quality item work: atomic capacity, natural process and cell area. The last mentioned, despite the fact that not a practical perspective in essence, is esteemed critical for useful explanation since proteins don't work in a vacuum (or rather in a saline arrangement) yet inside an all around characterized setting of the living cell. Every metaphysics is executed as a coordinated non-cyclic chart (DAG) where terms are spoken to as hubs in the diagram and are orchestrated from the general to the particular. The DAG game plan implies that every hub may have in excess of a solitary parent-this is to depict capacities that are engaged with in excess of a solitary organic process, cell compartment, or atomic capacity. </p>
<p>
The GO Annotation (GOA) task's central goal is to comment on genomes and different grouping and structure databases utilizing GO terms. At the point when GO terms are appointed to a quality item, a proof code expressing how the comment was acquired is relegated also. In this way, the unwavering quality of the explanation is noted-is it in light of trial prove that can be followed to a creator (high dependability)? Or on the other hand it is just construed by homology exchange comment that has not been looked into by a keeper (low unwavering quality)? GO has nine such confirmation codes, and they are talked about in the GO site (http://www.geneontology.org/GO.evidence.shtml). Note that the confirmation code isn't a piece of the philosophy DAG, but instead is related endless supply of a term, or terms, to a quality or quality item. There are other, more particular ontologies that are utilized as a part of more particular parts of atomic science or for different purposes, for example, therapeutically fascinating explanation or genomic perspectives special to a given living being (http://www.obo.org).</p>
 ]]></content:encoded>
				</item>
			

		</channel>
	</rss>

