# /// script
# requires-python = ">=3.10"
# ///
# Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
# https://thedataengineering.com/lab-code/LICENSE.txt
"""The sensor deadlock.

A worker pool of 10 slots (a semaphore). 20 "sensor" tasks each hold a slot
while they wait for a processing task to finish; 20 "processing" tasks need a
slot to run. All sensors start first. Then the same with sensors that release
their slot while they wait (acquire only to check, sleep outside the pool).
"""
import argparse, json, threading, time

p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("--slots", type=int, default=10, help="Pool slots.")
p.add_argument("--sensors", type=int, default=20, help="Sensor tasks.")
p.add_argument("--tasks", type=int, default=20, help="Processing tasks.")
p.add_argument("--timeout", type=float, default=5, help="Sensor timeout (s).")
p.add_argument("--json", help="Also write the results to this JSON file.")
args = p.parse_args()

def run(release_while_waiting):
    pool = threading.Semaphore(args.slots); done = threading.Event(); first = []; t0 = time.perf_counter()
    def processing(i):
        with pool:                                                     # needs a slot
            if not first: first.append(time.perf_counter() - t0)
            time.sleep(0.1)
        done.set()
    def sensor(i):
        if release_while_waiting:
            while True:
                with pool:                                             # hold a slot only to check
                    if done.is_set(): return
                time.sleep(0.05)                                       # wait outside the pool
                if time.perf_counter() - t0 > args.timeout: return
        else:
            with pool:                                                 # holds a slot while waiting
                done.wait(timeout=args.timeout)                        # waits for a processing task
    ts = [threading.Thread(target=sensor, args=(i,)) for i in range(args.sensors)]
    for t in ts: t.start()
    time.sleep(0.05)                                                   # all sensors first
    ps = [threading.Thread(target=processing, args=(i,)) for i in range(args.tasks)]
    for t in ps: t.start()
    for t in ts + ps: t.join()
    return {"sensor": "release the slot while waiting" if release_while_waiting else "hold the slot while waiting",
            "first_task_s": first[0], "total_s": time.perf_counter() - t0}

rows = [run(False), run(True)]
# Little's Law: sensors arrive together and each would hold a slot for W = timeout, so L = sensors in flight
summary = {"slots": args.slots, "sensor_demand": args.sensors, "lambda_w": args.sensors}
print(f"L = λW: {args.sensors} sensors each waiting up to {args.timeout:g} s want {args.sensors} slots at once; the pool has {args.slots}")
for r in rows: print(f"{r['sensor']:<32} first processing task started after {r['first_task_s']:.2f} s; all done after {r['total_s']:.2f} s")

if args.json:
    with open(args.json, "w") as f:
        json.dump({"summary": summary, "tables": {"runs": rows}}, f)
