fix(kanban-dashboard): use context-local board pin in specify/decompose endpoints
The dashboard specify and decompose endpoints run as sync FastAPI threadpool handlers and pinned the active board by mutating the process-global HERMES_KANBAN_BOARD env var. Two concurrent requests for different boards race on that shared global and cross-write — the same bug class as the CLI path (#38323), now using the scoped_current_board() contextvar introduced by the CLI fix.
This commit is contained in:
@ -38,7 +38,6 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
@ -1620,10 +1619,12 @@ def specify_task_endpoint(
|
|||||||
"""
|
"""
|
||||||
board = _resolve_board(board)
|
board = _resolve_board(board)
|
||||||
# Pin the board for the duration of this call so the specifier module
|
# Pin the board for the duration of this call so the specifier module
|
||||||
# (which calls ``kb.connect()`` with no args) hits the right DB.
|
# (which calls ``kb.connect()`` with no args) hits the right DB. Use a
|
||||||
prev_env = os.environ.get("HERMES_KANBAN_BOARD")
|
# context-local override rather than mutating the process-global
|
||||||
try:
|
# HERMES_KANBAN_BOARD env var — this endpoint runs in FastAPI's
|
||||||
os.environ["HERMES_KANBAN_BOARD"] = board or kanban_db.DEFAULT_BOARD
|
# threadpool, so two concurrent requests for different boards would
|
||||||
|
# otherwise race on the shared env var and cross-write (issue #38323).
|
||||||
|
with kanban_db.scoped_current_board(board or kanban_db.DEFAULT_BOARD):
|
||||||
# Import lazily so a missing auxiliary client at import time
|
# Import lazily so a missing auxiliary client at import time
|
||||||
# doesn't break plugin load.
|
# doesn't break plugin load.
|
||||||
from hermes_cli import kanban_specify # noqa: WPS433 (intentional)
|
from hermes_cli import kanban_specify # noqa: WPS433 (intentional)
|
||||||
@ -1632,11 +1633,6 @@ def specify_task_endpoint(
|
|||||||
task_id,
|
task_id,
|
||||||
author=(payload.author or None),
|
author=(payload.author or None),
|
||||||
)
|
)
|
||||||
finally:
|
|
||||||
if prev_env is None:
|
|
||||||
os.environ.pop("HERMES_KANBAN_BOARD", None)
|
|
||||||
else:
|
|
||||||
os.environ["HERMES_KANBAN_BOARD"] = prev_env
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": bool(outcome.ok),
|
"ok": bool(outcome.ok),
|
||||||
@ -2233,19 +2229,16 @@ def decompose_task_endpoint(
|
|||||||
can take minutes on reasoning models.
|
can take minutes on reasoning models.
|
||||||
"""
|
"""
|
||||||
board = _resolve_board(board)
|
board = _resolve_board(board)
|
||||||
prev_env = os.environ.get("HERMES_KANBAN_BOARD")
|
# Context-local board pin (see specify endpoint above): this sync
|
||||||
try:
|
# endpoint runs in FastAPI's threadpool, so mutating the process-global
|
||||||
os.environ["HERMES_KANBAN_BOARD"] = board or kanban_db.DEFAULT_BOARD
|
# HERMES_KANBAN_BOARD env var would let concurrent requests for
|
||||||
|
# different boards race and cross-write (issue #38323).
|
||||||
|
with kanban_db.scoped_current_board(board or kanban_db.DEFAULT_BOARD):
|
||||||
from hermes_cli import kanban_decompose # noqa: WPS433 (intentional)
|
from hermes_cli import kanban_decompose # noqa: WPS433 (intentional)
|
||||||
outcome = kanban_decompose.decompose_task(
|
outcome = kanban_decompose.decompose_task(
|
||||||
task_id,
|
task_id,
|
||||||
author=(payload.author or None),
|
author=(payload.author or None),
|
||||||
)
|
)
|
||||||
finally:
|
|
||||||
if prev_env is None:
|
|
||||||
os.environ.pop("HERMES_KANBAN_BOARD", None)
|
|
||||||
else:
|
|
||||||
os.environ["HERMES_KANBAN_BOARD"] = prev_env
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": bool(outcome.ok),
|
"ok": bool(outcome.ok),
|
||||||
|
|||||||
Reference in New Issue
Block a user