Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
20d66e6
⚡ Bolt: [성능 개선] 제너레이터 조기 종료 시 ThreadPoolExecutor hang 방지
seonghobae Sep 13, 2026
da9a336
⚡ Bolt: [성능 개선] 신속한 제너레이터 cleanup(iterator-close latency 개선)
seonghobae Sep 14, 2026
d8c6992
⚡ Bolt: [성능 개선] 신속한 제너레이터 cleanup(iterator-close latency 개선)
seonghobae Sep 14, 2026
362ef83
⚡ Bolt: [성능 개선] 신속한 제너레이터 cleanup(iterator-close latency 개선)
seonghobae Sep 14, 2026
c9aae8a
⚡ Bolt: [성능 개선] 신속한 제너레이터 cleanup(iterator-close latency 개선)
seonghobae Sep 14, 2026
861ddfc
⚡ Bolt: [성능 개선] 신속한 제너레이터 cleanup(iterator-close latency 개선)
seonghobae Sep 15, 2026
0a7fd19
⚡ Bolt: [성능 개선] 신속한 제너레이터 cleanup 및 HTTPError 메시지 보완
seonghobae Sep 16, 2026
b80a242
⚡ Bolt: [성능 개선] 신속한 제너레이터 cleanup(iterator-close latency 개선) 및 HTTPEr…
seonghobae Sep 16, 2026
00c5050
⚡ Bolt: [성능 개선] 신속한 제너레이터 cleanup 및 HTTPError 메시지 보완
seonghobae Sep 17, 2026
cc4e72c
merge(main): resolve #2184 conflicts; keep ThreadPoolExecutor hang fix
seonghobae Sep 17, 2026
cf3eab1
⚡ Bolt: [성능 개선] 신속한 제너레이터 cleanup(iterator-close latency 개선)
seonghobae Sep 17, 2026
e8c4f53
fix(sweep): join cooperative repository workers
seonghobae Sep 19, 2026
86b62ce
test(agent-mention): require full-suite dependency closure
seonghobae Sep 19, 2026
8c3434d
fix(agent-mention): install complete full-suite closure
seonghobae Sep 19, 2026
89e9fd3
fix: restore UTF-8 product gap baseline
seonghobae Sep 19, 2026
faeb93c
⚡ Bolt: 기록 추가 - ThreadPoolExecutor shutdown 시의 wait=False에 관한 교훈
seonghobae Sep 20, 2026
e0d5575
⚡ Bolt: 기록 추가 - ThreadPoolExecutor shutdown 시의 wait=False에 관한 교훈
seonghobae Sep 20, 2026
607f622
⚡ Bolt: 기록 추가 - ThreadPoolExecutor shutdown 시의 wait=False에 관한 교훈
seonghobae Sep 22, 2026
5e500ae
⚡ Bolt: 기록 추가 - ThreadPoolExecutor shutdown 시의 wait=False에 관한 교훈
seonghobae Sep 22, 2026
75bda20
Merge remote-tracking branch 'origin/main' into bolt-fix-executor-shu…
seonghobae Sep 28, 2026
0e3e3e6
test(agent-mention): cover cooperative cancellation edges
seonghobae Sep 28, 2026
58a039b
⚡ Bolt: 협력적 취소(cancellation)로 ThreadPoolExecutor 지연 현상 수정
seonghobae Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions scripts/ci/agent_mention_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,14 @@
# log tail and metrics. Stop dispatching new work with margin to spare so
# the sweep exits cleanly and reports what it completed.
#
# Returning early only stops NEW work: list_recent_pull_requests' generator
# cleanup still blocks (executor.shutdown(wait=True)) until every currently
# RUNNING repository fetch finishes on its own. GitHubClient's rate-limit
# retry costs up to ~255s worst case for one repository (six attempts, each
# up to the 30s subprocess timeout, plus ~75s of backoff between them), and
# up to max_workers of those can be running concurrently at the moment the
# deadline trips (bounded by that ceiling, not multiplied by it, since they
# run in parallel). Budget = 900s job timeout - ~60s setup/checkout
# overhead - ~255s worst-case cleanup wait, with a further margin still
# unspent.
DEFAULT_TIME_BUDGET_SECONDS = 480.0
# Returning early stops NEW work and promptly abandons running fetches:
# list_recent_pull_requests' generator cleanup no longer blocks
# (executor.shutdown(wait=False)) on currently RUNNING repository fetches.
# We no longer need to budget ~255s for a worst-case GitHubClient rate-limit
# retry cleanup wait, but workers may not terminate immediately if they
# are blocked on I/O.
# Budget = 900s job timeout - ~60s setup/checkout overhead - ~10s worker margin
DEFAULT_TIME_BUDGET_SECONDS = 830.0


@dataclass
Expand Down Expand Up @@ -254,7 +251,7 @@ def fetch(repository: str) -> list[dict[str, Any]]:
stop_event.set()
for future in futures:
future.cancel()
executor.shutdown(wait=True, cancel_futures=True)
executor.shutdown(wait=False, cancel_futures=True)


def list_recent_comments(
Expand Down
5 changes: 4 additions & 1 deletion scripts/ci/noema_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1672,8 +1672,11 @@ def call_llm(
f"Noema model output failed local validation: {current_failure}{suffix}"
) from None
if isinstance(exc, (urllib.error.URLError, http.client.HTTPException, OSError)):
msg_name = type(exc).__name__
if isinstance(exc, urllib.error.HTTPError):
msg_name = f"HTTPError"
raise NoemaTransportError(
f"Noema gateway transport failed: {type(exc).__name__}: {current_failure}{suffix}"
f"Noema gateway transport failed: {msg_name}: {current_failure}{suffix}"
) from exc
raise RuntimeError(
f"Noema review failed closed: {current_failure}{suffix}"
Expand Down
30 changes: 30 additions & 0 deletions tests/test_agent_mention_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,36 @@ def test_dispatched_agents_fetches_multiple_candidates_concurrently() -> None:
assert len(artifact_calls) == 2


def test_dispatched_agents_fetches_multiple_candidates_concurrently_shutdown(monkeypatch) -> None:
"""The thread pool shutdown uses wait=False to ensure fast cleanup."""

module = load_module()
request = module.parse_event(
event("@cwl-noema-review @opencode-agent")
)
assert request is not None
client = FakeClient()

import concurrent.futures
import threading

shutdown_called_with_no_wait = False

class MockExecutor(concurrent.futures.ThreadPoolExecutor):
def shutdown(self, wait=True, cancel_futures=False):
nonlocal shutdown_called_with_no_wait
if not wait and cancel_futures:
shutdown_called_with_no_wait = True
super().shutdown(wait=wait, cancel_futures=cancel_futures)

monkeypatch.setattr(concurrent.futures, "ThreadPoolExecutor", MockExecutor)

observed = module.dispatched_agents(request, client)

assert observed == frozenset()
assert shutdown_called_with_no_wait


def test_dispatched_agents_single_candidate_skips_thread_pool() -> None:
"""Exactly one uncached agent stays on the plain sequential path."""

Expand Down
69 changes: 69 additions & 0 deletions tests/test_agent_mention_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,75 @@ def recording_build_requests(client, *, issue, since):
assert "time budget" in capsys.readouterr().out


def test_list_recent_pull_requests_shutdown_behavior(monkeypatch) -> None:
"""The generator correctly invokes executor.shutdown(wait=False, cancel_futures=True) on cleanup."""

sweep = module()
client = FakeClient()
monkeypatch.setattr(
sweep, "list_accessible_repositories", lambda *args, **kwargs: ["ContextualWisdomLab/repo"]
)

import concurrent.futures
import threading
shutdown_called_with_no_wait = False

# We need a latch to ensure the worker starts running before we close.
worker_started = threading.Event()
worker_can_finish = threading.Event()

def fake_request(*args, **kwargs):
worker_started.set()
worker_can_finish.wait(timeout=5)
return [{"number": 1, "created_at": "2026-08-05T00:00:00Z", "updated_at": "2026-08-05T00:00:00Z"}]

monkeypatch.setattr(client, "request", fake_request)

class MockExecutor(concurrent.futures.ThreadPoolExecutor):
def shutdown(self, wait=True, cancel_futures=False):
nonlocal shutdown_called_with_no_wait
if not wait and cancel_futures:
shutdown_called_with_no_wait = True
super().shutdown(wait=wait, cancel_futures=cancel_futures)

monkeypatch.setattr(concurrent.futures, "ThreadPoolExecutor", MockExecutor)

gen = sweep.list_recent_pull_requests(
client,
organization="ContextualWisdomLab",
repository_source="organization",
since="2026-08-05T00:00:00Z",
)

# Prime the generator to start the executor and workers.
try:
next(gen)
except StopIteration:
pass
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated

worker_started.wait(timeout=2)
# Now close the generator, which will trigger the finally block.
# The worker is still running and blocked on worker_can_finish,
# so if wait=True, close() would hang. Since wait=False, close()
# will return immediately.
import time
start = time.monotonic()
gen.close()
elapsed = time.monotonic() - start

# We must explicitly advance the generator (or let it close) properly
# to measure latency.

# Release the worker so the test suite can clean up.
worker_can_finish.set()

# We must explicitly advance the generator (or let it close) properly
# to measure latency.

assert shutdown_called_with_no_wait
assert elapsed < 1.0


def test_sweep_time_budget_can_be_disabled(monkeypatch) -> None:
"""Passing None for the time budget preserves unbounded iteration."""

Expand Down
Loading