{"record":{"id":"34a40948d90b4f47","repo":"ZhuLinsen/daily_stock_analysis","slug":"portfolio-busy","errorCode":"portfolio_busy","errorMessage":"Portfolio ledger is busy; please retry shortly.","messagePattern":"Portfolio ledger is busy; please retry shortly\\.","errorType":"exception","errorClass":"PortfolioBusyError","httpStatus":409,"severity":"warning","filePath":"src/repositories/portfolio_repo.py","lineNumber":144,"sourceCode":"            if row is None:\n                return False\n            row.is_active = False\n            row.updated_at = datetime.now()\n            session.commit()\n            return True\n\n    # ------------------------------------------------------------------\n    # Event writes\n    # ------------------------------------------------------------------\n    @contextmanager\n    def portfolio_write_session(self):\n        session = self.db.get_session()\n        try:\n            session.connection().exec_driver_sql(\"BEGIN IMMEDIATE\")\n        except OperationalError as exc:\n            session.close()\n            if self._is_sqlite_locked_error(exc):\n                raise PortfolioBusyError(\"Portfolio ledger is busy; please retry shortly.\") from exc\n            raise\n\n        try:\n            yield session\n            session.commit()\n        except OperationalError as exc:\n            session.rollback()\n            if self._is_sqlite_locked_error(exc):\n                raise PortfolioBusyError(\"Portfolio ledger is busy; please retry shortly.\") from exc\n            raise\n        except Exception:\n            session.rollback()\n            raise\n        finally:\n            session.close()\n\n    def add_trade(\n        self,","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/repositories/portfolio_repo.py#L126-L162","documentation":"PortfolioBusyError (code portfolio_busy) raised from portfolio_write_session when the SQLite 'BEGIN IMMEDIATE' used to serialize ledger writes cannot acquire the write lock — another connection holds it. It is a transient, retryable contention signal: the code distinguishes SQLite 'database is locked' OperationalErrors from real failures and re-raises others unchanged.","triggerScenarios":"Two concurrent add_trade/ledger writes hit the same SQLite database file: the second session's BEGIN IMMEDIATE hits the lock and raises this immediately (no implicit long busy-wait). Typical with the web API (FastAPI thread pool) and a scheduled analysis run writing trades simultaneously.","commonSituations":"API request racing the scheduler; multiple worker processes on one SQLite file; long-running write transactions elsewhere (manual sqlite3 session, migrations) holding the write lock; SQLite busy_timeout set too low or 0.","solutions":["Retry the write after a short backoff — the error is explicitly designed as retryable ('please retry shortly')","Serialize portfolio writes through a single process/thread or an application-level lock so BEGIN IMMEDIATE contention cannot occur","Set a nonzero busy_timeout on the SQLite engine (e.g. sqlite3 connect timeout / SQLAlchemy connect_args) so brief lock holders release before the error fires","Move high-concurrency deployments off a single SQLite file to a client-server database if contention is constant"],"exampleFix":"# before\nwith repo.portfolio_write_session() as s:\n    repo.add_trade(...)\n\n# after\nfor attempt in range(3):\n    try:\n        with repo.portfolio_write_session() as s:\n            repo.add_trade(...)\n        break\n    except PortfolioBusyError:\n        time.sleep(0.2 * (attempt + 1))\nelse:\n    raise","handlingStrategy":"retry","validationCode":null,"typeGuard":"from src.repositories.portfolio_repo import PortfolioBusyError\n\ndef is_portfolio_busy(exc: BaseException) -> bool:\n    return isinstance(exc, PortfolioBusyError) or getattr(exc, \"code\", None) == \"portfolio_busy\"","tryCatchPattern":"from src.repositories.portfolio_repo import PortfolioBusyError\n\nfor attempt in range(5):\n    try:\n        with repo.portfolio_write_session() as session:\n            repo.add_trade(account_id=1, trade_uid=uid, ...)\n        break\n    except PortfolioBusyError:\n        if attempt == 4:\n            raise\n        time.sleep(0.1 * 2 ** attempt)","preventionTips":["Give every ledger write a stable trade_uid so retries are idempotent","Route all portfolio writes through one queue/worker to avoid lock races","Configure a nonzero SQLite busy_timeout to absorb micro-contention"],"tags":["sqlite","concurrency","database","retry","portfolio"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}