<?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: acejayl</title>
    <description>The latest articles on DEV Community by acejayl (@acejayl).</description>
    <link>https://dev.to/acejayl</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%2F4088861%2Fa7bab6fc-223b-40f8-ae4f-847f6702a3ee.png</url>
      <title>DEV Community: acejayl</title>
      <link>https://dev.to/acejayl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/acejayl"/>
    <language>en</language>
    <item>
      <title>The flaky test was right: a 58%-reproducible race in a scroll-reading pipeline's disk cache</title>
      <dc:creator>acejayl</dc:creator>
      <pubDate>Fri, 21 Aug 2026 21:36:25 +0000</pubDate>
      <link>https://dev.to/acejayl/the-flaky-test-was-right-a-58-reproducible-race-in-a-scroll-reading-pipelines-disk-cache-964</link>
      <guid>https://dev.to/acejayl/the-flaky-test-was-right-a-58-reproducible-race-in-a-scroll-reading-pipelines-disk-cache-964</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;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://scrollprize.org/" rel="noopener noreferrer"&gt;Vesuvius Challenge&lt;/a&gt; uses machine learning to read carbonized Herculaneum scrolls — 2,000-year-old papyrus that was buried by the eruption of Vesuvius and can never be physically unrolled. Its open-source monorepo, &lt;a href="https://github.com/ScrollPrize/villa" rel="noopener noreferrer"&gt;ScrollPrize/villa&lt;/a&gt;, contains the &lt;code&gt;vesuvius&lt;/code&gt; Python package that researchers use to stream multi-terabyte CT scan volumes and train ink-detection models.&lt;/p&gt;

&lt;p&gt;I was setting up that package on my Windows 11 machine (the project's CI only tests Ubuntu — the workflow file literally says &lt;em&gt;"Extend this list once the build scripts for macOS and Windows are confirmed"&lt;/em&gt;), working with an AI coding assistant to run the test suite on a platform it had never been tested on. One test failed. Then it passed. Then it failed again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;The test — &lt;code&gt;test_shared_cache_multiprocess_reads_are_not_torn&lt;/code&gt; — spawns four processes that read one scroll volume through a shared on-disk chunk cache. Run it once and you might see nothing wrong. So I ran it twelve times: &lt;strong&gt;7 failures out of 12&lt;/strong&gt;, all &lt;code&gt;PermissionError: [WinError 5] Access is denied&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A 58% flake is not a flake. It's a bug with a coin flip attached.&lt;/p&gt;

&lt;p&gt;The cache is on the hot path for real usage: it's the component behind the package's documented &lt;code&gt;volume_cache_dir&lt;/code&gt; config and the &lt;code&gt;--cache-dir&lt;/code&gt; flag of its inference CLI. Any PyTorch &lt;code&gt;DataLoader&lt;/code&gt; with &lt;code&gt;num_workers &amp;gt; 0&lt;/code&gt; puts multiple processes into exactly this concurrent pattern. On Windows, training runs would randomly die mid-epoch.&lt;/p&gt;

&lt;p&gt;Digging in (a standalone reproducer that propagated full worker tracebacks instead of &lt;code&gt;repr(exc)&lt;/code&gt;), the failure turned out to have &lt;strong&gt;three separate surfaces&lt;/strong&gt;, each hiding behind the previous one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cache-entry commit.&lt;/strong&gt; The zarr library commits each cache entry with a write-temp-then-&lt;code&gt;os.replace&lt;/code&gt; pattern. On POSIX, &lt;code&gt;rename(2)&lt;/code&gt; over a file another process has open is legal. On Windows, &lt;code&gt;MoveFileEx(MOVEFILE_REPLACE_EXISTING)&lt;/code&gt; returns &lt;code&gt;ERROR_ACCESS_DENIED&lt;/code&gt;. Four workers populating the same content-addressed keys collide constantly. (This is upstream &lt;a href="https://github.com/zarr-developers/zarr-python/issues/3522" rel="noopener noreferrer"&gt;zarr-developers/zarr-python#3522&lt;/a&gt; — open since October, three confirmations, no fix.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache read.&lt;/strong&gt; Fix the write path and a second surface appears: a concurrent commit can deny the &lt;em&gt;reader's&lt;/em&gt; open, too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eviction accounting.&lt;/strong&gt; The package's own LRU sweep caught only &lt;code&gt;FileNotFoundError&lt;/code&gt; when deleting old entries. Windows raises &lt;code&gt;PermissionError&lt;/code&gt; for in-use files — and the sweep then &lt;em&gt;subtracted the file's bytes from the size budget anyway&lt;/em&gt;, under-evicting a cache whose entire job is staying under a byte budget. &lt;strong&gt;This one is a genuine bug on every operating system&lt;/strong&gt;, not just Windows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix follows one principle: &lt;em&gt;a cache is an optimization, never a source of truth, so a refused cache operation must never abort the read that triggered it.&lt;/em&gt; I wrapped only the cache-side store: refused writes degrade to no-ops (logged at debug), unreadable entries report as a miss — which extends the store's own existing "missing file = miss" semantics, so the outer cache simply refetches from the source. The eviction sweep now skips undeletable entries without crediting their bytes and evicts the next-oldest instead. No platform-specific branches anywhere; the trade-off (a full disk becomes slow refetches rather than a crash) is deliberate and documented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measured result: 58% failure → 0 failures in 12 consecutive runs.&lt;/strong&gt; The package's Windows test suite went from 47 passed / 2 failed to &lt;strong&gt;52 passed / 0 failed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because a 58% race is a terrible CI signal, I also added &lt;strong&gt;three deterministic regression tests&lt;/strong&gt; that &lt;em&gt;force&lt;/em&gt; each &lt;code&gt;PermissionError&lt;/code&gt; surface via monkeypatching instead of racing for it — each verified to fail against the unfixed code on any OS. (Plus a bonus find while in there: the LRU test stamped files with 1–3 &lt;em&gt;nanosecond&lt;/em&gt; timestamps, which NTFS — 100 ns resolution — collapses to &lt;code&gt;st_mtime_ns == 0&lt;/code&gt;, silently destroying the ordering the test depends on. ext4 has 1 ns resolution, which is why Linux CI never noticed.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The full fix, tests, and methodology:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/ScrollPrize/villa/pull/1545" rel="noopener noreferrer"&gt;https://github.com/ScrollPrize/villa/pull/1545&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three files: the cache wrapper + eviction fix in &lt;code&gt;vesuvius/src/vesuvius/ink_detection/volume_io.py&lt;/code&gt;, the regression tests in &lt;code&gt;vesuvius/tests/ink_detection/test_volume_io.py&lt;/code&gt;, and a platform marker in &lt;code&gt;pyproject.toml&lt;/code&gt; (the CUDA-only &lt;code&gt;cucim-cu13&lt;/code&gt; dependency ships manylinux wheels only, which made &lt;code&gt;uv sync --extra all&lt;/code&gt; unresolvable on Windows and macOS).&lt;/p&gt;
&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A component that crashed 58% of the time under multiprocess access on Windows now runs clean, on the hot path used by anyone training ink-detection models with DataLoader workers and a disk cache.&lt;/li&gt;
&lt;li&gt;An OS-independent eviction accounting bug is gone.&lt;/li&gt;
&lt;li&gt;CI gets a deterministic signal for a whole class of failure it previously couldn't see — the racing test only catches the bug half the time even on the affected platform.&lt;/li&gt;
&lt;li&gt;The diagnosis is documented down to the Win32 semantics, including what the upstream zarr issue was missing (mechanism, failure rate, minimal repro), so it's actionable beyond this one repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two lessons I'm keeping: &lt;strong&gt;run flaky tests twelve times, not twice&lt;/strong&gt; — a 58% failure rate read as "flaky" for months because nobody measured it; and &lt;strong&gt;fix one surface at a time&lt;/strong&gt;, because two of the three bugs here were invisible until the one in front of them was gone.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Workflow transparency: I did this with an AI coding assistant (Claude) driving the investigation under my direction — measuring the failure rate, bisecting the three surfaces, and drafting the fix — with every measurement re-run and verified on my machine. The villa project explicitly welcomes LLM-assisted contributions with human commentary, and this writeup plus the PR discussion is exactly that.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;A multiprocess race condition is the hardest kind of bug to &lt;em&gt;see&lt;/em&gt;: the failure lives in the timing overlap between separate OS processes, so a single-process debugger or a stack trace from one worker tells you almost nothing. I used Sentry to make this one visible — and to prove the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error Monitoring — capturing the race with context.&lt;/strong&gt; I instrumented the four cache-reader processes so each captures its &lt;code&gt;PermissionError&lt;/code&gt; to Sentry with the details that actually matter: the worker PID, the shared cache directory, and which of the three failure surfaces it hit (the &lt;code&gt;os.replace&lt;/code&gt; commit vs. a concurrent open). Instead of a bare &lt;code&gt;WinError 5&lt;/code&gt;, each issue carries the full concurrent context, and Sentry groups the four near-simultaneous failures into one issue — immediately showing this is a &lt;em&gt;collision&lt;/em&gt;, not four independent flukes.&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%2Fpdqq0812zjfkr4t5hfef.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%2Fpdqq0812zjfkr4t5hfef.jpg" alt=" " width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distributed Tracing — making the invisible collision visible.&lt;/strong&gt; This is the part a debugger can't do. I propagated one trace across all four worker processes (Sentry's &lt;code&gt;continue_trace&lt;/code&gt; / trace-header propagation), with spans on each cache &lt;code&gt;open&lt;/code&gt; and &lt;code&gt;read&lt;/code&gt;. In the trace view, the four processes line up on a single timeline and you can &lt;em&gt;see&lt;/em&gt; them overlapping on the exact same content-addressed cache key at the same instant — which is precisely the window where zarr's write-temp-then-&lt;code&gt;os.replace&lt;/code&gt; commit collides on Windows. The trace turns an abstract "race condition" into a picture of four spans stacking on one resource.&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%2F87htmrltda2k4mgy84or.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%2F87htmrltda2k4mgy84or.jpg" alt=" " width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before / after, in the same dashboard.&lt;/strong&gt; I ran the identical workload two ways, tagged as two Sentry environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;villa-cache-unfixed&lt;/code&gt; — captured &lt;code&gt;PermissionError&lt;/code&gt; issues, and a trace where the four workers produced only &lt;strong&gt;21&lt;/strong&gt; &lt;code&gt;cache.read&lt;/code&gt; spans between them: several died mid-read the instant the race fired.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;villa-cache-fixed&lt;/code&gt; — the same four workers hitting the same collision window, but the hardened cache degrades a refused write to a miss instead of raising. &lt;strong&gt;Zero&lt;/strong&gt; issues, and &lt;strong&gt;48&lt;/strong&gt; &lt;code&gt;cache.read&lt;/code&gt; spans — every worker completed every read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same race window, same span instrumentation: errors and truncated traces on one environment, silence and complete traces on the other. That contrast &lt;em&gt;is&lt;/em&gt; the proof the fix works.&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%2Fhubyh51aeegdozejgi47.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%2Fhubyh51aeegdozejgi47.jpg" alt=" " width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The one trick that makes the cross-process trace work is propagating the parent's trace headers into each spawned worker, then continuing that trace inside the worker:&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;# parent: start one trace, hand its headers to every worker process
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_race_demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_scope&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;iter_trace_propagation_headers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# worker: continue the SAME trace, so all four line up in one trace view
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;continue_trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_worker_read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker_pid&lt;/span&gt;&lt;span class="sh"&gt;"&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;getpid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache.open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open_volume(cache)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open_volume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cache_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# cache.read spans; on PermissionError -&amp;gt; set_context(...) + capture_exception()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Tooling used: Sentry Error Monitoring (contextual exception capture + issue grouping) and Distributed Tracing (cross-process trace propagation with cache spans), via a standalone reproduction harness that runs the identical four-worker workload against the unfixed and fixed cache and reports both to Sentry.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Same race window, same span instrumentation: errors and truncated traces on one environment, silence and complete traces on the other. That contrast &lt;em&gt;is&lt;/em&gt; the proof the fix works.&lt;/p&gt;

&lt;p&gt;The one trick that makes the cross-process trace work is propagating the parent's trace headers into each spawned worker, then continuing that trace inside the worker:&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;# parent: start one trace, hand its headers to every worker process
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_race_demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_scope&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;iter_trace_propagation_headers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# worker: continue the SAME trace, so all four line up in one trace view
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;continue_trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_worker_read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker_pid&lt;/span&gt;&lt;span class="sh"&gt;"&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;getpid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache.open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open_volume(cache)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open_volume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cache_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# cache.read spans; on PermissionError -&amp;gt; set_context(...) + capture_exception()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Tooling used: Sentry Error Monitoring (contextual exception capture + issue grouping) and Distributed Tracing (cross-process trace propagation with cache spans), via a standalone reproduction harness that runs the identical four-worker workload against the unfixed and fixed cache and reports both to Sentry.&lt;/em&gt;&lt;/p&gt;

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