<?xml version="1.0" encoding="UTF-8"?>
<!-- Собирается скриптом scripts/make-assets.mjs -->
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Notes — Behruz Avezmatov</title>
  <subtitle>Short write-ups of problems I got stuck on. Written so I do not forget, published so somebody else does not lose an evening to them.</subtitle>
  <link href="https://kietos.me/feed.en.xml" rel="self"/>
  <link href="https://kietos.me/notes.html?lang=en"/>
  <id>https://kietos.me/feed.en.xml</id>
  <updated>2026-05-26T00:00:00Z</updated>
  <author><name>Behruz Avezmatov</name><email>behruzavezmatov@gmail.com</email></author>
  <entry>
    <title>Mask the data before the model, not after</title>
    <link href="https://kietos.me/note.html?id=mask-before-model&amp;lang=en"/>
    <id>https://kietos.me/note.html?id=mask-before-model</id>
    <updated>2026-05-26T00:00:00Z</updated>
    <summary>The obvious approach — send the text to the model and clean up the answer — does not work at all. Here is why the order of the steps matters more than the steps themselves.</summary>
    <category term="backend"/>
    <category term="llm"/>
    <category term="приватность"/>
    <content type="html">&lt;p&gt;The task sounded simple: process work messages with a language model, but in a way where names, phone numbers and contract numbers never leak.&lt;/p&gt;
&lt;p&gt;My first idea was wrong — instructively wrong.&lt;/p&gt;
&lt;h2&gt;What I wanted to do&lt;/h2&gt;
&lt;p&gt;Send the text to the model, get the answer, strip personal data from the answer. The scheme looks workable right up until one question: what already happened to the original text?&lt;/p&gt;
&lt;p&gt;It left. After that you can clean whatever you like — the data is already outside the perimeter.&lt;/p&gt;
&lt;h2&gt;What actually works&lt;/h2&gt;
&lt;p&gt;Masking goes first:&lt;/p&gt;
&lt;pre class=&quot;md-code&quot;&gt;&lt;code&gt;message → Presidio → [NAME_1], [PHONE_1] → model → answer → substitute back&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model physically never sees the real values. It works with labels, and that does not stop it understanding the meaning.&lt;/p&gt;
&lt;h2&gt;What follows for the architecture&lt;/h2&gt;
&lt;p&gt;Since masking has to happen before the model call, it cannot be an output filter — its place is in the pipeline, before the most expensive step:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;the &lt;strong&gt;gateway&lt;/strong&gt; accepts the message and puts it in a queue;&lt;/li&gt;&lt;li&gt;the &lt;strong&gt;worker&lt;/strong&gt; takes it, masks it, calls the local model;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;substitution back&lt;/strong&gt; returns the values into the finished answer.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;I had to split the gateway from the worker: processing takes seconds, and holding an HTTP connection through it means hitting timeouts within the first dozen requests.&lt;/p&gt;
&lt;h2&gt;The check that catches a regression&lt;/h2&gt;
&lt;p&gt;Worker logs print the text at every step. If someone reorders things and masking slides past the model call, you see it immediately — a real phone number where a label should be.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Why a circle detector does not find bubbles</title>
    <link href="https://kietos.me/note.html?id=why-hough-misses-bubbles&amp;lang=en"/>
    <id>https://kietos.me/note.html?id=why-hough-misses-bubbles</id>
    <updated>2026-05-04T00:00:00Z</updated>
    <summary>HoughCircles misses on eight bubble recordings out of ten. The cause is what you feed it.</summary>
    <category term="компьютерное зрение"/>
    <category term="opencv"/>
    <content type="html">&lt;p&gt;A bubble is a circle. So find circles. That is what I thought for the first half-day.&lt;/p&gt;
&lt;p&gt;In a textbook image HoughCircles finds circles. On real footage it either sees nothing or sees hundreds of circles where there are none, and threshold tuning does not get you out of it.&lt;/p&gt;
&lt;h2&gt;The catch&lt;/h2&gt;
&lt;p&gt;The detector looks for edges. And edges on a real frame are broken for three reasons at once:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;uneven lighting&lt;/strong&gt; — one corner is brighter than another, and a threshold that suits the left eats everything on the right;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;bubbles are semi-transparent&lt;/strong&gt; — the boundary is blurred, the brightness step is weak;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;bubbles are small&lt;/strong&gt; — a dozen pixels each, and noise is comparable to signal.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Tuning the detector is pointless here: it is doing its job correctly, it is just being handed an unusable image.&lt;/p&gt;
&lt;h2&gt;What helped&lt;/h2&gt;
&lt;p&gt;The frame matters more than the detector. Two operations before it do more than any thresholds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CLAHE&lt;/strong&gt; — contrast equalisation over local tiles rather than the whole frame. The dark corner becomes comparable to the light one, and a single threshold starts working everywhere.&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;python&quot;&gt;&lt;code&gt;clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Top-Hat&lt;/strong&gt; — a morphological operation: the image minus its own opening. What remains is smaller than the structuring element and brighter than the background. Exactly our bubbles, without the large background and gradients.&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;python&quot;&gt;&lt;code&gt;kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
gray = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The lesson that carried over&lt;/h2&gt;
&lt;p&gt;When an algorithm &amp;quot;does not work&amp;quot;, look at its input before its settings. Out of this grew a habit of dumping the intermediate image after every stage — you can see exactly where things fall apart instead of guessing.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>A money transfer and a race between two requests</title>
    <link href="https://kietos.me/note.html?id=transfer-race&amp;lang=en"/>
    <id>https://kietos.me/note.html?id=transfer-race</id>
    <updated>2026-02-10T00:00:00Z</updated>
    <summary>Check the balance, debit, credit. A second request fits between step one and step two, and the balance goes negative.</summary>
    <category term="backend"/>
    <category term="postgres"/>
    <content type="html">&lt;p&gt;A learning payment system, a transfer between accounts. The code reads like the problem statement:&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;python&quot;&gt;&lt;code&gt;if account.balance &amp;gt;= amount:
    account.balance -= amount
    target.balance += amount&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Works, as long as there is one request.&lt;/p&gt;
&lt;h2&gt;Where it breaks&lt;/h2&gt;
&lt;p&gt;Two transfers arrive at once. Both read the balance — both pass the check. Both debit. The balance goes negative by the amount of the second transfer.&lt;/p&gt;
&lt;p&gt;Easy to reproduce: two requests in a loop, and within a few iterations you see a negative number where one cannot exist.&lt;/p&gt;
&lt;h2&gt;What does not help&lt;/h2&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Checking after the debit.&lt;/strong&gt; Narrows the window, does not close it: the race just becomes rarer, which means it will be caught in production instead.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;A lock in the application.&lt;/strong&gt; Works right up to the second process. And a second process appears the day the service goes behind a load balancer.&lt;/li&gt;&lt;/ul&gt;
&lt;h2&gt;What does help&lt;/h2&gt;
&lt;p&gt;Let the database decide — it knows how. The row is locked for the duration of the transaction:&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;sql&quot;&gt;&lt;code&gt;SELECT balance FROM accounts WHERE id = :id FOR UPDATE;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second request waits on that row until the first finishes, and reads the new value. Plus a constraint in the schema as the last line of defence:&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE accounts ADD CONSTRAINT balance_non_negative CHECK (balance &amp;gt;= 0);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The check in the code stays: it gives a person a readable error instead of a database exception. But it cannot be relied on alone — it is a hint, not a guarantee.&lt;/p&gt;
&lt;h2&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;A rule I now check first: if someone else's request can fit between a read and a write, one day it will.&lt;/p&gt;</content>
  </entry>
</feed>
