[{"data":1,"prerenderedAt":2174},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job":3,"surround:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job":2139,"ancestors:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job":2148,"children:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job":2153,"related:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job":2154},{"id":4,"title":5,"body":6,"dateModified":2106,"datePublished":2106,"description":2107,"extension":2108,"faq":2109,"keyword":2118,"kind":2119,"meta":2120,"navigation":306,"path":2128,"seo":2129,"slug":2132,"stem":2133,"topics":2134,"type":2137,"__hash__":2138},"docs\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job\u002Findex.md","Prevent Overlapping Runs of an Excel Report Job",{"type":7,"value":8,"toc":2092},"minimark",[9,24,37,205,210,221,225,243,852,866,1099,1114,1118,1121,1372,1379,1383,1386,1531,1538,1632,1636,1639,1815,1821,1825,1828,1942,1946,1957,1961,2001,2005,2013,2017,2026,2030,2036,2042,2048,2054,2058,2088],[10,11,12,13,17,18,23],"p",{},"A report job that takes ten minutes on a normal day and forty on a bad one will, sooner or later, still be running when the scheduler starts the next copy. Two runs then read the same inputs, write the same workbook at the same time, and send the same email twice — or one overwrites the other's half-written file. The fix is to make the job ",[14,15,16],"strong",{},"single-instance",": each run takes a lock when it starts, and a run that cannot get the lock knows another is in progress. This page, part of ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002F","Error Handling and Logging in Excel Automation",", builds a small, portable lock for Python report jobs.",[10,25,26,27,31,32,36],{},"If the job runs from cron on Linux, wrapping the command in ",[28,29,30],"code",{},"flock -n"," does the same without any code, as ",[19,33,35],{"href":34},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron"," shows. The in-code version adds what the wrapper cannot: per-report locks, a message saying who holds the lock, waiting with a timeout, and Windows support.",[38,39,47,48,47,52,47,56,47,64,47,74,47,81,47,86,47,91,47,94,47,98,47,104,47,106,47,108,47,113,47,118,47,128,47,134,47,139,47,142,47,147,47,152,47,157,47,160,47,165,47,170,47,175,47,178,47,185,47,190,47,193,47,196,47,201],"svg",{"viewBox":40,"role":41,"ariaLabelledBy":42,"xmlns":45,"style":46},"4 0 752 340","img",[43,44],"lock-seq-t","lock-seq-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[49,50,51],"title",{"id":43},"Two runs of the same report, one lock",[53,54,55],"desc",{"id":44},"A sequence between run A, run B and the operating system lock. Run A asks for the lock and gets it, then starts writing the workbook. Run B asks for the same lock while A holds it, is refused, reads that A holds it, and exits as skipped. When A finishes, the operating system releases the lock automatically.",[57,58],"rect",{"x":59,"y":60,"width":61,"height":62,"fill":63},"4","0","752","340","#ffffff",[57,65],{"x":66,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"20.0","16.0","180.0","48.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[75,76,80],"text",{"x":77,"y":78,"style":79},"110.0","44.5","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","run A",[57,82],{"x":83,"y":67,"width":68,"height":69,"rx":70,"fill":84,"stroke":85,"style":73},"290.0","#e7ebef","var(--line,#cdd5e6)",[75,87,90],{"x":88,"y":78,"style":89},"380.0","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","OS lock",[57,92],{"x":93,"y":67,"width":68,"height":69,"rx":70,"fill":84,"stroke":85,"style":73},"560.0",[75,95,97],{"x":96,"y":78,"style":89},"650.0","run B",[99,100],"line",{"x1":77,"y1":101,"x2":77,"y2":102,"stroke":85,"style":103},"64.0","324.0","stroke-width:1.5px;stroke-dasharray:5 4",[99,105],{"x1":88,"y1":101,"x2":88,"y2":102,"stroke":85,"style":103},[99,107],{"x1":96,"y1":101,"x2":96,"y2":102,"stroke":85,"style":103},[99,109],{"x1":110,"y1":111,"x2":112,"y2":111,"stroke":72,"style":73},"115.0","104.0","371.0",[114,115],"polygon",{"points":116,"fill":117},"375.0,104.0 366.0,109.0 366.0,99.0","#5b5cf0",[57,119],{"x":120,"y":121,"width":122,"height":123,"rx":124,"fill":125,"stroke":126,"style":127},"183.5","75.0","122.9","21.0","10","#d9f4f1","var(--teal,#0f9488)","stroke-width:1.2px",[75,129,133],{"x":130,"y":131,"style":132},"245.0","89.5","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","1. lock daily_sales",[99,135],{"x1":136,"y1":137,"x2":138,"y2":137,"stroke":72,"style":73},"375.0","152.0","119.0",[114,140],{"points":141,"fill":117},"115.0,152.0 124.0,147.0 124.0,157.0",[57,143],{"x":144,"y":145,"width":146,"height":123,"rx":124,"fill":63,"stroke":85,"style":127},"150.7","123.0","188.5",[75,148,151],{"x":130,"y":149,"style":150},"137.5","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","2. granted — write workbook",[99,153],{"x1":154,"y1":155,"x2":156,"y2":155,"stroke":72,"style":73},"645.0","200.0","389.0",[114,158],{"points":159,"fill":117},"385.0,200.0 394.0,195.0 394.0,205.0",[57,161],{"x":162,"y":163,"width":164,"height":123,"rx":124,"fill":63,"stroke":85,"style":127},"452.2","171.0","125.5",[75,166,169],{"x":167,"y":168,"style":150},"515.0","185.5","3. lock daily_sales",[99,171],{"x1":172,"y1":173,"x2":174,"y2":173,"stroke":72,"style":73},"385.0","248.0","641.0",[114,176],{"points":177,"fill":117},"645.0,248.0 636.0,253.0 636.0,243.0",[57,179],{"x":180,"y":181,"width":182,"height":123,"rx":124,"fill":183,"stroke":184,"style":127},"439.0","219.0","152.1","#fee8f2","var(--accent,#f43f8f)",[75,186,189],{"x":167,"y":187,"style":188},"233.5","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","4. refused — held by A",[99,191],{"x1":110,"y1":192,"x2":112,"y2":192,"stroke":72,"style":73},"296.0",[114,194],{"points":195,"fill":117},"375.0,296.0 366.0,301.0 366.0,291.0",[57,197],{"x":198,"y":199,"width":200,"height":123,"rx":124,"fill":125,"stroke":126,"style":127},"141.9","267.0","206.1",[75,202,204],{"x":130,"y":203,"style":132},"281.5","5. exit — released automatically",[206,207,209],"h2",{"id":208},"prerequisites","Prerequisites",[10,211,212,213,216,217,220],{},"Only the standard library. ",[28,214,215],{},"single_instance.py"," is the lock helper this page builds; ",[28,218,219],{},"report_job.py"," is a stand-in report that holds the lock for a given number of seconds, so the examples can start overlapping runs on purpose.",[206,222,224],{"id":223},"step-1-take-an-operating-system-lock","Step 1 — Take an operating-system lock",[10,226,227,228,231,232,235,236,231,239,242],{},"The lock is an ordinary file, but what matters is not whether the file exists — it is the lock the operating system holds on it. ",[28,229,230],{},"fcntl.flock"," with ",[28,233,234],{},"LOCK_EX | LOCK_NB"," asks for an exclusive lock and fails immediately instead of waiting if another process has it; on Windows, ",[28,237,238],{},"msvcrt.locking",[28,240,241],{},"LK_NBLCK"," does the same. The helper wraps both in a context manager:",[244,245,250],"pre",{"className":246,"code":247,"language":248,"meta":249,"style":249},"language-python shiki shiki-themes github-light github-dark-high-contrast","# single_instance.py\nimport contextlib\nimport os\nimport time\nfrom pathlib import Path\n\nclass AlreadyRunning(RuntimeError):\n    pass\n\nif os.name == \"nt\":\n    import msvcrt\n    def _try_lock(fh):\n        try:\n            fh.seek(0); msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1); return True\n        except OSError:\n            return False\nelse:\n    import fcntl\n    def _try_lock(fh):\n        try:\n            fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB); return True\n        except BlockingIOError:\n            return False\n\n@contextlib.contextmanager\ndef single_instance(name, lock_dir=\"locks\", wait=0.0, poll=0.2):\n    Path(lock_dir).mkdir(exist_ok=True)\n    path = Path(lock_dir) \u002F f\"{name}.lock\"\n    fh = open(path, \"a+\")\n    deadline = time.monotonic() + wait\n    while not _try_lock(fh):\n        if time.monotonic() >= deadline:\n            try:\n                fh.seek(0)\n                holder = fh.read().strip() or \"unknown\"\n            except OSError:\n                holder = \"holder unknown\"\n            fh.close()\n            raise AlreadyRunning(f\"{name} is already running ({holder})\")\n        time.sleep(poll)\n    fh.seek(0); fh.truncate()\n    fh.write(f\"pid {os.getpid()} since {time.strftime('%H:%M:%S')}\"); fh.flush()\n    try:\n        yield path\n    finally:\n        fh.close()\n","python","",[28,251,252,260,271,279,287,301,308,328,334,339,358,367,380,388,416,427,436,444,452,461,468,492,502,509,514,520,556,574,607,626,643,655,669,677,687,704,714,724,730,765,771,782,821,829,838,846],{"__ignoreMap":249},[253,254,256],"span",{"class":99,"line":255},1,[253,257,259],{"class":258},"s-wDw","# single_instance.py\n",[253,261,263,267],{"class":99,"line":262},2,[253,264,266],{"class":265},"s-kum","import",[253,268,270],{"class":269},"skGVy"," contextlib\n",[253,272,274,276],{"class":99,"line":273},3,[253,275,266],{"class":265},[253,277,278],{"class":269}," os\n",[253,280,282,284],{"class":99,"line":281},4,[253,283,266],{"class":265},[253,285,286],{"class":269}," time\n",[253,288,290,293,296,298],{"class":99,"line":289},5,[253,291,292],{"class":265},"from",[253,294,295],{"class":269}," pathlib ",[253,297,266],{"class":265},[253,299,300],{"class":269}," Path\n",[253,302,304],{"class":99,"line":303},6,[253,305,307],{"emptyLinePlaceholder":306},true,"\n",[253,309,311,314,318,321,325],{"class":99,"line":310},7,[253,312,313],{"class":265},"class",[253,315,317],{"class":316},"sMTad"," AlreadyRunning",[253,319,320],{"class":269},"(",[253,322,324],{"class":323},"sP0c6","RuntimeError",[253,326,327],{"class":269},"):\n",[253,329,331],{"class":99,"line":330},8,[253,332,333],{"class":265},"    pass\n",[253,335,337],{"class":99,"line":336},9,[253,338,307],{"emptyLinePlaceholder":306},[253,340,342,345,348,351,355],{"class":99,"line":341},10,[253,343,344],{"class":265},"if",[253,346,347],{"class":269}," os.name ",[253,349,350],{"class":265},"==",[253,352,354],{"class":353},"srMev"," \"nt\"",[253,356,357],{"class":269},":\n",[253,359,361,364],{"class":99,"line":360},11,[253,362,363],{"class":265},"    import",[253,365,366],{"class":269}," msvcrt\n",[253,368,370,373,377],{"class":99,"line":369},12,[253,371,372],{"class":265},"    def",[253,374,376],{"class":375},"s_Opv"," _try_lock",[253,378,379],{"class":269},"(fh):\n",[253,381,383,386],{"class":99,"line":382},13,[253,384,385],{"class":265},"        try",[253,387,357],{"class":269},[253,389,391,394,396,399,401,404,407,410,413],{"class":99,"line":390},14,[253,392,393],{"class":269},"            fh.seek(",[253,395,60],{"class":323},[253,397,398],{"class":269},"); msvcrt.locking(fh.fileno(), msvcrt.",[253,400,241],{"class":323},[253,402,403],{"class":269},", ",[253,405,406],{"class":323},"1",[253,408,409],{"class":269},"); ",[253,411,412],{"class":265},"return",[253,414,415],{"class":323}," True\n",[253,417,419,422,425],{"class":99,"line":418},15,[253,420,421],{"class":265},"        except",[253,423,424],{"class":323}," OSError",[253,426,357],{"class":269},[253,428,430,433],{"class":99,"line":429},16,[253,431,432],{"class":265},"            return",[253,434,435],{"class":323}," False\n",[253,437,439,442],{"class":99,"line":438},17,[253,440,441],{"class":265},"else",[253,443,357],{"class":269},[253,445,447,449],{"class":99,"line":446},18,[253,448,363],{"class":265},[253,450,451],{"class":269}," fcntl\n",[253,453,455,457,459],{"class":99,"line":454},19,[253,456,372],{"class":265},[253,458,376],{"class":375},[253,460,379],{"class":269},[253,462,464,466],{"class":99,"line":463},20,[253,465,385],{"class":265},[253,467,357],{"class":269},[253,469,471,474,477,480,483,486,488,490],{"class":99,"line":470},21,[253,472,473],{"class":269},"            fcntl.flock(fh, fcntl.",[253,475,476],{"class":323},"LOCK_EX",[253,478,479],{"class":265}," |",[253,481,482],{"class":269}," fcntl.",[253,484,485],{"class":323},"LOCK_NB",[253,487,409],{"class":269},[253,489,412],{"class":265},[253,491,415],{"class":323},[253,493,495,497,500],{"class":99,"line":494},22,[253,496,421],{"class":265},[253,498,499],{"class":323}," BlockingIOError",[253,501,357],{"class":269},[253,503,505,507],{"class":99,"line":504},23,[253,506,432],{"class":265},[253,508,435],{"class":323},[253,510,512],{"class":99,"line":511},24,[253,513,307],{"emptyLinePlaceholder":306},[253,515,517],{"class":99,"line":516},25,[253,518,519],{"class":375},"@contextlib.contextmanager\n",[253,521,523,526,529,532,535,538,541,543,546,549,551,554],{"class":99,"line":522},26,[253,524,525],{"class":265},"def",[253,527,528],{"class":375}," single_instance",[253,530,531],{"class":269},"(name, lock_dir",[253,533,534],{"class":265},"=",[253,536,537],{"class":353},"\"locks\"",[253,539,540],{"class":269},", wait",[253,542,534],{"class":265},[253,544,545],{"class":323},"0.0",[253,547,548],{"class":269},", poll",[253,550,534],{"class":265},[253,552,553],{"class":323},"0.2",[253,555,327],{"class":269},[253,557,559,562,566,568,571],{"class":99,"line":558},27,[253,560,561],{"class":269},"    Path(lock_dir).mkdir(",[253,563,565],{"class":564},"sa561","exist_ok",[253,567,534],{"class":265},[253,569,570],{"class":323},"True",[253,572,573],{"class":269},")\n",[253,575,577,580,582,585,588,591,594,598,601,604],{"class":99,"line":576},28,[253,578,579],{"class":269},"    path ",[253,581,534],{"class":265},[253,583,584],{"class":269}," Path(lock_dir) ",[253,586,587],{"class":265},"\u002F",[253,589,590],{"class":265}," f",[253,592,593],{"class":353},"\"",[253,595,597],{"class":596},"sSjpA","{",[253,599,600],{"class":269},"name",[253,602,603],{"class":596},"}",[253,605,606],{"class":353},".lock\"\n",[253,608,610,613,615,618,621,624],{"class":99,"line":609},29,[253,611,612],{"class":269},"    fh ",[253,614,534],{"class":265},[253,616,617],{"class":323}," open",[253,619,620],{"class":269},"(path, ",[253,622,623],{"class":353},"\"a+\"",[253,625,573],{"class":269},[253,627,629,632,634,637,640],{"class":99,"line":628},30,[253,630,631],{"class":269},"    deadline ",[253,633,534],{"class":265},[253,635,636],{"class":269}," time.monotonic() ",[253,638,639],{"class":265},"+",[253,641,642],{"class":269}," wait\n",[253,644,646,649,652],{"class":99,"line":645},31,[253,647,648],{"class":265},"    while",[253,650,651],{"class":265}," not",[253,653,654],{"class":269}," _try_lock(fh):\n",[253,656,658,661,663,666],{"class":99,"line":657},32,[253,659,660],{"class":265},"        if",[253,662,636],{"class":269},[253,664,665],{"class":265},">=",[253,667,668],{"class":269}," deadline:\n",[253,670,672,675],{"class":99,"line":671},33,[253,673,674],{"class":265},"            try",[253,676,357],{"class":269},[253,678,680,683,685],{"class":99,"line":679},34,[253,681,682],{"class":269},"                fh.seek(",[253,684,60],{"class":323},[253,686,573],{"class":269},[253,688,690,693,695,698,701],{"class":99,"line":689},35,[253,691,692],{"class":269},"                holder ",[253,694,534],{"class":265},[253,696,697],{"class":269}," fh.read().strip() ",[253,699,700],{"class":265},"or",[253,702,703],{"class":353}," \"unknown\"\n",[253,705,707,710,712],{"class":99,"line":706},36,[253,708,709],{"class":265},"            except",[253,711,424],{"class":323},[253,713,357],{"class":269},[253,715,717,719,721],{"class":99,"line":716},37,[253,718,692],{"class":269},[253,720,534],{"class":265},[253,722,723],{"class":353}," \"holder unknown\"\n",[253,725,727],{"class":99,"line":726},38,[253,728,729],{"class":269},"            fh.close()\n",[253,731,733,736,739,742,744,746,748,750,753,755,758,760,763],{"class":99,"line":732},39,[253,734,735],{"class":265},"            raise",[253,737,738],{"class":269}," AlreadyRunning(",[253,740,741],{"class":265},"f",[253,743,593],{"class":353},[253,745,597],{"class":596},[253,747,600],{"class":269},[253,749,603],{"class":596},[253,751,752],{"class":353}," is already running (",[253,754,597],{"class":596},[253,756,757],{"class":269},"holder",[253,759,603],{"class":596},[253,761,762],{"class":353},")\"",[253,764,573],{"class":269},[253,766,768],{"class":99,"line":767},40,[253,769,770],{"class":269},"        time.sleep(poll)\n",[253,772,774,777,779],{"class":99,"line":773},41,[253,775,776],{"class":269},"    fh.seek(",[253,778,60],{"class":323},[253,780,781],{"class":269},"); fh.truncate()\n",[253,783,785,788,790,793,795,798,800,803,805,808,811,814,816,818],{"class":99,"line":784},42,[253,786,787],{"class":269},"    fh.write(",[253,789,741],{"class":265},[253,791,792],{"class":353},"\"pid ",[253,794,597],{"class":596},[253,796,797],{"class":269},"os.getpid()",[253,799,603],{"class":596},[253,801,802],{"class":353}," since ",[253,804,597],{"class":596},[253,806,807],{"class":269},"time.strftime(",[253,809,810],{"class":353},"'%H:%M:%S'",[253,812,813],{"class":269},")",[253,815,603],{"class":596},[253,817,593],{"class":353},[253,819,820],{"class":269},"); fh.flush()\n",[253,822,824,827],{"class":99,"line":823},43,[253,825,826],{"class":265},"    try",[253,828,357],{"class":269},[253,830,832,835],{"class":99,"line":831},44,[253,833,834],{"class":265},"        yield",[253,836,837],{"class":269}," path\n",[253,839,841,844],{"class":99,"line":840},45,[253,842,843],{"class":265},"    finally",[253,845,357],{"class":269},[253,847,849],{"class":99,"line":848},46,[253,850,851],{"class":269},"        fh.close()\n",[10,853,854,855,858,859,862,863,865],{},"A job uses it by wrapping its work in the ",[28,856,857],{},"with"," block and turning ",[28,860,861],{},"AlreadyRunning"," into a clean exit — this is ",[28,864,219],{},", the stand-in used below:",[244,867,869],{"className":246,"code":868,"language":248,"meta":249,"style":249},"# report_job.py\nimport sys, time\nfrom single_instance import single_instance, AlreadyRunning\n\nname, seconds, wait = sys.argv[1], float(sys.argv[2]), float(sys.argv[3]) if len(sys.argv) > 3 else 0\ntry:\n    with single_instance(name, wait=wait):\n        print(f\"{name}: running\", flush=True)\n        time.sleep(seconds)\n        print(f\"{name}: finished\", flush=True)\nexcept AlreadyRunning as exc:\n    print(f\"skipped: {exc}\", flush=True)\n    sys.exit(75)\n",[28,870,871,876,883,895,899,956,963,979,1010,1015,1044,1058,1089],{"__ignoreMap":249},[253,872,873],{"class":99,"line":255},[253,874,875],{"class":258},"# report_job.py\n",[253,877,878,880],{"class":99,"line":262},[253,879,266],{"class":265},[253,881,882],{"class":269}," sys, time\n",[253,884,885,887,890,892],{"class":99,"line":273},[253,886,292],{"class":265},[253,888,889],{"class":269}," single_instance ",[253,891,266],{"class":265},[253,893,894],{"class":269}," single_instance, AlreadyRunning\n",[253,896,897],{"class":99,"line":281},[253,898,307],{"emptyLinePlaceholder":306},[253,900,901,904,906,909,911,914,917,920,923,926,928,930,933,936,938,941,944,947,950,953],{"class":99,"line":289},[253,902,903],{"class":269},"name, seconds, wait ",[253,905,534],{"class":265},[253,907,908],{"class":269}," sys.argv[",[253,910,406],{"class":323},[253,912,913],{"class":269},"], ",[253,915,916],{"class":323},"float",[253,918,919],{"class":269},"(sys.argv[",[253,921,922],{"class":323},"2",[253,924,925],{"class":269},"]), ",[253,927,916],{"class":323},[253,929,919],{"class":269},[253,931,932],{"class":323},"3",[253,934,935],{"class":269},"]) ",[253,937,344],{"class":265},[253,939,940],{"class":323}," len",[253,942,943],{"class":269},"(sys.argv) ",[253,945,946],{"class":265},">",[253,948,949],{"class":323}," 3",[253,951,952],{"class":265}," else",[253,954,955],{"class":323}," 0\n",[253,957,958,961],{"class":99,"line":303},[253,959,960],{"class":265},"try",[253,962,357],{"class":269},[253,964,965,968,971,974,976],{"class":99,"line":310},[253,966,967],{"class":265},"    with",[253,969,970],{"class":269}," single_instance(name, ",[253,972,973],{"class":564},"wait",[253,975,534],{"class":265},[253,977,978],{"class":269},"wait):\n",[253,980,981,984,986,988,990,992,994,996,999,1001,1004,1006,1008],{"class":99,"line":330},[253,982,983],{"class":323},"        print",[253,985,320],{"class":269},[253,987,741],{"class":265},[253,989,593],{"class":353},[253,991,597],{"class":596},[253,993,600],{"class":269},[253,995,603],{"class":596},[253,997,998],{"class":353},": running\"",[253,1000,403],{"class":269},[253,1002,1003],{"class":564},"flush",[253,1005,534],{"class":265},[253,1007,570],{"class":323},[253,1009,573],{"class":269},[253,1011,1012],{"class":99,"line":336},[253,1013,1014],{"class":269},"        time.sleep(seconds)\n",[253,1016,1017,1019,1021,1023,1025,1027,1029,1031,1034,1036,1038,1040,1042],{"class":99,"line":341},[253,1018,983],{"class":323},[253,1020,320],{"class":269},[253,1022,741],{"class":265},[253,1024,593],{"class":353},[253,1026,597],{"class":596},[253,1028,600],{"class":269},[253,1030,603],{"class":596},[253,1032,1033],{"class":353},": finished\"",[253,1035,403],{"class":269},[253,1037,1003],{"class":564},[253,1039,534],{"class":265},[253,1041,570],{"class":323},[253,1043,573],{"class":269},[253,1045,1046,1049,1052,1055],{"class":99,"line":360},[253,1047,1048],{"class":265},"except",[253,1050,1051],{"class":269}," AlreadyRunning ",[253,1053,1054],{"class":265},"as",[253,1056,1057],{"class":269}," exc:\n",[253,1059,1060,1063,1065,1067,1070,1072,1075,1077,1079,1081,1083,1085,1087],{"class":99,"line":369},[253,1061,1062],{"class":323},"    print",[253,1064,320],{"class":269},[253,1066,741],{"class":265},[253,1068,1069],{"class":353},"\"skipped: ",[253,1071,597],{"class":596},[253,1073,1074],{"class":269},"exc",[253,1076,603],{"class":596},[253,1078,593],{"class":353},[253,1080,403],{"class":269},[253,1082,1003],{"class":564},[253,1084,534],{"class":265},[253,1086,570],{"class":323},[253,1088,573],{"class":269},[253,1090,1091,1094,1097],{"class":99,"line":382},[253,1092,1093],{"class":269},"    sys.exit(",[253,1095,1096],{"class":323},"75",[253,1098,573],{"class":269},[10,1100,1101,1102,1105,1106,1110,1111,1113],{},"Three details make it robust. The file is opened in append mode (",[28,1103,1104],{},"a+","), so opening it never truncates information another run wrote. Once the lock is acquired, the run writes its PID and start time into the file — not to ",[1107,1108,1109],"em",{},"be"," the lock, but so a refused run can say who holds it. (On Windows the locked byte cannot be read by another process, so the helper falls back to \"holder unknown\" there.) And the lock lives only as long as the open file handle: when the ",[28,1112,857],{}," block ends, the process exits or the process crashes, the operating system releases it. There is nothing to clean up and nothing to go stale.",[206,1115,1117],{"id":1116},"step-2-see-a-second-run-refused","Step 2 — See a second run refused",[10,1119,1120],{},"Start one run that takes two seconds, then a second run of the same report straight away:",[244,1122,1124],{"className":246,"code":1123,"language":248,"meta":249,"style":249},"import subprocess\nimport sys\nimport time\n\nfirst = subprocess.Popen([sys.executable, \"report_job.py\", \"daily_sales\", \"2\"], stdout=subprocess.PIPE, text=True)\ntime.sleep(0.5)\nsecond = subprocess.run([sys.executable, \"report_job.py\", \"daily_sales\", \"0\"], capture_output=True, text=True)\nprint(\"second run:\", second.stdout.strip(), \"| exit code\", second.returncode)\nprint(\"first run: \", first.communicate()[0].strip().replace(\"\\n\", \" \u002F \"), \"| exit code\", first.returncode)\n\nthird = subprocess.run([sys.executable, \"report_job.py\", \"daily_sales\", \"0\"], capture_output=True, text=True)\nprint(\"third run: \", third.stdout.strip().replace(\"\\n\", \" \u002F \"), \"| exit code\", third.returncode)\n",[28,1125,1126,1133,1140,1146,1150,1196,1206,1246,1265,1302,1306,1343],{"__ignoreMap":249},[253,1127,1128,1130],{"class":99,"line":255},[253,1129,266],{"class":265},[253,1131,1132],{"class":269}," subprocess\n",[253,1134,1135,1137],{"class":99,"line":262},[253,1136,266],{"class":265},[253,1138,1139],{"class":269}," sys\n",[253,1141,1142,1144],{"class":99,"line":273},[253,1143,266],{"class":265},[253,1145,286],{"class":269},[253,1147,1148],{"class":99,"line":281},[253,1149,307],{"emptyLinePlaceholder":306},[253,1151,1152,1155,1157,1160,1163,1165,1168,1170,1173,1175,1178,1180,1183,1186,1188,1190,1192,1194],{"class":99,"line":289},[253,1153,1154],{"class":269},"first ",[253,1156,534],{"class":265},[253,1158,1159],{"class":269}," subprocess.Popen([sys.executable, ",[253,1161,1162],{"class":353},"\"report_job.py\"",[253,1164,403],{"class":269},[253,1166,1167],{"class":353},"\"daily_sales\"",[253,1169,403],{"class":269},[253,1171,1172],{"class":353},"\"2\"",[253,1174,913],{"class":269},[253,1176,1177],{"class":564},"stdout",[253,1179,534],{"class":265},[253,1181,1182],{"class":269},"subprocess.",[253,1184,1185],{"class":323},"PIPE",[253,1187,403],{"class":269},[253,1189,75],{"class":564},[253,1191,534],{"class":265},[253,1193,570],{"class":323},[253,1195,573],{"class":269},[253,1197,1198,1201,1204],{"class":99,"line":303},[253,1199,1200],{"class":269},"time.sleep(",[253,1202,1203],{"class":323},"0.5",[253,1205,573],{"class":269},[253,1207,1208,1211,1213,1216,1218,1220,1222,1224,1227,1229,1232,1234,1236,1238,1240,1242,1244],{"class":99,"line":310},[253,1209,1210],{"class":269},"second ",[253,1212,534],{"class":265},[253,1214,1215],{"class":269}," subprocess.run([sys.executable, ",[253,1217,1162],{"class":353},[253,1219,403],{"class":269},[253,1221,1167],{"class":353},[253,1223,403],{"class":269},[253,1225,1226],{"class":353},"\"0\"",[253,1228,913],{"class":269},[253,1230,1231],{"class":564},"capture_output",[253,1233,534],{"class":265},[253,1235,570],{"class":323},[253,1237,403],{"class":269},[253,1239,75],{"class":564},[253,1241,534],{"class":265},[253,1243,570],{"class":323},[253,1245,573],{"class":269},[253,1247,1248,1251,1253,1256,1259,1262],{"class":99,"line":330},[253,1249,1250],{"class":323},"print",[253,1252,320],{"class":269},[253,1254,1255],{"class":353},"\"second run:\"",[253,1257,1258],{"class":269},", second.stdout.strip(), ",[253,1260,1261],{"class":353},"\"| exit code\"",[253,1263,1264],{"class":269},", second.returncode)\n",[253,1266,1267,1269,1271,1274,1277,1279,1282,1284,1287,1289,1291,1294,1297,1299],{"class":99,"line":336},[253,1268,1250],{"class":323},[253,1270,320],{"class":269},[253,1272,1273],{"class":353},"\"first run: \"",[253,1275,1276],{"class":269},", first.communicate()[",[253,1278,60],{"class":323},[253,1280,1281],{"class":269},"].strip().replace(",[253,1283,593],{"class":353},[253,1285,1286],{"class":596},"\\n",[253,1288,593],{"class":353},[253,1290,403],{"class":269},[253,1292,1293],{"class":353},"\" \u002F \"",[253,1295,1296],{"class":269},"), ",[253,1298,1261],{"class":353},[253,1300,1301],{"class":269},", first.returncode)\n",[253,1303,1304],{"class":99,"line":341},[253,1305,307],{"emptyLinePlaceholder":306},[253,1307,1308,1311,1313,1315,1317,1319,1321,1323,1325,1327,1329,1331,1333,1335,1337,1339,1341],{"class":99,"line":360},[253,1309,1310],{"class":269},"third ",[253,1312,534],{"class":265},[253,1314,1215],{"class":269},[253,1316,1162],{"class":353},[253,1318,403],{"class":269},[253,1320,1167],{"class":353},[253,1322,403],{"class":269},[253,1324,1226],{"class":353},[253,1326,913],{"class":269},[253,1328,1231],{"class":564},[253,1330,534],{"class":265},[253,1332,570],{"class":323},[253,1334,403],{"class":269},[253,1336,75],{"class":564},[253,1338,534],{"class":265},[253,1340,570],{"class":323},[253,1342,573],{"class":269},[253,1344,1345,1347,1349,1352,1355,1357,1359,1361,1363,1365,1367,1369],{"class":99,"line":369},[253,1346,1250],{"class":323},[253,1348,320],{"class":269},[253,1350,1351],{"class":353},"\"third run: \"",[253,1353,1354],{"class":269},", third.stdout.strip().replace(",[253,1356,593],{"class":353},[253,1358,1286],{"class":596},[253,1360,593],{"class":353},[253,1362,403],{"class":269},[253,1364,1293],{"class":353},[253,1366,1296],{"class":269},[253,1368,1261],{"class":353},[253,1370,1371],{"class":269},", third.returncode)\n",[10,1373,1374,1375,1378],{},"The second run is refused and names the process holding the lock. Once the first finishes, the third run gets the lock normally — no leftover file had to be deleted. The skipped run exits with code 75, which by convention (",[28,1376,1377],{},"EX_TEMPFAIL",") means \"temporary condition, try later\"; using a distinct code lets monitoring tell a skipped run from a failed one. Whatever code you choose, log the skip: a report that silently never runs because its predecessor hangs forever is its own kind of outage.",[206,1380,1382],{"id":1381},"step-3-lock-per-report-not-per-machine","Step 3 — Lock per report, not per machine",[10,1384,1385],{},"A single global lock makes every job wait for every other. Name the lock after the report — and the period, if runs for different periods may safely overlap — so unrelated jobs run in parallel and only true duplicates are stopped:",[244,1387,1389],{"className":246,"code":1388,"language":248,"meta":249,"style":249},"a = subprocess.Popen([sys.executable, \"report_job.py\", \"daily_sales\", \"1\"], stdout=subprocess.PIPE, text=True)\nb = subprocess.Popen([sys.executable, \"report_job.py\", \"stock_levels\", \"1\"], stdout=subprocess.PIPE, text=True)\nprint(a.communicate()[0].split(\"\\n\")[0], \"|\", b.communicate()[0].split(\"\\n\")[0])\nprint(\"both exit codes:\", a.returncode, b.returncode)\n",[28,1390,1391,1431,1471,1519],{"__ignoreMap":249},[253,1392,1393,1396,1398,1400,1402,1404,1406,1408,1411,1413,1415,1417,1419,1421,1423,1425,1427,1429],{"class":99,"line":255},[253,1394,1395],{"class":269},"a ",[253,1397,534],{"class":265},[253,1399,1159],{"class":269},[253,1401,1162],{"class":353},[253,1403,403],{"class":269},[253,1405,1167],{"class":353},[253,1407,403],{"class":269},[253,1409,1410],{"class":353},"\"1\"",[253,1412,913],{"class":269},[253,1414,1177],{"class":564},[253,1416,534],{"class":265},[253,1418,1182],{"class":269},[253,1420,1185],{"class":323},[253,1422,403],{"class":269},[253,1424,75],{"class":564},[253,1426,534],{"class":265},[253,1428,570],{"class":323},[253,1430,573],{"class":269},[253,1432,1433,1436,1438,1440,1442,1444,1447,1449,1451,1453,1455,1457,1459,1461,1463,1465,1467,1469],{"class":99,"line":262},[253,1434,1435],{"class":269},"b ",[253,1437,534],{"class":265},[253,1439,1159],{"class":269},[253,1441,1162],{"class":353},[253,1443,403],{"class":269},[253,1445,1446],{"class":353},"\"stock_levels\"",[253,1448,403],{"class":269},[253,1450,1410],{"class":353},[253,1452,913],{"class":269},[253,1454,1177],{"class":564},[253,1456,534],{"class":265},[253,1458,1182],{"class":269},[253,1460,1185],{"class":323},[253,1462,403],{"class":269},[253,1464,75],{"class":564},[253,1466,534],{"class":265},[253,1468,570],{"class":323},[253,1470,573],{"class":269},[253,1472,1473,1475,1478,1480,1483,1485,1487,1489,1492,1494,1496,1499,1502,1504,1506,1508,1510,1512,1514,1516],{"class":99,"line":273},[253,1474,1250],{"class":323},[253,1476,1477],{"class":269},"(a.communicate()[",[253,1479,60],{"class":323},[253,1481,1482],{"class":269},"].split(",[253,1484,593],{"class":353},[253,1486,1286],{"class":596},[253,1488,593],{"class":353},[253,1490,1491],{"class":269},")[",[253,1493,60],{"class":323},[253,1495,913],{"class":269},[253,1497,1498],{"class":353},"\"|\"",[253,1500,1501],{"class":269},", b.communicate()[",[253,1503,60],{"class":323},[253,1505,1482],{"class":269},[253,1507,593],{"class":353},[253,1509,1286],{"class":596},[253,1511,593],{"class":353},[253,1513,1491],{"class":269},[253,1515,60],{"class":323},[253,1517,1518],{"class":269},"])\n",[253,1520,1521,1523,1525,1528],{"class":99,"line":281},[253,1522,1250],{"class":323},[253,1524,320],{"class":269},[253,1526,1527],{"class":353},"\"both exit codes:\"",[253,1529,1530],{"class":269},", a.returncode, b.returncode)\n",[10,1532,1533,1534,1537],{},"The sales and stock reports each hold their own lock and run side by side. A name such as ",[28,1535,1536],{},"f\"month_end_{period}\""," lets a backfill for March run while the live September job is running, but stops two September runs colliding.",[38,1539,47,1544,47,1547,47,1550,47,1553,47,1558,47,1562,47,1567,47,1570,47,1574,47,1578,47,1584,47,1586,47,1591,47,1593,47,1596,47,1599,47,1603,47,1605,47,1609,47,1611,47,1614,47,1617,47,1621,47,1623,47,1627,47,1629],{"viewBox":1540,"role":41,"ariaLabelledBy":1541,"xmlns":45,"style":46},"4 0 752 174",[1542,1543],"lock-scope-t","lock-scope-d",[49,1545,1546],{"id":1542},"Choosing what one lock protects",[53,1548,1549],{"id":1543},"A table of lock scopes. One lock per machine serialises every job, so unrelated reports wait for each other. One lock per report stops duplicate runs of that report and lets other reports run. One lock per report and period also lets backfills for other periods run alongside the live job.",[57,1551],{"x":59,"y":60,"width":61,"height":1552,"fill":63},"174",[57,1554],{"x":66,"y":67,"width":1555,"height":1556,"rx":60,"fill":63,"stroke":85,"style":1557},"164.0","40.0","stroke-width:1px",[57,1559],{"x":1560,"y":67,"width":1561,"height":1556,"rx":60,"fill":71,"stroke":72,"style":127},"184.0","278.0",[75,1563,1566],{"x":1564,"y":1565,"style":79},"323.0","41.0","Stops",[57,1568],{"x":1569,"y":67,"width":1561,"height":1556,"rx":60,"fill":71,"stroke":72,"style":127},"462.0",[75,1571,1573],{"x":1572,"y":1565,"style":79},"601.0","Still allows",[57,1575],{"x":66,"y":1576,"width":1555,"height":1577,"rx":60,"fill":84,"stroke":85,"style":1557},"56.0","34.0",[75,1579,1583],{"x":1580,"y":1581,"style":1582},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","One per machine",[57,1585],{"x":1560,"y":1576,"width":1561,"height":1577,"rx":60,"fill":63,"stroke":85,"style":1557},[75,1587,1590],{"x":1564,"y":1588,"style":1589},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","everything overlapping",[57,1592],{"x":1569,"y":1576,"width":1561,"height":1577,"rx":60,"fill":63,"stroke":85,"style":1557},[75,1594,1595],{"x":1572,"y":1588,"style":1589},"nothing in parallel",[57,1597],{"x":66,"y":1598,"width":1555,"height":1577,"rx":60,"fill":84,"stroke":85,"style":1557},"90.0",[75,1600,1602],{"x":1580,"y":1601,"style":1582},"112.0","One per report",[57,1604],{"x":1560,"y":1598,"width":1561,"height":1577,"rx":60,"fill":63,"stroke":85,"style":1557},[75,1606,1608],{"x":1564,"y":1607,"style":1589},"111.0","duplicate runs of it",[57,1610],{"x":1569,"y":1598,"width":1561,"height":1577,"rx":60,"fill":63,"stroke":85,"style":1557},[75,1612,1613],{"x":1572,"y":1607,"style":1589},"other reports",[57,1615],{"x":66,"y":1616,"width":1555,"height":1577,"rx":60,"fill":84,"stroke":85,"style":1557},"124.0",[75,1618,1620],{"x":1580,"y":1619,"style":1582},"146.0","Per report and period",[57,1622],{"x":1560,"y":1616,"width":1561,"height":1577,"rx":60,"fill":63,"stroke":85,"style":1557},[75,1624,1626],{"x":1564,"y":1625,"style":1589},"145.0","same report and period",[57,1628],{"x":1569,"y":1616,"width":1561,"height":1577,"rx":60,"fill":63,"stroke":85,"style":1557},[75,1630,1631],{"x":1572,"y":1625,"style":1589},"backfills of other periods",[206,1633,1635],{"id":1634},"step-4-wait-instead-of-skipping-when-the-work-must-not-be-lost","Step 4 — Wait instead of skipping, when the work must not be lost",[10,1637,1638],{},"Skipping is right for a scheduled report: the next scheduled run picks up the work. When a run is triggered by an event — a file arriving, a user pressing \"generate\" — skipping would lose it. Wait for the lock instead, with a timeout so a hung predecessor cannot block forever:",[244,1640,1642],{"className":246,"code":1641,"language":248,"meta":249,"style":249},"first = subprocess.Popen([sys.executable, \"report_job.py\", \"on_upload\", \"1.5\"], stdout=subprocess.PIPE, text=True)\ntime.sleep(0.3)\nstart = time.monotonic()\nwaiter = subprocess.run([sys.executable, \"report_job.py\", \"on_upload\", \"0\", \"5\"], capture_output=True, text=True)\nfirst.wait()\nprint(f\"waiter: {waiter.stdout.strip().replace(chr(10), ' \u002F ')} after {time.monotonic() - start:.1f} s, exit {waiter.returncode}\")\n",[28,1643,1644,1684,1693,1703,1745,1750],{"__ignoreMap":249},[253,1645,1646,1648,1650,1652,1654,1656,1659,1661,1664,1666,1668,1670,1672,1674,1676,1678,1680,1682],{"class":99,"line":255},[253,1647,1154],{"class":269},[253,1649,534],{"class":265},[253,1651,1159],{"class":269},[253,1653,1162],{"class":353},[253,1655,403],{"class":269},[253,1657,1658],{"class":353},"\"on_upload\"",[253,1660,403],{"class":269},[253,1662,1663],{"class":353},"\"1.5\"",[253,1665,913],{"class":269},[253,1667,1177],{"class":564},[253,1669,534],{"class":265},[253,1671,1182],{"class":269},[253,1673,1185],{"class":323},[253,1675,403],{"class":269},[253,1677,75],{"class":564},[253,1679,534],{"class":265},[253,1681,570],{"class":323},[253,1683,573],{"class":269},[253,1685,1686,1688,1691],{"class":99,"line":262},[253,1687,1200],{"class":269},[253,1689,1690],{"class":323},"0.3",[253,1692,573],{"class":269},[253,1694,1695,1698,1700],{"class":99,"line":273},[253,1696,1697],{"class":269},"start ",[253,1699,534],{"class":265},[253,1701,1702],{"class":269}," time.monotonic()\n",[253,1704,1705,1708,1710,1712,1714,1716,1718,1720,1722,1724,1727,1729,1731,1733,1735,1737,1739,1741,1743],{"class":99,"line":281},[253,1706,1707],{"class":269},"waiter ",[253,1709,534],{"class":265},[253,1711,1215],{"class":269},[253,1713,1162],{"class":353},[253,1715,403],{"class":269},[253,1717,1658],{"class":353},[253,1719,403],{"class":269},[253,1721,1226],{"class":353},[253,1723,403],{"class":269},[253,1725,1726],{"class":353},"\"5\"",[253,1728,913],{"class":269},[253,1730,1231],{"class":564},[253,1732,534],{"class":265},[253,1734,570],{"class":323},[253,1736,403],{"class":269},[253,1738,75],{"class":564},[253,1740,534],{"class":265},[253,1742,570],{"class":323},[253,1744,573],{"class":269},[253,1746,1747],{"class":99,"line":289},[253,1748,1749],{"class":269},"first.wait()\n",[253,1751,1752,1754,1756,1758,1761,1763,1766,1769,1771,1773,1775,1778,1780,1782,1785,1787,1790,1793,1796,1799,1801,1804,1806,1809,1811,1813],{"class":99,"line":303},[253,1753,1250],{"class":323},[253,1755,320],{"class":269},[253,1757,741],{"class":265},[253,1759,1760],{"class":353},"\"waiter: ",[253,1762,597],{"class":596},[253,1764,1765],{"class":269},"waiter.stdout.strip().replace(",[253,1767,1768],{"class":323},"chr",[253,1770,320],{"class":269},[253,1772,124],{"class":323},[253,1774,1296],{"class":269},[253,1776,1777],{"class":353},"' \u002F '",[253,1779,813],{"class":269},[253,1781,603],{"class":596},[253,1783,1784],{"class":353}," after ",[253,1786,597],{"class":596},[253,1788,1789],{"class":269},"time.monotonic() ",[253,1791,1792],{"class":265},"-",[253,1794,1795],{"class":269}," start",[253,1797,1798],{"class":265},":.1f",[253,1800,603],{"class":596},[253,1802,1803],{"class":353}," s, exit ",[253,1805,597],{"class":596},[253,1807,1808],{"class":269},"waiter.returncode",[253,1810,603],{"class":596},[253,1812,593],{"class":353},[253,1814,573],{"class":269},[10,1816,1817,1818,1820],{},"The waiting run polls until the first releases the lock, about a second later, and then runs. If the timeout expires first, it raises ",[28,1819,861],{}," just as the skipping version does — at which point alerting a person is better than waiting longer.",[206,1822,1824],{"id":1823},"why-not-a-pid-file","Why not a PID file?",[10,1826,1827],{},"The common home-made alternative is to write a file containing the process ID at start, delete it at the end, and refuse to start if it exists. It fails in exactly the situations a lock is for. A crashed or killed run never deletes its file, so every later run refuses to start until someone notices — often days later. Checking whether the recorded PID is still alive helps, but process IDs are reused, so an unrelated process can make a dead run look alive. An operating-system lock has neither problem, because it belongs to the running process and vanishes with it.",[38,1829,47,1834,47,1837,47,1840,47,1843,47,1848,47,1859,47,1864,47,1869,47,1874,47,1881,47,1885,47,1890,47,1894,47,1899,47,1901,47,1908,47,1913,47,1917,47,1921,47,1926,47,1929,47,1934,47,1937],{"viewBox":1830,"role":41,"ariaLabelledBy":1831,"xmlns":45,"style":46},"4 0 752 207",[1832,1833],"lock-pid-t","lock-pid-d",[49,1835,1836],{"id":1832},"A PID file versus an operating-system lock after a crash",[53,1838,1839],{"id":1833},"Two panels. A PID file is left behind when a run crashes, so every later run refuses to start until someone deletes it, and a reused process ID can make a dead run look alive. An operating-system lock is released the moment the process ends, crash or not, so the next run starts normally.",[57,1841],{"x":59,"y":60,"width":61,"height":1842,"fill":63},"207",[57,1844],{"x":66,"y":67,"width":1845,"height":1846,"rx":1847,"fill":183,"stroke":184,"style":73},"351.0","175.0","14",[1849,1850,1852],"g",{"transform":1851},"translate(182.5,29.0) scale(1.083)",[1853,1854],"path",{"d":1855,"fill":1856,"stroke":1857,"style":1858},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","none","var(--accent-ink,#be185d)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[75,1860,1863],{"x":1861,"y":1581,"style":1862},"195.5","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","PID file",[99,1865],{"x1":1866,"y1":1867,"x2":1868,"y2":1867,"stroke":184,"style":1557},"36.0","91.0","355.0",[75,1870,1873],{"x":1871,"y":1607,"style":1872},"60.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","left behind by a crash",[1849,1875,1877],{"transform":1876},"translate(38.5,99.5) scale(0.625)",[1853,1878],{"d":1879,"fill":1856,"stroke":184,"style":1880},"M6 6l12 12 M18 6L6 18","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[75,1882,1884],{"x":1871,"y":1883,"style":1872},"137.0","blocks every later run",[1849,1886,1888],{"transform":1887},"translate(38.5,125.5) scale(0.625)",[1853,1889],{"d":1879,"fill":1856,"stroke":184,"style":1880},[75,1891,1893],{"x":1871,"y":1892,"style":1872},"163.0","reused PIDs look alive",[1849,1895,1897],{"transform":1896},"translate(38.5,151.5) scale(0.625)",[1853,1898],{"d":1879,"fill":1856,"stroke":184,"style":1880},[57,1900],{"x":156,"y":67,"width":1845,"height":1846,"rx":1847,"fill":125,"stroke":126,"style":73},[1849,1902,1904],{"transform":1903},"translate(551.5,29.0) scale(1.083)",[1853,1905],{"d":1906,"fill":1856,"stroke":1907,"style":1858},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[75,1909,1912],{"x":1910,"y":1581,"style":1911},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","OS lock on a file",[99,1914],{"x1":1915,"y1":1867,"x2":1916,"y2":1867,"stroke":126,"style":1557},"405.0","724.0",[75,1918,1920],{"x":1919,"y":1607,"style":1872},"429.0","released when the process ends",[1849,1922,1924],{"transform":1923},"translate(407.5,99.5) scale(0.625)",[1853,1925],{"d":1906,"fill":1856,"stroke":126,"style":1880},[75,1927,1928],{"x":1919,"y":1883,"style":1872},"next run starts normally",[1849,1930,1932],{"transform":1931},"translate(407.5,125.5) scale(0.625)",[1853,1933],{"d":1906,"fill":1856,"stroke":126,"style":1880},[75,1935,1936],{"x":1919,"y":1892,"style":1872},"nothing to clean up",[1849,1938,1940],{"transform":1939},"translate(407.5,151.5) scale(0.625)",[1853,1941],{"d":1906,"fill":1856,"stroke":126,"style":1880},[206,1943,1945],{"id":1944},"when-runs-start-on-more-than-one-machine","When runs start on more than one machine",[10,1947,1948,1949,1952,1953,1956],{},"File locks protect runs on one machine. If a job can start on several — a pool of workers, a scheduled container that might be scheduled twice — the lock has to live somewhere they all share. Locks on network file shares are unreliable: whether they work depends on the file system and its settings. Use a service built for it instead. With PostgreSQL, ",[28,1950,1951],{},"SELECT pg_try_advisory_lock(hashtext('daily_sales'))"," returns true for exactly one session and is released when that session ends; Redis offers ",[28,1954,1955],{},"SET key value NX PX 600000"," for a lock that also expires by itself. Both follow the same pattern as the file lock: try, and skip or wait if someone else has it.",[206,1958,1960],{"id":1959},"common-pitfalls","Common pitfalls",[1962,1963,1964,1971,1977,1983,1989,1995],"ul",{},[1965,1966,1967,1970],"li",{},[14,1968,1969],{},"Checking the file exists instead of locking it."," Existence is not a lock; take an OS lock on the file.",[1965,1972,1973,1976],{},[14,1974,1975],{},"PID files."," They go stale after a crash; use a lock the OS releases.",[1965,1978,1979,1982],{},[14,1980,1981],{},"One lock for everything."," Name locks per report (and period) so unrelated jobs are not blocked.",[1965,1984,1985,1988],{},[14,1986,1987],{},"Silent skips."," Log every skipped run and alert when runs keep being skipped.",[1965,1990,1991,1994],{},[14,1992,1993],{},"Waiting forever."," Always give a waiting run a timeout.",[1965,1996,1997,2000],{},[14,1998,1999],{},"Locks on network shares."," Use a database or Redis lock across machines.",[206,2002,2004],{"id":2003},"performance-notes","Performance notes",[10,2006,2007,2008,2012],{},"Taking and releasing a file lock costs microseconds; the only real cost of this pattern is the run you skip. If runs are regularly skipped because the previous one is still going, the schedule is too tight or the job too slow — ",[19,2009,2011],{"href":2010},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fskip-a-report-when-the-input-file-has-not-changed\u002F","Skip a Report When the Input File Has Not Changed"," removes unnecessary runs altogether.",[206,2014,2016],{"id":2015},"conclusion","Conclusion",[10,2018,2019,2020,2022,2023,2025],{},"Make report jobs single-instance by taking an exclusive, non-blocking operating-system lock — ",[28,2021,230],{}," or ",[28,2024,238],{}," — on a lock file named after the report, and write the holder's PID into it so refused runs can say who is running. Skip with a distinct exit code for scheduled runs, wait with a timeout for event-driven ones, avoid PID files that go stale, and move the lock to a database or Redis when runs can start on more than one machine.",[206,2027,2029],{"id":2028},"frequently-asked-questions","Frequently asked questions",[10,2031,2032,2035],{},[14,2033,2034],{},"How do I stop a Python script from running twice at the same time?","\nOpen a lock file and take an exclusive, non-blocking lock on it with fcntl.flock on Linux and macOS or msvcrt.locking on Windows. If the lock is already held, another run is in progress, so exit. The operating system releases the lock when the process ends, even if it crashes.",[10,2037,2038,2041],{},[14,2039,2040],{},"Why not just create a PID file and check whether it exists?","\nA PID file outlives a crashed run, so the next run sees it and refuses to start until someone deletes it — and a recycled PID can make a dead run look alive. An operating-system lock disappears with the process that held it, so it cannot go stale.",[10,2043,2044,2047],{},[14,2045,2046],{},"Should the second run wait or exit?","\nFor a report that runs on a schedule, exit: the next scheduled run will pick up the work. Wait, with a timeout, when the job is triggered by an event that must not be lost, such as a file arriving.",[10,2049,2050,2053],{},[14,2051,2052],{},"Does a lock file work when jobs run on several servers?","\nNot reliably. File locks on network shares depend on the file system and its configuration. When runs can start on different machines, use a lock in a shared service instead, such as a PostgreSQL advisory lock or a lock key in Redis.",[206,2055,2057],{"id":2056},"related","Related",[1962,2059,2060,2066,2074,2081],{},[1965,2061,2062,2063,2065],{},"Up one level: ",[19,2064,22],{"href":21}," — making unattended jobs fail well.",[1965,2067,2068,2070,2071,2073],{},[19,2069,35],{"href":34}," — the ",[28,2072,30],{}," wrapper.",[1965,2075,2076,2080],{},[19,2077,2079],{"href":2078},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python\u002F","Handle Permission Denied When Writing Excel in Python"," — the file already open elsewhere.",[1965,2082,2083,2087],{},[19,2084,2086],{"href":2085},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fbackfill-excel-reports-for-past-dates\u002F","Backfill Excel Reports for Past Dates"," — runs that overlap on purpose.",[2089,2090,2091],"style",{},"html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}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);}",{"title":249,"searchDepth":262,"depth":262,"links":2093},[2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105],{"id":208,"depth":262,"text":209},{"id":223,"depth":262,"text":224},{"id":1116,"depth":262,"text":1117},{"id":1381,"depth":262,"text":1382},{"id":1634,"depth":262,"text":1635},{"id":1823,"depth":262,"text":1824},{"id":1944,"depth":262,"text":1945},{"id":1959,"depth":262,"text":1960},{"id":2003,"depth":262,"text":2004},{"id":2015,"depth":262,"text":2016},{"id":2028,"depth":262,"text":2029},{"id":2056,"depth":262,"text":2057},"2026-09-25","Stop a scheduled report from starting while the previous run is still writing the workbook. Take an operating-system lock at the start of the job, skip or wait when another run holds it, record who holds it, use one lock per report so unrelated jobs still run in parallel, and choose a different lock when jobs run on more than one machine.","md",[2110,2112,2114,2116],{"q":2034,"a":2111},"Open a lock file and take an exclusive, non-blocking lock on it with fcntl.flock on Linux and macOS or msvcrt.locking on Windows. If the lock is already held, another run is in progress, so exit. The operating system releases the lock when the process ends, even if it crashes.",{"q":2040,"a":2113},"A PID file outlives a crashed run, so the next run sees it and refuses to start until someone deletes it — and a recycled PID can make a dead run look alive. An operating-system lock disappears with the process that held it, so it cannot go stale.",{"q":2046,"a":2115},"For a report that runs on a schedule, exit: the next scheduled run will pick up the work. Wait, with a timeout, when the job is triggered by an event that must not be lost, such as a file arriving.",{"q":2052,"a":2117},"Not reliably. File locks on network shares depend on the file system and its configuration. When runs can start on different machines, use a lock in a shared service instead, such as a PostgreSQL advisory lock or a lock key in Redis.","prevent overlapping runs of an excel report job",null,{"breadcrumb":2121},[2122,2124,2127],{"name":2123,"item":587},"Home",{"name":2125,"item":2126},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job",{"title":2130,"description":2131},"Prevent Overlapping Python Report Runs with a Lock File","Make a Python Excel job single-instance: fcntl.flock and msvcrt locks, skip versus wait with a timeout, the holder's PID in the lock file, why PID files go stale, per-report lock names, exit codes for skipped runs, and database advisory locks across machines.","prevent-overlapping-runs-of-an-excel-report-job","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job\u002Findex",[2135,2136],"reliable-jobs","scheduling","how-to","CE2k7fW2V9shBgLUmlkx_xgR8nTpCSgBXDxZCOqthxc",[2140,2144],{"title":2141,"path":2142,"stem":2143,"children":-1},"Prevent Formula Injection in Excel Exports from Python","\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports\u002Findex",{"title":2145,"path":2146,"stem":2147,"children":-1},"Quarantine Bad Excel Files in a Batch Job","\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job\u002Findex",[2149,2151],{"path":2150,"title":22},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation",{"path":2152,"title":2125},"\u002Fautomating-reporting-workflows",[],[2155,2159,2162,2165,2168,2171],{"path":2156,"title":2157,"kind":2158},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts","Testing and Packaging Excel Automation Scripts","guide",{"path":2160,"title":2161,"kind":2137},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fblock-large-or-sensitive-excel-files-with-a-pre-commit-hook","Block Large or Sensitive Excel Files with a Pre-commit Hook",{"path":2163,"title":2164,"kind":2137},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse","Build a Command-Line Tool for Excel Reports with argparse",{"path":2166,"title":2167,"kind":2137},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests","Compare Excel Output to a Golden File in Tests",{"path":2169,"title":2170,"kind":2137},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory","Create Excel Test Fixtures in Memory for pytest",{"path":2172,"title":2173,"kind":2137},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fdiff-excel-files-in-git-with-a-textconv-driver","Diff Excel Files in Git with a textconv Driver",1790972132066]