<?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: EazyHood</title>
    <description>The latest articles on DEV Community by EazyHood (@eazyhood).</description>
    <link>https://dev.to/eazyhood</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%2F4059300%2Fc073d3f1-31e0-436b-a478-e94296e9d9cc.png</url>
      <title>DEV Community: EazyHood</title>
      <link>https://dev.to/eazyhood</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eazyhood"/>
    <language>en</language>
    <item>
      <title>The cache key that only looked at one of its four inputs</title>
      <dc:creator>EazyHood</dc:creator>
      <pubDate>Sun, 02 Aug 2026 15:47:59 +0000</pubDate>
      <link>https://dev.to/eazyhood/the-cache-key-that-ignored-the-question-2g59</link>
      <guid>https://dev.to/eazyhood/the-cache-key-that-ignored-the-question-2g59</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xudapwnqmzh9vdxl63n.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xudapwnqmzh9vdxl63n.jpg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Paritok-official/paritok-4b-v1" rel="noopener noreferrer"&gt;Paritok&lt;/a&gt; is a context compressor. You&lt;br&gt;
hand it a blob of text, a query describing what you care about, and a compression level,&lt;br&gt;
and it hands back a shorter version that is supposed to keep what matters &lt;em&gt;for that query&lt;/em&gt;&lt;br&gt;
and drop the rest. Results are cached, because compression is the expensive part.&lt;/p&gt;

&lt;p&gt;I was not looking for bugs in it. I was building an audit harness to measure how much&lt;br&gt;
&lt;em&gt;meaning&lt;/em&gt; compression costs — feeding the same corpus through with different queries and&lt;br&gt;
checking which critical substrings survive: file paths, error types, line numbers,&lt;br&gt;
identifiers.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;Two rows in my results table were byte-for-byte identical. Different query, different&lt;br&gt;
compression level, same 544 characters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query="Fix the IntegrityError on commit"              level=L0  -&amp;gt;  159 tok  cache_hit=False
query="Explain the tax rounding TODO in compute_tax"  level=L3  -&amp;gt;  159 tok  cache_hit=True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;compress()&lt;/code&gt; takes four things that shape its output — &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;level&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;kind&lt;/code&gt;. The cache was keyed on one of them:&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;sid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;content_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cached_compressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the first caller's answer was served to every later caller with the same input text, no&lt;br&gt;
matter what they actually asked. The second caller got a compression tuned to somebody&lt;br&gt;
else's question, marked &lt;code&gt;cache_hit=True&lt;/code&gt;, with nothing in the response to say so.&lt;/p&gt;

&lt;p&gt;This is worse than a stale cache. A stale cache gives you an old answer to &lt;em&gt;your&lt;/em&gt; question.&lt;br&gt;
This gives you a fresh answer to &lt;em&gt;someone else's&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The fix is open as a PR: &lt;strong&gt;&lt;a href="https://github.com/Paritok-official/paritok-4b-v1/pull/17" rel="noopener noreferrer"&gt;Paritok-official/paritok-4b-v1#17&lt;/a&gt;&lt;/strong&gt; — 2 files, +118/−12.&lt;/p&gt;

&lt;p&gt;The audit harness that surfaced it is at&lt;br&gt;
&lt;a href="https://github.com/EazyHood/paritok-audit" rel="noopener noreferrer"&gt;EazyHood/paritok-audit&lt;/a&gt; (Apache-2.0).&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="c1"&gt;# The compressed result depends on query, level and kind as well as on the
# content -- query is documented as driving keep/drop -- so all four belong
# in the key. Keying on content alone returned the first caller's answer to
# every later caller. `sid` deliberately stays content-only, because
# expand_context resolves originals by content and that behaviour is correct.
&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;content_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 4. Cache check (same content AND same intent gets the same answer)
&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cached_compressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;Three decisions in this patch are worth explaining, because the obvious fix is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Don't change &lt;code&gt;sid&lt;/code&gt; — add a key beside it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tempting one-liner is to fold the query into &lt;code&gt;sid&lt;/code&gt; and move on. That breaks something&lt;br&gt;
else: &lt;code&gt;expand_context&lt;/code&gt; resolves a compressed reference back to its original &lt;em&gt;by content&lt;br&gt;
hash&lt;/em&gt;. &lt;code&gt;sid&lt;/code&gt; has to stay content-only for that to keep working. So the patch introduces a&lt;br&gt;
separate &lt;code&gt;cache_key&lt;/code&gt; and leaves &lt;code&gt;sid&lt;/code&gt; exactly as it was. Two identifiers, two jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Classify &lt;code&gt;kind&lt;/code&gt; before building the key, not after.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kind&lt;/code&gt; was being resolved &lt;em&gt;after&lt;/em&gt; the cache check. That meant a caller passing &lt;code&gt;kind=None&lt;/code&gt;&lt;br&gt;
and a caller passing the same kind explicitly would key differently, even though the&lt;br&gt;
compressor would treat them identically — a cache miss on every first call, forever.&lt;/p&gt;

&lt;p&gt;Moving classification above the cache check fixed that, and it fixed a second bug on the&lt;br&gt;
way: only &lt;code&gt;LocalModelStrategy&lt;/code&gt; sniffed &lt;code&gt;kind&lt;/code&gt; internally, so &lt;code&gt;GpuServerStrategy&lt;/code&gt; was&lt;br&gt;
forwarding &lt;code&gt;kind=None&lt;/code&gt; to the server. Central classification means every backend now&lt;br&gt;
receives a real value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Join the parts with a null byte.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;"\x00".join(...)&lt;/code&gt; rather than &lt;code&gt;"-".join(...)&lt;/code&gt; or plain concatenation. Without a separator&lt;br&gt;
that cannot occur in the inputs, &lt;code&gt;level="L0"&lt;/code&gt; + &lt;code&gt;kind="1code"&lt;/code&gt; and &lt;code&gt;level="L01"&lt;/code&gt; +&lt;br&gt;
&lt;code&gt;kind="code"&lt;/code&gt; collide into the same key. It costs nothing and removes a class of bug that&lt;br&gt;
would be genuinely miserable to track down later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The test.&lt;/strong&gt; &lt;code&gt;tests/test_cache_key_intent.py&lt;/code&gt; (+92) asserts the property directly: same&lt;br&gt;
content with different queries must not return the same object, and the same content with&lt;br&gt;
the same intent must hit the cache. It fails on the original code and passes on the patch,&lt;br&gt;
which is the only claim about a fix I actually trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away from it
&lt;/h2&gt;

&lt;p&gt;I found this because my harness printed &lt;code&gt;cache_hit&lt;/code&gt; next to every row. If it had only&lt;br&gt;
printed token counts, two identical numbers would have looked like a compressor being&lt;br&gt;
consistent, which is exactly what you want it to be.&lt;/p&gt;

&lt;p&gt;The bug was invisible in the metric everyone watches and obvious in the one nobody logs.&lt;br&gt;
Since then I print the cache flag next to every cached result I benchmark — the cost is one&lt;br&gt;
column, and it is the column that told me the answer I was measuring wasn't mine.&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>devchallenge</category>
      <category>debugging</category>
      <category>python</category>
    </item>
  </channel>
</rss>
