{"record":{"id":"840214e1b8a01522","repo":"ZhuLinsen/daily_stock_analysis","slug":"history-deletion-made-no-progress","errorCode":null,"errorMessage":"history deletion made no progress","messagePattern":"history deletion made no progress","errorType":"exception","errorClass":"RuntimeError","httpStatus":500,"severity":"error","filePath":"api/v1/endpoints/history.py","lineNumber":335,"sourceCode":"        if not candidates:\n            raise HTTPException(\n                status_code=400,\n                detail={\"error\": \"invalid_request\", \"message\": \"stock_code 不能为空\"},\n            )\n\n        deleted = 0\n        while True:\n            records, _ = db_manager.get_analysis_history_paginated(\n                code=candidates,\n                limit=_DELETE_BY_CODE_BATCH_SIZE,\n            )\n            record_ids = [r.id for r in records if r.id is not None]\n            if not record_ids:\n                break\n\n            batch_deleted = db_manager.delete_analysis_history_records(record_ids)\n            if batch_deleted == 0:\n                raise RuntimeError(\"history deletion made no progress\")\n            deleted += batch_deleted\n\n            if len(records) < _DELETE_BY_CODE_BATCH_SIZE:\n                break\n\n        return DeleteHistoryResponse(deleted=deleted)\n    except HTTPException:\n        raise\n    except Exception as e:\n        logger.error(f\"按股票代码删除历史记录失败: {e}\", exc_info=True)\n        raise HTTPException(\n            status_code=500,\n            detail={\"error\": \"internal_error\", \"message\": f\"删除失败: {str(e)}\"},\n        )\n\n\n@router.delete(\n    \"\",","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/api/v1/endpoints/history.py#L317-L353","documentation":"RuntimeError('history deletion made no progress') is an internal 500 raised inside DELETE /history/by-code/{stock_code}. The loop repeatedly fetches a batch of records matching the code candidates and deletes them by ID; if db_manager.delete_analysis_history_records(record_ids) returns 0 while the query still returns rows, the loop would spin forever, so the guard converts it into an error. It signals a mismatch between what get_analysis_history_paginated returns and what the delete call can actually remove.","triggerScenarios":"Database records whose id is non-None in the read path but fail to delete (row-level permissions, concurrent deletion by another request, a stale read replica serving reads while deletes go to primary), or a delete_analysis_history_records implementation that filters rows (e.g. soft-delete already applied) and reports 0 affected rows.","commonSituations":"Two concurrent DELETE /by-code calls racing on the same stock; SQLite/database locking causing the delete to silently affect 0 rows; a repository bug where the delete statement's WHERE clause uses a different code normalization than the paginated query","solutions":["Retry the DELETE request once — a transient race (another client deleted the batch between read and delete) resolves itself.","Check server logs (the exception is logged with traceback via logger.error('按股票代码删除历史记录失败')) for the underlying DB error.","Inspect src/repositories (delete_analysis_history_records) and confirm its deletion criteria match get_analysis_history_paginated's code filter.","If concurrent deletions are expected in your workflow, serialize them (delete one stock at a time from one client)."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await delByCode(code);\n} catch (e) {\n  if (isHttp500(e) && /no progress/.test(e.message)) {\n    await sleep(500); // concurrent delete race; retry once\n    await delByCode(code);\n  } else throw e;\n}","preventionTips":["Avoid firing concurrent DELETE /by-code calls for the same stock from multiple clients.","Treat 'made no progress' as a transient race first, a repository bug second (check logs).","Monitor server logs for the 按股票代码删除历史记录失败 traceback when it repeats."],"tags":["database","concurrency","history-api","internal-error"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}