<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jigon Yoo</title>
    <description>The latest articles on DEV Community by Jigon Yoo (@jigonyoo).</description>
    <link>https://dev.to/jigonyoo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4101706%2F2bbc8df4-989c-4de0-ac19-03fced7f02f4.jpg</url>
      <title>DEV Community: Jigon Yoo</title>
      <link>https://dev.to/jigonyoo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jigonyoo"/>
    <language>en</language>
    <item>
      <title>I removed every delete from my exactly-once guard. Then the bugs moved.</title>
      <dc:creator>Jigon Yoo</dc:creator>
      <pubDate>Mon, 31 Aug 2026 01:16:36 +0000</pubDate>
      <link>https://dev.to/jigonyoo/i-removed-every-delete-from-my-exactly-once-guard-then-the-bugs-moved-302i</link>
      <guid>https://dev.to/jigonyoo/i-removed-every-delete-from-my-exactly-once-guard-then-the-bugs-moved-302i</guid>
      <description>&lt;p&gt;Your monitoring will not flag a duplicate charge. Every one of them is a 200.&lt;/p&gt;

&lt;p&gt;The refund succeeded. It succeeded again four hundred milliseconds later on a&lt;br&gt;
second worker, and once more after a lost response made the client retry. Three&lt;br&gt;
successes, three green spans, three log lines that say &lt;code&gt;ok&lt;/code&gt;. The customer's&lt;br&gt;
statement is the only place the incident exists.&lt;/p&gt;

&lt;p&gt;Guardrails are usually built around &lt;em&gt;may this happen?&lt;/em&gt; — permissions, scopes,&lt;br&gt;
approval gates. This is the other question, and almost nothing asks it: &lt;strong&gt;did I&lt;br&gt;
already?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent a week building a small answer to it, and then had it torn apart four&lt;br&gt;
times. This is what each round moved, and why the last three rounds found&lt;br&gt;
nothing where I was looking.&lt;/p&gt;
&lt;h2&gt;
  
  
  The easy half
&lt;/h2&gt;

&lt;p&gt;The mechanism is one line and it is genuinely correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_CREAT&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_EXCL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_WRONLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;O_EXCL&lt;/code&gt; is a compare-and-swap on existence. Exactly one process creates the&lt;br&gt;
file; everyone else gets &lt;code&gt;FileExistsError&lt;/code&gt;. No daemon, no Redis, no dependency.&lt;/p&gt;

&lt;p&gt;Forty OS processes — real processes, started before any of them are joined, not&lt;br&gt;
threads — racing the same charge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;40 concurrent processes, identical call | executed 40 | executed 1 (always)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Threads would have passed on the GIL alone and proved nothing about production.&lt;/p&gt;

&lt;p&gt;That number is real and it reproduces. It is also the easy half, and I spent&lt;br&gt;
two review rounds learning how easy.&lt;/p&gt;
&lt;h2&gt;
  
  
  And then someone dies
&lt;/h2&gt;

&lt;p&gt;A claim is a promise that someone is working. When the holder dies — OOM,&lt;br&gt;
deploy, &lt;code&gt;SIGKILL&lt;/code&gt; — the promise outlives the worker, and the file becomes a&lt;br&gt;
tombstone that blocks that call forever.&lt;/p&gt;

&lt;p&gt;So you need a TTL, and a way to take the claim back. And here is the thing&lt;br&gt;
nobody tells you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;POSIX has no "delete this specific inode."&lt;/strong&gt; &lt;code&gt;unlink&lt;/code&gt; takes a &lt;em&gt;path&lt;/em&gt;. Between&lt;br&gt;
the moment you decide a claim is stale and the moment you remove it, that path&lt;br&gt;
can point at something else — including a fresh claim that a faster process just&lt;br&gt;
won.&lt;/p&gt;
&lt;h2&gt;
  
  
  Round one: unlink
&lt;/h2&gt;

&lt;p&gt;My first reclaim was three lines and looked obviously right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# holder died; race again
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reads the stale record. A unlinks. A wins &lt;code&gt;O_EXCL&lt;/code&gt; and starts charging. B —&lt;br&gt;
which read the same stale bytes a microsecond earlier — unlinks &lt;strong&gt;A's fresh&lt;br&gt;
claim&lt;/strong&gt;, wins, and charges too.&lt;/p&gt;

&lt;p&gt;My test suite was green. It probed the expiry path with a single caller and the&lt;br&gt;
concurrency path from an empty directory, so the bug lived exactly in the gap&lt;br&gt;
between two passing tests.&lt;/p&gt;

&lt;p&gt;The repo still carries that variant, and forces the interleaving instead of&lt;br&gt;
hoping for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bug variants: claim-unlink 20/20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty out of twenty. Not "flaky under load" — deterministic, in about a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round two: I added a lock to guard a copy of the bug
&lt;/h2&gt;

&lt;p&gt;The fix serialized takeover with a sentinel file and a POSIX &lt;code&gt;flock&lt;/code&gt;. Tests&lt;br&gt;
green again.&lt;/p&gt;

&lt;p&gt;A second reviewer, who had not seen the first, replaced &lt;code&gt;flock&lt;/code&gt; with a no-op —&lt;br&gt;
which is what you get on NFS with &lt;code&gt;local_lock&lt;/code&gt;, or after a lock file is rotated&lt;br&gt;
out from under you. Detection went to &lt;strong&gt;zero&lt;/strong&gt;. The lock had never been doing&lt;br&gt;
the work; &lt;code&gt;O_EXCL&lt;/code&gt; on the sentinel was.&lt;/p&gt;

&lt;p&gt;And the branch the lock existed to protect was still &lt;code&gt;unlink&lt;/code&gt;-then-recreate, on&lt;br&gt;
a path, on a timestamp that can be stale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bug variants: sentinel-unlink 20/20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;I had added a lock to guard a copy of the bug I was fixing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same review found the lock was scoped to the claim &lt;em&gt;directory&lt;/em&gt;, not to the&lt;br&gt;
key — so four unrelated charges, racing nothing at all, blocked each other and&lt;br&gt;
most of them were refused. The README calls a missed charge the worse half of the trade. I had&lt;br&gt;
shipped it to calls that were never racing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Round three: stop deleting
&lt;/h2&gt;

&lt;p&gt;Two failures, one shape. Both were code that removed a path.&lt;/p&gt;

&lt;p&gt;So I removed the removal. Claims are now generations — &lt;code&gt;&amp;lt;sig&amp;gt;.gen0.claim&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;&amp;lt;sig&amp;gt;.gen1.claim&lt;/code&gt;, and so on. An expired claim is never reclaimed. It stays&lt;br&gt;
where it is, and a new generation opens beside it. There is no "take the claim&lt;br&gt;
back" operation to get wrong, because there is no such operation.&lt;/p&gt;

&lt;p&gt;The evidence for that is a command, not a paragraph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-cE&lt;/span&gt; &lt;span class="s2"&gt;"unlink|os&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;replace|rename|flock"&lt;/span&gt; once_guard.py
0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the same forced interleaving that caught both earlier variants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fixed generation: 0/20 double executions
16 workers x 40 trials: 0 double executions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reclaim path was now provably clean. I thought I was done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round four: the bugs moved
&lt;/h2&gt;

&lt;p&gt;The next review found eleven more defects. &lt;strong&gt;Not one of them was in the writing&lt;br&gt;
side.&lt;/strong&gt; Every single one was in the reading side — deciding whether a claim is&lt;br&gt;
old, whether a half-written result is finished, whose clock to believe.&lt;/p&gt;

&lt;p&gt;The one worth your time is claim age. A claim records when it was made. That&lt;br&gt;
record is written &lt;em&gt;by the claim&lt;/em&gt;, so it can be wrong — a worker whose clock&lt;br&gt;
jumped backward writes a timestamp from the past and its live claim looks&lt;br&gt;
ancient. Meanwhile the filesystem records an mtime, which that worker cannot&lt;br&gt;
forge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mtime_age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;younger&lt;/strong&gt; of the two. Not the more accurate one — the safer one. If the&lt;br&gt;
record lies and I believe it, I steal a live worker's claim and charge twice. If&lt;br&gt;
mtime bounds the age and I am wrong, a dead worker's claim stays locked a while&lt;br&gt;
longer and a refund is late. I picked the direction I would rather be wrong in,&lt;br&gt;
and that is the whole design decision.&lt;/p&gt;

&lt;p&gt;Writing the claim took one syscall. Deciding whether a claim is &lt;em&gt;currently&lt;br&gt;
valid&lt;/em&gt; took eleven fixes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Round five: my tests believed the same lie
&lt;/h2&gt;

&lt;p&gt;With that rule in the library, two tests started failing intermittently. They&lt;br&gt;
seeded an expired claim like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;holder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:0}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;at=0&lt;/code&gt; is 1970. The file's mtime is &lt;em&gt;now&lt;/em&gt;. And the library believes the younger&lt;br&gt;
of the two — because I made it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;freshly written file, at=0:  0.0000s   → not expired (ttl 0.01)
50ms later:                  0.0503s   → expired
mtime backdated to 1970:     1788115004s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a worker that reached the age check within the TTL saw a fresh claim and&lt;br&gt;
correctly refused. &lt;strong&gt;The library was doing exactly what I designed. The test was&lt;br&gt;
believing the claim's own story about itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix was one &lt;code&gt;os.utime&lt;/code&gt; in a shared helper. Remove that single line again and&lt;br&gt;
nine tests across four files fail — which is how I know the helper is load&lt;br&gt;
bearing and not decoration.&lt;/p&gt;

&lt;p&gt;Three rounds, three layers, one mistake: &lt;strong&gt;trusting what a claim says about&lt;br&gt;
itself without checking it against something the claim cannot forge.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Then I stopped looking for defects
&lt;/h2&gt;

&lt;p&gt;Five rounds all asked the same question — &lt;em&gt;what breaks?&lt;/em&gt; At some point that&lt;br&gt;
becomes the failure mode. So the last pass asked a different one: &lt;strong&gt;does the&lt;br&gt;
documentation say what the code does?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I pulled every claim out of the README — every behavioral assertion, every&lt;br&gt;
number, every command, every support-range statement — numbered them, and&lt;br&gt;
checked each one against the code or against a command I actually ran. One&lt;br&gt;
hundred and fifty-eight claims. One hundred and thirty matched. Seven were&lt;br&gt;
wrong. Three I could not check in this environment. &lt;strong&gt;Eighteen had no supporting&lt;br&gt;
evidence in the repository at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That third category is the interesting one. Those sentences were not false. They&lt;br&gt;
just had nothing behind them — the kind of line a stranger reads and asks "how&lt;br&gt;
do you know that?" and you have no answer.&lt;/p&gt;

&lt;p&gt;Two of mine, both embarrassing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I published latency figures and called them reproducible. There was no script
in the repo that produced them. They are gone now.&lt;/li&gt;
&lt;li&gt;I described five sibling repos as "each one file." One click disproves it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No amount of adversarial defect review would ever have caught either. The code&lt;br&gt;
was right. The document was writing checks the repo could not cash.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it still does not do
&lt;/h2&gt;

&lt;p&gt;The demo has a sixth row, and it exists to be embarrassing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the side effect outlives its TTL | off: executed 2 | on: executed 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two, and two. The guard does not help. Once a real side effect has outlived its&lt;br&gt;
TTL, the old owner and its legitimate replacement have both genuinely executed,&lt;br&gt;
and no bookkeeping turns two real charges into one.&lt;/p&gt;

&lt;p&gt;There is a control row too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the same four, with note wrongly inside the key | off: executed 4 | on: executed 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put a value the model rewrites into the idempotency key and every retry gets a&lt;br&gt;
new identity. &lt;code&gt;$126.00&lt;/code&gt; extra in &lt;strong&gt;both&lt;/strong&gt; columns. The guard does not fix a&lt;br&gt;
misconfigured key, and I report that row separately from the headline instead of&lt;br&gt;
averaging it in.&lt;/p&gt;

&lt;p&gt;And what is untested is not a limitation I discovered, it is one I am telling&lt;br&gt;
you about: power loss is not covered — the result file is fsynced, the parent&lt;br&gt;
directory is not. NFS is not covered; the warning in the README about &lt;code&gt;O_EXCL&lt;/code&gt;&lt;br&gt;
on older servers is judgement, not measurement. I have marked those as&lt;br&gt;
judgement in the README rather than letting them sit next to the measured&lt;br&gt;
numbers looking equally solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check in your own code
&lt;/h2&gt;

&lt;p&gt;If you have an exactly-once guard anywhere — a Redis &lt;code&gt;SETNX&lt;/code&gt;, a unique index, a&lt;br&gt;
lock table — the interesting question is not how you take the lock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when the holder dies?&lt;/strong&gt; If the answer is a TTL, you have a&lt;br&gt;
reclaim path, and that is where the bugs are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does your guard ask the state how old it is, or check it against something the&lt;br&gt;
state cannot forge?&lt;/strong&gt; Redis TTLs, an &lt;code&gt;updated_at&lt;/code&gt; column, a &lt;code&gt;heartbeat_at&lt;/code&gt; in a&lt;br&gt;
lock table — every one of those is a value the record wrote about itself. For&lt;br&gt;
each place you read one, write down in a single sentence which way you fail if&lt;br&gt;
that value is a lie. If the answer is "double execution," that is your next&lt;br&gt;
incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is your race test's sensitivity a function of your CPU count?&lt;/strong&gt; Mine was. The&lt;br&gt;
same suite on a 4-core machine and a 2-core one gave different answers, and the&lt;br&gt;
statistical version passed on both while the bug was live. Hooking the read and&lt;br&gt;
forcing the loser to lose finds it 20 times out of 20 in about a second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you ever counted the claims in your own README?&lt;/strong&gt; How many of them can&lt;br&gt;
you prove with one command?&lt;/p&gt;

&lt;h2&gt;
  
  
  Take it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/jigonyoo/once-guard
&lt;span class="nb"&gt;cd &lt;/span&gt;once-guard &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python3 demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen seconds, six scenarios, the failures priced in dollars — including the&lt;br&gt;
two rows where the guard does nothing. 71 tests, no dependencies, MIT, CI on&lt;br&gt;
Python 3.8 and 3.11.&lt;/p&gt;

&lt;p&gt;The most useful thing you can do with it is point the demo at your own guard and&lt;br&gt;
see which rows you lose.&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
