Skip to content

fix(executor): task refs + pipeline semaphore + off-loop broker [修复组③] - #27

Merged
2233admin merged 1 commit into
mainfrom
fix/executor-taskref-concurrency-broker
Jul 19, 2026
Merged

fix(executor): task refs + pipeline semaphore + off-loop broker [修复组③]#27
2233admin merged 1 commit into
mainfrom
fix/executor-taskref-concurrency-broker

Conversation

@2233admin

Copy link
Copy Markdown
Owner

修复组③ — 执行器 (账本 C5/C6/C9)

Sonnet 实施, Fable 审计通过 (强引用时序 / semaphore 包执行体 / to_thread 逐项核过)。

  • C5create_task 可被 GC 吞任务 → 强引用 dict (照同类 dispatch_acquisition 先例)。关键时序: task 入库先于 acquire semaphore, 故阻塞等待期间也被引用, 不只运行时。manual (task_id 唯一) track-and-pop; scheduled (schedule_id 复用) skip-if-inflight 消自重叠
  • C6 进程内 pipeline 无全局并发上限 → asyncio.Semaphore 按 event-loop id keyed (照 domain_limiter), 包 _run_collection/_run_scheduled 执行体不包 dispatch (dispatch 保持快速非阻塞)。上限 LOCAL_MAX_CONCURRENT_PIPELINES 默认 8, 走 pydantic-settings
  • C9 apply_async 同步 broker 往返阻塞事件循环 → 两处包 asyncio.to_thread

Deviation (审计已收)

  1. 两个 tracking dict (task_id / schedule_id 不同 id 空间) — 非一 dict 双键
  2. scheduled skip-if-inflight (按 dispatch_acquisition 先例, spec 指定)
  3. C9 只改 spec 点名的两方法; dispatch_acquisition 自己的 apply_async 未动 — flag 为 follow-up

Test

  • tests/unit/test_executor.py 21/21 (5× 连跑稳定): C5 引用保留+完成弹出、scheduled 在飞跳过、C6 semaphore 真正限流 N>limit、config 默认+env override、C9 经 to_thread
  • 全量 unit: 1253 passed, 2 fail = 账本 P3-6 已知存量 flake (nodes_install GBK, 与本改动无关)

…p broker dispatch [C5,C6,C9]

- C5: LocalExecutor.dispatch_collection/dispatch_scheduled_collection now
  hold a strong reference to their asyncio.Task (in new _collection_tasks /
  _scheduled_collection_tasks dicts, popped on completion) instead of
  discarding the return of create_task, which asyncio may otherwise GC
  mid-flight — same fix already in place for dispatch_acquisition.
  dispatch_scheduled_collection also gains dispatch_acquisition's
  skip-if-inflight check, since schedule_id (unlike task_id) is not unique
  per tick.

- C6: local.py gains a process-wide asyncio.Semaphore bounding concurrently
  RUNNING pipeline executions, independent of the per-domain cap in
  pipeline/domain_limiter.py. The limit is configurable via the new
  Settings.local_max_concurrent_pipelines field (env
  LOCAL_MAX_CONCURRENT_PIPELINES, default 8). The semaphore wraps pipeline
  execution (inside _run_collection/_run_scheduled), not dispatch, so
  dispatch_collection/dispatch_scheduled_collection stay fast and
  non-blocking; a task queued behind the semaphore is already referenced
  per the C5 fix, so it can't be GC'd while waiting.

- C9: CeleryExecutor.dispatch_collection/dispatch_scheduled_collection wrap
  their apply_async calls in asyncio.to_thread so the synchronous broker
  round-trip no longer runs inline on the event loop.

Tests extended in tests/unit/test_executor.py for all three findings.
@repowise-bot

repowise-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

✅ Health: 9.1

📋 At a glance
2 hotspots touched · 1 new finding introduced.

Files & modules (2)
  • backend (1 file)
    • backend/config.py
  • tests (1 file)
    • tests/unit/test_executor.py

🚨 Change risk: 9.0/10 (high)
This change's risk is driven by:

  • more lines added than baseline
  • more scattered than baseline
🔎 More signals (1)

🔥 Hotspots touched (2)

  • tests/unit/test_executor.py — 1 commits/90d, 0 dependents · primary owner: xujinghua (100%)
  • backend/config.py — 14 commits/90d, 15 dependents · primary owner: xujinghua (100%)

👀 Suggested reviewers @xujinghua


📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-18 17:06 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added a configurable limit for concurrent local pipeline executions, defaulting to 8.
    • Prevented overlapping scheduled pipeline runs for the same schedule.
  • Bug Fixes
    • Improved responsiveness when dispatching tasks through Celery by preventing broker operations from blocking the event loop.
    • Ensured local pipeline tasks remain tracked while running and are cleaned up after completion.

Walkthrough

The local executor now limits concurrent pipeline runs, retains inflight tasks, and skips overlapping scheduled runs. Celery broker dispatch is offloaded to worker threads, and tests cover configuration, concurrency, task tracking, scheduling, and event-loop responsiveness.

Changes

Executor dispatch behavior

Layer / File(s) Summary
Local pipeline concurrency and task tracking
backend/config.py, backend/executor/local.py, tests/unit/test_executor.py
Adds the configurable local pipeline limit, loop-specific semaphore handling, strong task references, scheduled-run deduplication, and coverage for these behaviors.
Celery broker dispatch offloading
backend/executor/celery_exec.py, tests/unit/test_executor.py
Runs synchronous Celery apply_async calls through asyncio.to_thread and tests returned metadata and event-loop progress.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: lunnynight

Poem

A bunny bounds through tasks in flight,
Eight pipelines hop just right.
Celery calls leave loops unblocked,
Scheduled hops stay neatly clocked.
Tests thump paws: the flow is bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the executor fixes: task references, pipeline semaphore, and off-loop broker calls.
Description check ✅ Passed The description is directly related to the PR and accurately summarizes the executor fixes and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a process-wide concurrency cap for local pipeline executions, ensures strong task references are retained to prevent premature garbage collection, and offloads Celery's synchronous apply_async calls to a separate thread using asyncio.to_thread. The review feedback suggests using weakref.WeakKeyDictionary instead of id(loop) to prevent potential bugs with recycled event loop IDs, which also renders the manual test cleanup fixture obsolete.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread backend/executor/local.py
Comment on lines 3 to +27
import asyncio
import logging

from backend.config import get_settings
from backend.executor.base import AbstractExecutor

logger = logging.getLogger(__name__)

# AUDIT C6: process-wide cap on concurrently-RUNNING pipeline executions
# (manual/webhook dispatch_collection + every dispatch_scheduled_collection),
# independent of the per-domain cap in pipeline/domain_limiter.py. Keyed by
# event loop id, mirroring domain_limiter._semaphore, so a semaphore is never
# reused across event loops (production runs one loop and shares correctly;
# tests get a fresh loop each and never touch a stale entry).
_pipeline_semaphores: dict[int, asyncio.Semaphore] = {}


def _pipeline_semaphore() -> asyncio.Semaphore:
loop = asyncio.get_running_loop()
key = id(loop)
sem = _pipeline_semaphores.get(key)
if sem is None:
sem = asyncio.Semaphore(get_settings().local_max_concurrent_pipelines)
_pipeline_semaphores[key] = sem
return sem

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using id(loop) as a key in a global dictionary is prone to bugs because Python's id() is only guaranteed to be unique during the lifetime of the object. When an event loop is closed and garbage collected (especially during tests or loop restarts), its memory address (and thus its id) can be recycled for a newly created event loop. This causes the new loop to reuse a stale semaphore bound to the old loop, leading to RuntimeError or unexpected concurrency limits.

Using weakref.WeakKeyDictionary with the event loop object itself as the key resolves this cleanly. It automatically removes the semaphore entry when the event loop is garbage collected, preventing both memory leaks and ID recycling bugs without requiring manual cleanup fixtures in tests.

import asyncio
import logging
import weakref

from backend.config import get_settings
from backend.executor.base import AbstractExecutor

logger = logging.getLogger(__name__)

# AUDIT C6: process-wide cap on concurrently-RUNNING pipeline executions
# (manual/webhook dispatch_collection + every dispatch_scheduled_collection),
# independent of the per-domain cap in pipeline/domain_limiter.py. Keyed by
# event loop object using a WeakKeyDictionary, so semaphores are automatically
# cleaned up when loops are closed and garbage collected, preventing memory
# leaks and ID reuse bugs.
_pipeline_semaphores: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, asyncio.Semaphore] = weakref.WeakKeyDictionary()


def _pipeline_semaphore() -> asyncio.Semaphore:
    loop = asyncio.get_running_loop()
    sem = _pipeline_semaphores.get(loop)
    if sem is None:
        sem = asyncio.Semaphore(get_settings().local_max_concurrent_pipelines)
        _pipeline_semaphores[loop] = sem
    return sem

Comment on lines +9 to +21
from backend.executor import local as local_executor_module


@pytest.fixture(autouse=True)
def _clear_pipeline_semaphore_registry():
"""AUDIT C6's semaphore registry is a module-level dict keyed by event
loop id (mirrors backend.pipeline.domain_limiter._semaphores). Clear it
around every test in this file so a stale entry from a prior test's
(closed) event loop can never be reused just because CPython recycled
its id() for a new loop object."""
local_executor_module._pipeline_semaphores.clear()
yield
local_executor_module._pipeline_semaphores.clear()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the transition of _pipeline_semaphores to a weakref.WeakKeyDictionary, the manual clearing of the registry between tests is no longer necessary. Closed event loops will be garbage collected, and their corresponding semaphore entries will be automatically discarded. We can safely remove this fixture and its import.

Suggested change
from backend.executor import local as local_executor_module
@pytest.fixture(autouse=True)
def _clear_pipeline_semaphore_registry():
"""AUDIT C6's semaphore registry is a module-level dict keyed by event
loop id (mirrors backend.pipeline.domain_limiter._semaphores). Clear it
around every test in this file so a stale entry from a prior test's
(closed) event loop can never be reused just because CPython recycled
its id() for a new loop object."""
local_executor_module._pipeline_semaphores.clear()
yield
local_executor_module._pipeline_semaphores.clear()
# The _clear_pipeline_semaphore_registry fixture is no longer needed
# because _pipeline_semaphores now uses a WeakKeyDictionary.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/config.py`:
- Around line 27-34: The local_max_concurrent_pipelines configuration currently
accepts invalid non-positive values. Validate this setting as strictly greater
than zero during configuration loading or validation, preserving the existing
default of 8, and add boundary tests covering zero and negative values being
rejected.

In `@backend/executor/celery_exec.py`:
- Around line 38-51: Update the Celery enqueue flows in the collection dispatch
method and dispatch_scheduled_collection to shield or otherwise await the
in-flight asyncio.to_thread broker call after cancellation, allowing apply_async
to finish before re-raising CancelledError. Preserve the existing task arguments
and return behavior, and add regression coverage verifying cancellation cannot
leave collection or scheduled work enqueued in the background or duplicated on
retry.

In `@backend/executor/local.py`:
- Around line 17-26: Update _pipeline_semaphore and _pipeline_semaphores to key
entries by the asyncio event loop object rather than id(loop), ensuring each
loop retrieves only its own semaphore. Preserve the existing per-loop creation
and configured concurrency limit while avoiding stale semaphore reuse across
closed and newly created loops.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 57783396-2f47-4397-9332-b9fb673faf3a

📥 Commits

Reviewing files that changed from the base of the PR and between cb6c1ab and 96eb927.

📒 Files selected for processing (4)
  • backend/config.py
  • backend/executor/celery_exec.py
  • backend/executor/local.py
  • tests/unit/test_executor.py

Comment thread backend/config.py
Comment on lines +27 to +34
# AUDIT C6: process-wide cap on concurrently-RUNNING pipeline executions in
# the local (in-process asyncio) executor — independent of the per-domain
# cap in pipeline/domain_limiter.py. Bounds how many schedules firing on
# the same tick plus manual/webhook triggers can drive Chrome/opencli
# subprocesses at once on the one event loop. Only meaningful when
# task_executor="local" (celery fans out across worker processes
# instead). Env: LOCAL_MAX_CONCURRENT_PIPELINES.
local_max_concurrent_pipelines: int = 8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== backend/config.py ==\n'
ast-grep outline backend/config.py --view expanded || true

printf '\n== relevant settings/tests search ==\n'
rg -n "local_max_concurrent_pipelines|LOCAL_MAX_CONCURRENT_PIPELINES|Semaphore|concurrent pipelines" backend tests . || true

Repository: 2233admin/opencli-admin

Length of output: 5930


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== backend/executor/local.py ==\n'
cat -n backend/executor/local.py

printf '\n== tests/unit/test_executor.py (relevant section) ==\n'
sed -n '180,260p' tests/unit/test_executor.py

printf '\n== runtime probe: asyncio.Semaphore and pydantic settings coercion ==\n'
python3 - <<'PY'
import asyncio
import os
from pydantic import BaseSettings

class S(BaseSettings):
    x: int = 8

for value in ["0", "-1", "3"]:
    os.environ["X"] = value
    try:
        s = S()
        print(f"env={value!r} -> parsed={s.x}")
        sem = asyncio.Semaphore(s.x)
        print(f"  semaphore._value={sem._value}")
    except Exception as e:
        print(f"env={value!r} -> ERROR {type(e).__name__}: {e}")
PY

Repository: 2233admin/opencli-admin

Length of output: 10628


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== backend/config.py (top) ==\n'
sed -n '1,80p' backend/config.py | cat -n

printf '\n== runtime probe: actual backend.config.Settings parsing ==\n'
python3 - <<'PY'
import os
from contextlib import contextmanager

`@contextmanager`
def setenv(key, value):
    old = os.environ.get(key)
    os.environ[key] = value
    try:
        yield
    finally:
        if old is None:
            os.environ.pop(key, None)
        else:
            os.environ[key] = old

from backend.config import Settings

for value in ["0", "-1", "3"]:
    with setenv("LOCAL_MAX_CONCURRENT_PIPELINES", value):
        try:
            s = Settings()
            print(f"value={value!r} -> parsed={s.local_max_concurrent_pipelines!r}")
        except Exception as e:
            print(f"value={value!r} -> ERROR {type(e).__name__}: {e}")
PY

Repository: 2233admin/opencli-admin

Length of output: 4255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import asyncio
import inspect

for value in [0, -1, 1]:
    try:
        sem = asyncio.Semaphore(value)
        print(f"Semaphore({value}) -> ok, initial={sem._value}")
    except Exception as e:
        print(f"Semaphore({value}) -> {type(e).__name__}: {e}")

print("\n--- asyncio.Semaphore source snippet ---")
src = inspect.getsource(asyncio.Semaphore)
print(src)
PY

Repository: 2233admin/opencli-admin

Length of output: 3334


Reject non-positive concurrency limits. 0 leaves the local executor semaphore permanently locked, and negatives raise ValueError when it is created; constrain local_max_concurrent_pipelines to > 0 and add boundary tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/config.py` around lines 27 - 34, The local_max_concurrent_pipelines
configuration currently accepts invalid non-positive values. Validate this
setting as strictly greater than zero during configuration loading or
validation, preserving the existing default of 8, and add boundary tests
covering zero and negative values being rejected.

Comment on lines +38 to +51
result = await asyncio.to_thread(
run_collection.apply_async,
kwargs={"task_id": task_id, "parameters": parameters},
)
return {"task_id": task_id, "celery_task_id": result.id}

async def dispatch_scheduled_collection(
self, schedule_id: str, source_id: str, parameters: dict
) -> None:
from backend.worker.tasks import run_scheduled_collection
run_scheduled_collection.apply_async(
kwargs={"schedule_id": schedule_id, "source_id": source_id, "parameters": parameters}

await asyncio.to_thread(
run_scheduled_collection.apply_async,
kwargs={"schedule_id": schedule_id, "source_id": source_id, "parameters": parameters},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== backend/executor/celery_exec.py ==\n'
wc -l backend/executor/celery_exec.py
sed -n '1,220p' backend/executor/celery_exec.py

printf '\n== search for celery_exec tests / cancellation ==\n'
rg -n "celery_exec|apply_async|to_thread|cancellation|CancelledError|dispatch_scheduled_collection|dispatch_collection" backend tests . -g '!**/node_modules/**' || true

printf '\n== asyncio.to_thread cancellation probe ==\n'
python3 - <<'PY'
import asyncio, threading, time

events = []

def work():
    events.append(("work_start", threading.current_thread().name, time.time()))
    time.sleep(0.2)
    events.append(("work_end", threading.current_thread().name, time.time()))
    return 123

async def main():
    task = asyncio.create_task(asyncio.to_thread(work))
    await asyncio.sleep(0.05)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        events.append(("cancelled", time.time()))
    await asyncio.sleep(0.3)
    print(events)

asyncio.run(main())
PY

Repository: 2233admin/opencli-admin

Length of output: 31706


Guard the Celery enqueue against cancellation races. asyncio.to_thread() can keep running after CancelledError, so a retry may enqueue duplicate collection or scheduled work in the background. Let the broker call finish before re-raising cancellation, and add a cancellation regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/executor/celery_exec.py` around lines 38 - 51, Update the Celery
enqueue flows in the collection dispatch method and
dispatch_scheduled_collection to shield or otherwise await the in-flight
asyncio.to_thread broker call after cancellation, allowing apply_async to finish
before re-raising CancelledError. Preserve the existing task arguments and
return behavior, and add regression coverage verifying cancellation cannot leave
collection or scheduled work enqueued in the background or duplicated on retry.

Comment thread backend/executor/local.py
Comment on lines +17 to +26
_pipeline_semaphores: dict[int, asyncio.Semaphore] = {}


def _pipeline_semaphore() -> asyncio.Semaphore:
loop = asyncio.get_running_loop()
key = id(loop)
sem = _pipeline_semaphores.get(key)
if sem is None:
sem = asyncio.Semaphore(get_settings().local_max_concurrent_pipelines)
_pipeline_semaphores[key] = sem

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the executor implementation and its tests
git ls-files backend/executor backend | sed -n '1,200p'

# Map the local executor file structure
if [ -f backend/executor/local.py ]; then
  echo "=== backend/executor/local.py ==="
  cat -n backend/executor/local.py | sed -n '1,220p'
fi

# Find references to the semaphore registry and any test fixture cleanup
rg -n "_pipeline_semaphores|_pipeline_semaphore|get_running_loop|local_max_concurrent_pipelines|clear\\(" backend tests -S

# Find tests touching loop reuse / event loop fixtures
rg -n "event_loop|asyncio\\.Semaphore|loop id|id\\(loop\\)|closed loop|recycled" tests backend -S

Repository: 2233admin/opencli-admin

Length of output: 29677


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== backend/pipeline/domain_limiter.py ==="
cat -n backend/pipeline/domain_limiter.py | sed -n '1,140p'

echo "=== tests/unit/test_executor.py (fixture + semaphore tests) ==="
cat -n tests/unit/test_executor.py | sed -n '1,280p'

echo "=== tests/conftest.py (event loop fixture) ==="
cat -n tests/conftest.py | sed -n '1,140p'

Repository: 2233admin/opencli-admin

Length of output: 19453


🏁 Script executed:

python3 - <<'PY'
import asyncio, gc, sys

print("python", sys.version)

# Probe 1: loop id recycling after creating/closing many loops.
ids = []
for _ in range(2000):
    loop = asyncio.new_event_loop()
    ids.append(id(loop))
    loop.close()
    del loop
gc.collect()

dups = len(ids) - len(set(ids))
print("loop_id_duplicates", dups)
if dups:
    seen = set()
    for i, x in enumerate(ids):
        if x in seen:
            print("first_reused_id", x, "at_index", i)
            break
        seen.add(x)

# Probe 2: create semaphore in one loop, then use it in another loop.
async def make_sem():
    return asyncio.Semaphore(0)

loop1 = asyncio.new_event_loop()
try:
    sem = loop1.run_until_complete(make_sem())
finally:
    loop1.close()

async def try_acquire(s):
    await asyncio.wait_for(s.acquire(), timeout=0.1)
    return "acquired"

loop2 = asyncio.new_event_loop()
try:
    result = loop2.run_until_complete(try_acquire(sem))
    print("cross_loop_acquire", result)
except Exception as e:
    print("cross_loop_acquire_error", type(e).__name__, str(e))
finally:
    loop2.close()
PY

Repository: 2233admin/opencli-admin

Length of output: 324


Key the semaphore registry by the loop object, not id(loop). id(loop) is reused quickly, and this registry never evicts entries, so a new loop can pick up a stale semaphore from a closed one. Use the loop object (or a weak-key registry) to keep semaphore ownership tied to loop lifetime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/executor/local.py` around lines 17 - 26, Update _pipeline_semaphore
and _pipeline_semaphores to key entries by the asyncio event loop object rather
than id(loop), ensuring each loop retrieves only its own semaphore. Preserve the
existing per-loop creation and configured concurrency limit while avoiding stale
semaphore reuse across closed and newly created loops.

@2233admin
2233admin merged commit 2f5215a into main Jul 19, 2026
5 checks passed
@2233admin
2233admin deleted the fix/executor-taskref-concurrency-broker branch July 19, 2026 03:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant