[{"data":1,"prerenderedAt":1286},["ShallowReactive",2],{"page-\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Freproducing-a-race-condition-deterministically\u002F":3},{"id":4,"title":5,"body":6,"description":1249,"extension":1250,"meta":1251,"navigation":82,"path":1282,"seo":1283,"stem":1284,"__hash__":1285},"content\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Freproducing-a-race-condition-deterministically\u002Findex.md","Reproducing a Race Condition Deterministically",{"type":7,"value":8,"toc":1238},"minimark",[9,13,18,53,57,60,160,338,517,521,535,549,553,620,624,723,731,755,762,765,769,772,787,890,897,900,993,997,1000,1010,1086,1096,1156,1167,1171,1181,1190,1196,1200,1229,1234],[10,11,12],"p",{},"An intermittent failure in production, a ticket that says \"cannot reproduce\", and a defensive lock added on suspicion: that is the usual life cycle of a race condition. The alternative is a test that fails on every single run while the bug is present, which is achievable far more often than people expect because Python's races nearly always have a window that a test can hold open rather than wait for.",[14,15,17],"h2",{"id":16},"prerequisites","Prerequisites",[19,20,21,34,44],"ul",{},[22,23,24,25,29,30,33],"li",{},"Python 3.9+; ",[26,27,28],"code",{},"threading"," and ",[26,31,32],{},"unittest.mock"," are standard library.",[22,35,36,39,40,43],{},[26,37,38],{},"pytest >= 8.0"," plus ",[26,41,42],{},"pytest-timeout",", so a mis-written coordination deadlocks into a failure rather than a hang.",[22,45,46,47,52],{},"The concepts in ",[48,49,51],"a",{"href":50},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002F","testing threads and race conditions",", particularly why the GIL does not make statements atomic.",[14,54,56],{"id":55},"solution","Solution",[10,58,59],{},"Take a lazily-initialised client — the classic double-initialisation bug — and make it fail reliably.",[61,62,67],"pre",{"className":63,"code":64,"language":65,"meta":66,"style":66},"language-python shiki shiki-themes github-light github-dark","import threading\n\n\nclass ServiceRegistry:\n    def __init__(self, build_client):\n        self._build_client = build_client\n        self._client = None\n        self.build_count = 0\n\n    def get_client(self):\n        if self._client is None:            # check\n            client = self._build_client()   # ← the window: slow, and not atomic\n            self.build_count += 1\n            self._client = client           # act\n        return self._client\n","python","",[26,68,69,77,84,89,95,101,107,113,119,124,130,136,142,148,154],{"__ignoreMap":66},[70,71,74],"span",{"class":72,"line":73},"line",1,[70,75,76],{},"import threading\n",[70,78,80],{"class":72,"line":79},2,[70,81,83],{"emptyLinePlaceholder":82},true,"\n",[70,85,87],{"class":72,"line":86},3,[70,88,83],{"emptyLinePlaceholder":82},[70,90,92],{"class":72,"line":91},4,[70,93,94],{},"class ServiceRegistry:\n",[70,96,98],{"class":72,"line":97},5,[70,99,100],{},"    def __init__(self, build_client):\n",[70,102,104],{"class":72,"line":103},6,[70,105,106],{},"        self._build_client = build_client\n",[70,108,110],{"class":72,"line":109},7,[70,111,112],{},"        self._client = None\n",[70,114,116],{"class":72,"line":115},8,[70,117,118],{},"        self.build_count = 0\n",[70,120,122],{"class":72,"line":121},9,[70,123,83],{"emptyLinePlaceholder":82},[70,125,127],{"class":72,"line":126},10,[70,128,129],{},"    def get_client(self):\n",[70,131,133],{"class":72,"line":132},11,[70,134,135],{},"        if self._client is None:            # check\n",[70,137,139],{"class":72,"line":138},12,[70,140,141],{},"            client = self._build_client()   # ← the window: slow, and not atomic\n",[70,143,145],{"class":72,"line":144},13,[70,146,147],{},"            self.build_count += 1\n",[70,149,151],{"class":72,"line":150},14,[70,152,153],{},"            self._client = client           # act\n",[70,155,157],{"class":72,"line":156},15,[70,158,159],{},"        return self._client\n",[61,161,163],{"className":63,"code":162,"language":65,"meta":66,"style":66},"import threading\n\nimport pytest\n\n\n@pytest.mark.timeout(10)\ndef test_client_is_built_exactly_once():\n    inside = threading.Event()              # first thread reached the window\n    release = threading.Event()             # test says the first may continue\n\n    def slow_build():\n        inside.set()\n        assert release.wait(timeout=5), \"test never released the builder\"\n        return object()\n\n    registry = ServiceRegistry(build_client=slow_build)\n    results: list[object] = []\n\n    first = threading.Thread(target=lambda: results.append(registry.get_client()))\n    first.start()\n    assert inside.wait(timeout=5), \"first thread never entered the builder\"\n\n    # Second thread arrives while the first is provably still inside the window.\n    second = threading.Thread(target=lambda: results.append(registry.get_client()))\n    second.start()\n\n    release.set()\n    for thread in (first, second):\n        thread.join(timeout=5)\n        assert not thread.is_alive()\n\n    assert registry.build_count == 1, \"the client was built more than once\"\n    assert results[0] is results[1], \"the two threads got different clients\"\n",[26,164,165,169,173,178,182,186,191,196,201,206,210,215,220,225,230,234,240,246,251,257,263,269,274,280,286,292,297,303,309,315,321,326,332],{"__ignoreMap":66},[70,166,167],{"class":72,"line":73},[70,168,76],{},[70,170,171],{"class":72,"line":79},[70,172,83],{"emptyLinePlaceholder":82},[70,174,175],{"class":72,"line":86},[70,176,177],{},"import pytest\n",[70,179,180],{"class":72,"line":91},[70,181,83],{"emptyLinePlaceholder":82},[70,183,184],{"class":72,"line":97},[70,185,83],{"emptyLinePlaceholder":82},[70,187,188],{"class":72,"line":103},[70,189,190],{},"@pytest.mark.timeout(10)\n",[70,192,193],{"class":72,"line":109},[70,194,195],{},"def test_client_is_built_exactly_once():\n",[70,197,198],{"class":72,"line":115},[70,199,200],{},"    inside = threading.Event()              # first thread reached the window\n",[70,202,203],{"class":72,"line":121},[70,204,205],{},"    release = threading.Event()             # test says the first may continue\n",[70,207,208],{"class":72,"line":126},[70,209,83],{"emptyLinePlaceholder":82},[70,211,212],{"class":72,"line":132},[70,213,214],{},"    def slow_build():\n",[70,216,217],{"class":72,"line":138},[70,218,219],{},"        inside.set()\n",[70,221,222],{"class":72,"line":144},[70,223,224],{},"        assert release.wait(timeout=5), \"test never released the builder\"\n",[70,226,227],{"class":72,"line":150},[70,228,229],{},"        return object()\n",[70,231,232],{"class":72,"line":156},[70,233,83],{"emptyLinePlaceholder":82},[70,235,237],{"class":72,"line":236},16,[70,238,239],{},"    registry = ServiceRegistry(build_client=slow_build)\n",[70,241,243],{"class":72,"line":242},17,[70,244,245],{},"    results: list[object] = []\n",[70,247,249],{"class":72,"line":248},18,[70,250,83],{"emptyLinePlaceholder":82},[70,252,254],{"class":72,"line":253},19,[70,255,256],{},"    first = threading.Thread(target=lambda: results.append(registry.get_client()))\n",[70,258,260],{"class":72,"line":259},20,[70,261,262],{},"    first.start()\n",[70,264,266],{"class":72,"line":265},21,[70,267,268],{},"    assert inside.wait(timeout=5), \"first thread never entered the builder\"\n",[70,270,272],{"class":72,"line":271},22,[70,273,83],{"emptyLinePlaceholder":82},[70,275,277],{"class":72,"line":276},23,[70,278,279],{},"    # Second thread arrives while the first is provably still inside the window.\n",[70,281,283],{"class":72,"line":282},24,[70,284,285],{},"    second = threading.Thread(target=lambda: results.append(registry.get_client()))\n",[70,287,289],{"class":72,"line":288},25,[70,290,291],{},"    second.start()\n",[70,293,295],{"class":72,"line":294},26,[70,296,83],{"emptyLinePlaceholder":82},[70,298,300],{"class":72,"line":299},27,[70,301,302],{},"    release.set()\n",[70,304,306],{"class":72,"line":305},28,[70,307,308],{},"    for thread in (first, second):\n",[70,310,312],{"class":72,"line":311},29,[70,313,314],{},"        thread.join(timeout=5)\n",[70,316,318],{"class":72,"line":317},30,[70,319,320],{},"        assert not thread.is_alive()\n",[70,322,324],{"class":72,"line":323},31,[70,325,83],{"emptyLinePlaceholder":82},[70,327,329],{"class":72,"line":328},32,[70,330,331],{},"    assert registry.build_count == 1, \"the client was built more than once\"\n",[70,333,335],{"class":72,"line":334},33,[70,336,337],{},"    assert results[0] is results[1], \"the two threads got different clients\"\n",[339,340,343,513],"figure",{"className":341},[342],"diagram",[344,345,352,353,352,357,352,361,352,379,352,387,352,396,352,402,352,406,352,410,352,417,352,420,352,423,352,433,352,438,352,443,352,447,352,453,352,457,352,464,352,468,352,471,352,475,352,479,352,483,352,486,352,490,352,498,352,504,352,507],"svg",{"viewBox":346,"role":347,"ariaLabelledBy":348,"xmlns":351},"0 0 820 268","img",[349,350],"race2-t","race2-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[354,355,356],"title",{"id":349},"Holding the initialisation window open with two events",[358,359,360],"desc",{"id":350},"A timeline. The first thread enters the builder and sets the inside event, then blocks on the release event. The test observes the inside event and starts the second thread, which passes the None check because the first has not assigned yet. The test then sets the release event and both threads complete, having built the client twice.",[362,363,364,365,352],"defs",{},"\n    ",[366,367,374],"marker",{"id":368,"viewBox":369,"refX":370,"refY":371,"markerWidth":372,"markerHeight":372,"orient":373},"race2-a","0 0 10 10","9","5","7","auto-start-reverse",[375,376],"path",{"d":377,"fill":378},"M0 0 L10 5 L0 10 z","#3d405b",[380,381],"rect",{"x":382,"y":382,"width":383,"height":384,"rx":385,"fill":386},"0","820","268","14","#fffdf8",[388,389,395],"text",{"x":390,"y":391,"textAnchor":392,"fontSize":393,"fontWeight":394,"fill":378},"410","28","middle","16","700","The test, not the scheduler, decides the interleaving",[388,397,401],{"x":398,"y":399,"fontSize":400,"fontWeight":394,"fill":378},"40","82","12","thread 1",[388,403,405],{"x":398,"y":404,"fontSize":400,"fontWeight":394,"fill":378},"150","test",[388,407,409],{"x":398,"y":408,"fontSize":400,"fontWeight":394,"fill":378},"218","thread 2",[72,411],{"x1":412,"y1":413,"x2":414,"y2":413,"stroke":415,"strokeWidth":416},"122","78","790","rgba(61,64,91,0.35)","1.3",[72,418],{"x1":412,"y1":419,"x2":414,"y2":419,"stroke":415,"strokeWidth":416},"146",[72,421],{"x1":412,"y1":422,"x2":414,"y2":422,"stroke":415,"strokeWidth":416},"214",[380,424],{"x":425,"y":426,"width":427,"height":428,"rx":429,"fill":430,"stroke":431,"strokeWidth":432},"132","60","140","34","8","#e6f0ea","#81b29a","1.7",[388,434,437],{"x":435,"y":399,"textAnchor":392,"fontSize":436,"fill":378},"202","10.5","check: None",[380,439],{"x":440,"y":426,"width":404,"height":428,"rx":429,"fill":441,"stroke":442,"strokeWidth":432},"280","#f7f0da","#f2cc8f",[388,444,446],{"x":445,"y":399,"textAnchor":392,"fontSize":436,"fill":378},"355","inside.set(); wait",[380,448],{"x":449,"y":426,"width":450,"height":428,"rx":429,"fill":451,"stroke":452,"strokeWidth":432},"600","180","#fbe9e3","#e07a5f",[388,454,456],{"x":455,"y":399,"textAnchor":392,"fontSize":436,"fill":378},"690","build #1, assign",[380,458],{"x":459,"y":460,"width":461,"height":428,"rx":429,"fill":462,"stroke":378,"strokeWidth":463},"300","128","170","#f4f1de","1.6",[388,465,467],{"x":466,"y":404,"textAnchor":392,"fontSize":436,"fill":378},"385","inside.wait() returns",[380,469],{"x":470,"y":460,"width":404,"height":428,"rx":429,"fill":462,"stroke":378,"strokeWidth":463},"490",[388,472,474],{"x":473,"y":404,"textAnchor":392,"fontSize":436,"fill":378},"565","release.set()",[380,476],{"x":477,"y":478,"width":404,"height":428,"rx":429,"fill":430,"stroke":431,"strokeWidth":432},"470","196",[388,480,482],{"x":481,"y":408,"textAnchor":392,"fontSize":436,"fill":378},"545","check: still None",[380,484],{"x":485,"y":478,"width":404,"height":428,"rx":429,"fill":451,"stroke":452,"strokeWidth":432},"640",[388,487,489],{"x":488,"y":408,"textAnchor":392,"fontSize":436,"fill":378},"715","build #2",[72,491],{"x1":466,"y1":460,"x2":492,"y2":493,"stroke":494,"strokeWidth":416,"strokeDashArray":495},"360","98","rgba(61,64,91,0.4)",[496,497],"4","3",[72,499],{"x1":477,"y1":500,"x2":501,"y2":502,"stroke":494,"strokeWidth":416,"strokeDashArray":503},"162","520","192",[496,497],[72,505],{"x1":473,"y1":500,"x2":485,"y2":493,"stroke":494,"strokeWidth":416,"strokeDashArray":506},[496,497],[388,508,512],{"x":390,"y":509,"textAnchor":392,"fontSize":510,"fontWeight":394,"fill":511},"254","11.5","#8f3d22","build_count == 2 — every run, not one in fifty",[514,515,516],"figcaption",{},"Two events are enough: one to learn that the window is open, one to close it. Nothing here depends on how the scheduler feels.",[14,518,520],{"id":519},"why-this-works","Why this works",[10,522,523,524,527,528,531,532,534],{},"The bug needs the second thread to evaluate ",[26,525,526],{},"self._client is None"," after the first has entered ",[26,529,530],{},"_build_client"," and before it assigns. Waiting for that by chance requires the window to be wide, which it is not — ",[26,533,530],{}," might take microseconds. The seam makes the window as wide as the test wants.",[10,536,537,540,541,544,545,548],{},[26,538,539],{},"inside.set()"," tells the test the first thread has passed the check and is in the builder. ",[26,542,543],{},"release.wait()"," keeps it there. The test starts the second thread at that exact moment, so the ordering is a precondition of the test rather than a coincidence. Every run therefore produces the same interleaving, and the invariant assertion — ",[26,546,547],{},"build_count == 1"," — fails every time until the lock is added.",[14,550,552],{"id":551},"edge-cases-and-failure-modes","Edge cases and failure modes",[19,554,555,573,591,600,614],{},[22,556,557,561,562,29,565,568,569,572],{},[558,559,560],"strong",{},"No timeout on the waits."," Every ",[26,563,564],{},"Event.wait",[26,566,567],{},"Thread.join"," needs one, or a mistake in the coordination produces a hang instead of a failure. The assertion on ",[26,570,571],{},"wait","'s return value converts a timed-out wait into a readable message.",[22,574,575,578,579,582,583,586,587,590],{},[558,576,577],{},"Assertions inside worker threads."," An ",[26,580,581],{},"AssertionError"," in a thread is printed and discarded. Collect results in a list and assert in the main thread, or use a ",[26,584,585],{},"ThreadPoolExecutor"," whose ",[26,588,589],{},"result()"," re-raises.",[22,592,593,596,597,599],{},[558,594,595],{},"Exceptions before the event is set."," If the builder raises before ",[26,598,539],{},", the test waits the full five seconds and then fails with a confusing message. Set the event first, then do the work that can fail.",[22,601,602,605,606,609,610,613],{},[558,603,604],{},"The seam left in production."," A ",[26,607,608],{},"build_client"," parameter with a real default is fine. A module-level ",[26,611,612],{},"TESTING"," flag is not — the production path stops being the tested path.",[22,615,616,619],{},[558,617,618],{},"Fixing only the symptom."," Adding a lock around the assignment but not the check leaves the bug intact. The lock must cover the whole check-then-act sequence, which the same test will confirm.",[14,621,623],{"id":622},"the-fix-and-confirming-the-test-sees-it","The fix, and confirming the test sees it",[61,625,627],{"className":63,"code":626,"language":65,"meta":66,"style":66},"import threading\n\n\nclass ServiceRegistry:\n    def __init__(self, build_client):\n        self._build_client = build_client\n        self._client = None\n        self._lock = threading.Lock()\n        self.build_count = 0\n\n    def get_client(self):\n        # Fast path: no lock once initialised.\n        if self._client is None:\n            with self._lock:\n                # Re-check inside the lock: another thread may have built it\n                # while this one was waiting for the lock.\n                if self._client is None:\n                    client = self._build_client()\n                    self.build_count += 1\n                    self._client = client\n        return self._client\n",[26,628,629,633,637,641,645,649,653,657,662,666,670,674,679,684,689,694,699,704,709,714,719],{"__ignoreMap":66},[70,630,631],{"class":72,"line":73},[70,632,76],{},[70,634,635],{"class":72,"line":79},[70,636,83],{"emptyLinePlaceholder":82},[70,638,639],{"class":72,"line":86},[70,640,83],{"emptyLinePlaceholder":82},[70,642,643],{"class":72,"line":91},[70,644,94],{},[70,646,647],{"class":72,"line":97},[70,648,100],{},[70,650,651],{"class":72,"line":103},[70,652,106],{},[70,654,655],{"class":72,"line":109},[70,656,112],{},[70,658,659],{"class":72,"line":115},[70,660,661],{},"        self._lock = threading.Lock()\n",[70,663,664],{"class":72,"line":121},[70,665,118],{},[70,667,668],{"class":72,"line":126},[70,669,83],{"emptyLinePlaceholder":82},[70,671,672],{"class":72,"line":132},[70,673,129],{},[70,675,676],{"class":72,"line":138},[70,677,678],{},"        # Fast path: no lock once initialised.\n",[70,680,681],{"class":72,"line":144},[70,682,683],{},"        if self._client is None:\n",[70,685,686],{"class":72,"line":150},[70,687,688],{},"            with self._lock:\n",[70,690,691],{"class":72,"line":156},[70,692,693],{},"                # Re-check inside the lock: another thread may have built it\n",[70,695,696],{"class":72,"line":236},[70,697,698],{},"                # while this one was waiting for the lock.\n",[70,700,701],{"class":72,"line":242},[70,702,703],{},"                if self._client is None:\n",[70,705,706],{"class":72,"line":248},[70,707,708],{},"                    client = self._build_client()\n",[70,710,711],{"class":72,"line":253},[70,712,713],{},"                    self.build_count += 1\n",[70,715,716],{"class":72,"line":259},[70,717,718],{},"                    self._client = client\n",[70,720,721],{"class":72,"line":265},[70,722,159],{},[10,724,725,726,730],{},"The inner re-check is the part people omit. Without it, both threads block on the lock, the first builds, and the second — already past the outer check — builds again the moment it acquires the lock. The test above catches exactly that mistake, which is why it is worth running against the ",[727,728,729],"em",{},"partial"," fix as well as against no fix at all.",[61,732,736],{"className":733,"code":734,"language":735,"meta":66,"style":66},"language-bash shiki shiki-themes github-light github-dark","pytest tests\u002Ftest_registry.py::test_client_is_built_exactly_once --count=10 -q\n","bash",[26,737,738],{"__ignoreMap":66},[70,739,740,744,748,752],{"class":72,"line":73},[70,741,743],{"class":742},"sScJk","pytest",[70,745,747],{"class":746},"sZZnC"," tests\u002Ftest_registry.py::test_client_is_built_exactly_once",[70,749,751],{"class":750},"sj4cs"," --count=10",[70,753,754],{"class":750}," -q\n",[61,756,760],{"className":757,"code":759,"language":388,"meta":66},[758],"language-text","..........                                                    [100%]\n10 passed in 0.14s\n",[26,761,759],{"__ignoreMap":66},[10,763,764],{},"Ten passes with the full fix, ten failures without it, and ten failures with the lock but no re-check. That three-way check is what makes the test trustworthy rather than merely green.",[14,766,768],{"id":767},"generalising-the-pattern","Generalising the pattern",[10,770,771],{},"The registry above is one instance of a shape that recurs constantly, and recognising the shape is what makes the next reproduction quick rather than exploratory.",[10,773,774,775,778,779,782,783,786],{},"Every check-then-act race has the same three parts: a ",[558,776,777],{},"predicate"," read from shared state, an ",[558,780,781],{},"action"," that changes that state, and a ",[558,784,785],{},"gap"," between them that is not atomic. Naming those three for a given bug tells you immediately where the seam goes — inside the gap — and what to assert — the invariant the predicate was protecting.",[788,789,790,809],"table",{},[791,792,793],"thead",{},[794,795,796,800,803,806],"tr",{},[797,798,799],"th",{},"Bug",[797,801,802],{},"Predicate",[797,804,805],{},"Action",[797,807,808],{},"Invariant to assert",[810,811,812,828,844,860,874],"tbody",{},[794,813,814,818,822,825],{},[815,816,817],"td",{},"Double initialisation",[815,819,820],{},[26,821,526],{},[815,823,824],{},"assign the client",[815,826,827],{},"built exactly once",[794,829,830,833,838,841],{},[815,831,832],{},"Cache stampede",[815,834,835],{},[26,836,837],{},"key not in cache",[815,839,840],{},"compute and store",[815,842,843],{},"computed once per key",[794,845,846,849,854,857],{},[815,847,848],{},"Duplicate order",[815,850,851],{},[26,852,853],{},"not exists(order_id)",[815,855,856],{},"insert the row",[815,858,859],{},"one row per id",[794,861,862,865,868,871],{},[815,863,864],{},"Lost update",[815,866,867],{},"read the counter",[815,869,870],{},"write counter + 1",[815,872,873],{},"total equals the number of increments",[794,875,876,879,884,887],{},[815,877,878],{},"Double release",[815,880,881],{},[26,882,883],{},"not self._released",[815,885,886],{},"release the resource",[815,888,889],{},"release called once",[10,891,892,893,896],{},"The gap is not always visible as a line of code. It can be a property access that triggers a lazy import, a ",[26,894,895],{},"__getattr__"," that queries a registry, or a logging call that formats an expensive repr — anything that yields control. That is why the seam-based approach beats reasoning: rather than proving the gap exists, the test simply holds it open and observes what another thread sees. Where the gap turns out not to exist, the test passes immediately and costs nothing; where it does, the failure names the invariant directly.",[10,898,899],{},"Reading the table the other way is also useful during review: any code matching column two without a lock spanning columns one and three is a race, whether or not anyone has seen it fail yet.",[339,901,903,990],{"className":902},[342],[344,904,352,909,352,912,352,915,352,922,352,926,352,931,352,938,352,942,352,946,352,952,352,956,352,960,352,963,352,967,352,971,352,974,352,977,352,983,352,987],{"viewBox":905,"role":347,"ariaLabelledBy":906,"xmlns":351},"0 0 800 226",[907,908],"shape-t","shape-d",[354,910,911],{"id":907},"The three parts of every check-then-act race",[358,913,914],{"id":908},"A predicate reads shared state, a gap follows in which another thread may observe the unchanged state, and an action then modifies it. The seam goes in the gap and the assertion goes on the invariant the predicate was protecting.",[362,916,364,917,352],{},[366,918,920],{"id":919,"viewBox":369,"refX":370,"refY":371,"markerWidth":372,"markerHeight":372,"orient":373},"shape-a",[375,921],{"d":377,"fill":378},[380,923],{"x":382,"y":382,"width":924,"height":925,"rx":385,"fill":386},"800","226",[388,927,930],{"x":928,"y":391,"textAnchor":392,"fontSize":929,"fontWeight":394,"fill":378},"400","15.5","Find these three and the test writes itself",[380,932],{"x":428,"y":933,"width":934,"height":935,"rx":936,"fill":430,"stroke":431,"strokeWidth":937},"58","200","66","11","2",[388,939,777],{"x":940,"y":941,"textAnchor":392,"fontSize":400,"fontWeight":394,"fill":378},"134","84",[388,943,945],{"x":940,"y":944,"textAnchor":392,"fontSize":936,"fill":378},"106","reads shared state",[72,947],{"x1":948,"y1":949,"x2":950,"y2":949,"stroke":378,"strokeWidth":463,"markerEnd":951},"238","91","266","url(#shape-a)",[380,953],{"x":954,"y":933,"width":955,"height":935,"rx":936,"fill":451,"stroke":452,"strokeWidth":937},"272","220",[388,957,959],{"x":958,"y":941,"textAnchor":392,"fontSize":400,"fontWeight":394,"fill":378},"382","the gap",[388,961,962],{"x":958,"y":944,"textAnchor":392,"fontSize":936,"fill":511},"put the seam here",[72,964],{"x1":965,"y1":949,"x2":966,"y2":949,"stroke":378,"strokeWidth":463,"markerEnd":951},"496","524",[380,968],{"x":969,"y":933,"width":970,"height":935,"rx":936,"fill":430,"stroke":431,"strokeWidth":937},"530","236",[388,972,781],{"x":973,"y":941,"textAnchor":392,"fontSize":400,"fontWeight":394,"fill":378},"648",[388,975,976],{"x":973,"y":944,"textAnchor":392,"fontSize":936,"fill":378},"changes shared state",[380,978],{"x":428,"y":979,"width":980,"height":981,"rx":982,"fill":441,"stroke":442,"strokeWidth":937},"148","732","56","10",[388,984,986],{"x":928,"y":985,"textAnchor":392,"fontSize":400,"fontWeight":394,"fill":378},"172","the invariant is what the predicate was protecting",[388,988,989],{"x":928,"y":502,"textAnchor":392,"fontSize":936,"fill":378},"assert on it, not on whether a lock was taken",[514,991,992],{},"The lock must span all three boxes. A lock covering only the action leaves the gap open, which is the partial fix the test above is designed to reject.",[14,994,996],{"id":995},"when-no-seam-is-available","When no seam is available",[10,998,999],{},"Sometimes the window sits inside code you cannot change — a third-party client, a C extension wrapper, a function whose signature is fixed by a framework. Two techniques cover most of those cases.",[10,1001,1002,1005,1006,1009],{},[558,1003,1004],{},"Patch a collaborator rather than the function."," The window is usually around a call to something slower: a database query, an HTTP request, a file read. Patching ",[727,1007,1008],{},"that"," collaborator with a slow version widens the window without touching the function under test.",[61,1011,1013],{"className":63,"code":1012,"language":65,"meta":66,"style":66},"from unittest.mock import patch\n\n\ndef test_double_write_without_modifying_the_service():\n    entered, release = threading.Event(), threading.Event()\n    original = repository.fetch\n\n    def slow_fetch(*args, **kwargs):\n        entered.set()\n        release.wait(timeout=5)\n        return original(*args, **kwargs)\n\n    # The seam is the collaborator, which is already injectable via patching.\n    with patch.object(repository, \"fetch\", slow_fetch):\n        ...\n",[26,1014,1015,1020,1024,1028,1033,1038,1043,1047,1052,1057,1062,1067,1071,1076,1081],{"__ignoreMap":66},[70,1016,1017],{"class":72,"line":73},[70,1018,1019],{},"from unittest.mock import patch\n",[70,1021,1022],{"class":72,"line":79},[70,1023,83],{"emptyLinePlaceholder":82},[70,1025,1026],{"class":72,"line":86},[70,1027,83],{"emptyLinePlaceholder":82},[70,1029,1030],{"class":72,"line":91},[70,1031,1032],{},"def test_double_write_without_modifying_the_service():\n",[70,1034,1035],{"class":72,"line":97},[70,1036,1037],{},"    entered, release = threading.Event(), threading.Event()\n",[70,1039,1040],{"class":72,"line":103},[70,1041,1042],{},"    original = repository.fetch\n",[70,1044,1045],{"class":72,"line":109},[70,1046,83],{"emptyLinePlaceholder":82},[70,1048,1049],{"class":72,"line":115},[70,1050,1051],{},"    def slow_fetch(*args, **kwargs):\n",[70,1053,1054],{"class":72,"line":121},[70,1055,1056],{},"        entered.set()\n",[70,1058,1059],{"class":72,"line":126},[70,1060,1061],{},"        release.wait(timeout=5)\n",[70,1063,1064],{"class":72,"line":132},[70,1065,1066],{},"        return original(*args, **kwargs)\n",[70,1068,1069],{"class":72,"line":138},[70,1070,83],{"emptyLinePlaceholder":82},[70,1072,1073],{"class":72,"line":144},[70,1074,1075],{},"    # The seam is the collaborator, which is already injectable via patching.\n",[70,1077,1078],{"class":72,"line":150},[70,1079,1080],{},"    with patch.object(repository, \"fetch\", slow_fetch):\n",[70,1082,1083],{"class":72,"line":156},[70,1084,1085],{},"        ...\n",[10,1087,1088,1091,1092,1095],{},[558,1089,1090],{},"Lower the switch interval and repeat."," When there is no collaborator to patch either, ",[26,1093,1094],{},"sys.setswitchinterval(1e-6)"," makes CPython preempt threads far more aggressively, which widens every window in the process. Combined with a few thousand iterations it converts a one-in-a-million race into a one-in-ten, which is enough for a regression test if it is marked and kept out of the fast suite.",[339,1097,1099,1153],{"className":1098},[342],[344,1100,352,1105,352,1108,352,1111,352,1114,352,1117,352,1122,352,1127,352,1132,352,1135,352,1138,352,1142,352,1145,352,1149],{"viewBox":1101,"role":347,"ariaLabelledBy":1102,"xmlns":351},"0 0 800 224",[1103,1104],"seam-t","seam-d",[354,1106,1107],{"id":1103},"Three places to put the seam, in order of preference",[358,1109,1110],{"id":1104},"Three options ranked. An injected callable in the code under test is the clearest and is deterministic. Patching a slow collaborator is almost as good and needs no production change. Lowering the interpreter switch interval is the fallback, widening every window but only making the failure probable rather than certain.",[380,1112],{"x":382,"y":382,"width":924,"height":1113,"rx":385,"fill":386},"224",[388,1115,1116],{"x":928,"y":391,"textAnchor":392,"fontSize":929,"fontWeight":394,"fill":378},"Prefer the seam closest to the window",[380,1118],{"x":1119,"y":1120,"width":1121,"height":1120,"rx":982,"fill":430,"stroke":431,"strokeWidth":937},"26","50","748",[388,1123,1126],{"x":1124,"y":1125,"fontSize":400,"fontWeight":394,"fill":378},"46","72","1 · injected callable",[388,1128,1131],{"x":1124,"y":1129,"fontSize":936,"fill":1130},"90","#2a5f49","deterministic · names the concurrency point · needs a one-line production change",[380,1133],{"x":1119,"y":1134,"width":1121,"height":1120,"rx":982,"fill":441,"stroke":442,"strokeWidth":937},"110",[388,1136,1137],{"x":1124,"y":425,"fontSize":400,"fontWeight":394,"fill":378},"2 · patched collaborator",[388,1139,1141],{"x":1124,"y":404,"fontSize":936,"fill":1140},"#8a5a00","deterministic · no production change · needs a collaborator inside the window",[380,1143],{"x":1119,"y":461,"width":1121,"height":1144,"rx":982,"fill":451,"stroke":452,"strokeWidth":937},"44",[388,1146,1148],{"x":1124,"y":1147,"fontSize":400,"fontWeight":394,"fill":378},"190","3 · switch interval plus repetition",[388,1150,1152],{"x":1124,"y":1151,"fontSize":936,"fill":511},"207","probabilistic · works anywhere · belongs in a nightly job, not the fast suite",[514,1154,1155],{},"Only the first two produce a test that fails every run. The third is a way to find the bug, not a way to keep it fixed.",[10,1157,1158,1159,1162,1163,1166],{},"Whichever seam is used, restore the setting afterwards. ",[26,1160,1161],{},"sys.setswitchinterval"," is process-global, so leaving it at a microsecond for the rest of the session slows every subsequent test measurably and can itself introduce flakiness elsewhere — a fixture with a ",[26,1164,1165],{},"finally"," that restores the original value is the minimum discipline.",[14,1168,1170],{"id":1169},"frequently-asked-questions","Frequently Asked Questions",[10,1172,1173,1176,1177,1180],{},[558,1174,1175],{},"Is adding a hook to production code just for a test acceptable?","\nA single injected callable with a no-op default is acceptable and often clarifying — it names the point where concurrency matters. What is not acceptable is test-only branching inside the function, such as an ",[26,1178,1179],{},"if TESTING"," check, because that means production takes a different path from the one under test.",[10,1182,1183,1186,1187,1189],{},[558,1184,1185],{},"What if I cannot find the window?","\nWiden the search by lowering ",[26,1188,1161],{}," so the interpreter preempts more aggressively, and run the operation from many threads with an invariant assertion. That turns a rare failure into a frequent one, which is enough to locate the window; then convert the frequent failure into a deterministic one with a seam.",[10,1191,1192,1195],{},[558,1193,1194],{},"Should the deterministic test replace the stress test?","\nIt should replace it in the fast suite. Keep a stress loop in a nightly job for the races nobody has thought of yet, but the known bug deserves a test that fails every time, not one that fails one run in fifty.",[14,1197,1199],{"id":1198},"related","Related",[19,1201,1202,1208,1215,1222],{},[22,1203,1204,1207],{},[48,1205,1206],{"href":50},"Testing Threads & Race Conditions"," — the barrier technique for when every thread runs the same code.",[22,1209,1210,1214],{},[48,1211,1213],{"href":1212},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Ftesting-thread-safety-with-barriers-and-events\u002F","Testing Thread Safety with Barriers and Events"," — the primitive-by-primitive patterns behind this coordination.",[22,1216,1217,1221],{},[48,1218,1220],{"href":1219},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Fdumping-stacks-on-deadlock-with-faulthandler\u002F","Dumping Stacks on Deadlock with faulthandler"," — what to do when the fix introduces a lock-ordering problem.",[22,1223,1224,1228],{},[48,1225,1227],{"href":1226},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-class-attributes-with-patch-object\u002F","Patching Class Attributes with patch.object"," — the alternative seam when injection is not available.",[10,1230,1231,1232],{},"← Back to ",[48,1233,1206],{"href":50},[1235,1236,1237],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":66,"searchDepth":79,"depth":79,"links":1239},[1240,1241,1242,1243,1244,1245,1246,1247,1248],{"id":16,"depth":79,"text":17},{"id":55,"depth":79,"text":56},{"id":519,"depth":79,"text":520},{"id":551,"depth":79,"text":552},{"id":622,"depth":79,"text":623},{"id":767,"depth":79,"text":768},{"id":995,"depth":79,"text":996},{"id":1169,"depth":79,"text":1170},{"id":1198,"depth":79,"text":1199},"Turn an intermittent concurrency bug into a test that fails every run: locate the window, inject a seam, coordinate threads with events, and assert on the invariant.","md",{"slug":1252,"type":1253,"breadcrumb":1254,"datePublished":1255,"dateModified":1255,"faq":1256,"howto":1263},"reproducing-a-race-condition-deterministically","article","Deterministic Repro","2026-09-18",[1257,1259,1261],{"q":1175,"a":1258},"A single injected callable with a no-op default is acceptable and often clarifying — it names the point where concurrency matters. What is not acceptable is test-only branching inside the function, such as an if TESTING check, because that means production takes a different path from the one under test.",{"q":1185,"a":1260},"Widen the search by lowering sys.setswitchinterval so the interpreter preempts more aggressively, and run the operation from many threads with an invariant assertion. That turns a rare failure into a frequent one, which is enough to locate the window; then convert the frequent failure into a deterministic one with a seam.",{"q":1194,"a":1262},"It should replace it in the fast suite. Keep a stress loop in a nightly job for the races nobody has thought of yet, but the known bug deserves a test that fails every time, not one that fails one run in fifty.",{"name":1264,"description":1265,"steps":1266},"How to make a race condition reproduce every run","Identify the non-atomic window, hold it open with a seam and events, and assert on the invariant it violates.",[1267,1270,1273,1276,1279],{"name":1268,"text":1269},"Locate the check-then-act window","Find the read-modify-write or check-then-act sequence whose intermediate state another thread can observe.",{"name":1271,"text":1272},"Inject a seam","Add an injected callable with a no-op default at the exact point inside the window.",{"name":1274,"text":1275},"Coordinate two threads with events","Use one event to signal that the first thread is inside the window and another to release it.",{"name":1277,"text":1278},"Assert on the invariant","Check the aggregate outcome — a count, a total, a single initialisation — rather than inspecting locks.",{"name":1280,"text":1281},"Verify it fails without the fix and passes with it","Run ten times in each state; ten failures then ten passes is the evidence the test is real.","\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Freproducing-a-race-condition-deterministically",{"title":5,"description":1249},"testing-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Freproducing-a-race-condition-deterministically\u002Findex","mld6hPGn48bs_mffKBItEP0GspUCa8NN9MUtPJY26YU",1789718768447]