shareAI-lab/learn-claude-code · error · WorkflowInputError

workflow run {run_id} is already active

Error message

workflow run {run_id} is already active

What it means

workflow_run_lock first takes an in-process threading.Lock keyed by run_id; a non-blocking acquire failure means another thread in this process is already running that run, and WorkflowInputError is raised immediately. This is the cheap first tier of a two-tier lock that also uses an flock file (see error 108) to guard across processes.

Source

Thrown at s16_workflow_runtime/code.py:94

    return run_id


# -- Errors --
class WorkflowInputError(Exception):
    """Bad workflow, metadata, or schema input."""


_run_locks_guard = threading.Lock()
_run_locks: dict[str, threading.Lock] = {}


@contextmanager
def workflow_run_lock(run_id: str):
    """Hold one run across threads and host processes for its full lifecycle."""
    with _run_locks_guard:
        local_lock = _run_locks.setdefault(run_id, threading.Lock())
    if not local_lock.acquire(blocking=False):
        raise WorkflowInputError(f"workflow run {run_id} is already active")

    handle = None
    try:
        STORE.mkdir(parents=True, exist_ok=True)
        handle = (STORE / f"{run_id}.lock").open("a+")
        try:
            fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError as exc:
            raise WorkflowInputError(
                f"workflow run {run_id} is already active"
            ) from exc
        yield
    finally:
        if handle is not None:
            try:
                fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
            finally:
                handle.close()

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Let the first invocation finish before retrying, or make retries conditional on the run no longer being active
  2. Generate a fresh run_id per logical execution so concurrent runs are distinct runs
  3. Catch WorkflowInputError around launch and surface 'already running' to the caller instead of failing opaquely

Example fix

# before
run_async(workflow, run_id=fixed_id)  # retry while first still active

# after
run_id = reserve_run_id(meta)  # unique per execution
run_async(workflow, run_id=run_id)
Defensive patterns

Strategy: try-catch

Validate before calling

# Best pre-check: make concurrent runs distinct by construction
run_id = reserve_run_id(meta)  # unique per execution; same-thread re-entry then cannot collide

Try / catch

try:
    with workflow_run_lock(run_id):
        result = run(workflow, run_id=run_id)
except WorkflowInputError as exc:
    if "already active" in str(exc):
        # another thread in this process holds it; poll for completion or return 409-style status
        return {"status": "already-running", "run_id": run_id}
    raise

Prevention

When it happens

Trigger: Two threads calling run()/resume() with the same run_id concurrently — e.g. a web handler retrying a request while the first is still executing, or a scheduler firing a workflow that a manual invocation already started.

Common situations: Duplicate HTTP submissions racing each other. Retry middleware that re-invokes an async workflow before the first attempt finished. Background scheduler overlapping with a manual trigger.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/a684ee3d808ee596. Report an issue: GitHub.