Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 21 additions & 0 deletions docs/task4-compatibility-boundary-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 任务四:提交失败恢复与可观测性证据

## 问题/目标

`POST /api/submit` 的旧调用仍使用 `code`、`assignment_id`,默认语言仍为 `cpp`。同步评估器抛出异常时,提交已经以 `pending` 写入数据库;目标是在不改变旧响应契约的前提下,保证记录可恢复为 `failed`,并留下可定位日志。

## 复现与证据

在测试中用 `patch('routes.api.evaluate_cpp_code', side_effect=RuntimeError(...))` 调用真实 `/api/submit` 路由。修复前异常分支没有回滚会话,也没有保存异常原因;这会让失败记录缺少恢复信息。测试验证 HTTP 仍为 `500`,同时检查数据库状态和应用日志。

## 计划改动

只修改 `routes/api.py:submit_code` 的同步评估异常分支:先 `db.session.rollback()`,再将同一提交标记为 `failed` 并保存反馈,最后用 `current_app.logger.exception` 记录 `submission_id`。不改请求字段、成功响应、错误状态码、队列接口或数据库结构。

## 学习总结

人工排查沿着 `pending -> evaluated/failed` 状态路径追踪,并把“评估器异常后是否可恢复”写成可证伪假设。拒绝统一错误码、幂等键和异步队列重构等过度设计,因为它们会改变公开契约,不能由当前故障证明必要。

## 验证结果

`python -m pytest tests/test_task4_submission_failure_recovery.py -q`:`1 passed`。回归测试覆盖旧请求字段、500 响应、`failed` 状态、异常反馈和带提交 ID 的应用日志。
9 changes: 7 additions & 2 deletions routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,10 +813,15 @@ def submit_code():
)

except Exception as e:
current_app.logger.exception('评估代码失败')

# 评估可能在事务中途失败,先回滚会话再保存可恢复的失败状态。
db.session.rollback()
submission.status = 'failed'
submission.feedback = f'代码评估失败: {str(e)}'
db.session.commit()
current_app.logger.exception(
'评估代码失败,提交已标记为 failed (submission_id=%s)',
submission.id,
)

return error_response("代码评估失败,请稍后重试", 500)

Expand Down
57 changes: 57 additions & 0 deletions tests/test_task4_submission_failure_recovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import os
import tempfile
from unittest.mock import patch

from app import create_app
from models import Assignment, Submission, User, db


def test_sync_submission_failure_is_persisted_and_observable(caplog):
fd, database_path = tempfile.mkstemp()
app = create_app('testing')
app.config.update(
SQLALCHEMY_DATABASE_URI=f'sqlite:///{database_path}',
TESTING=True,
SUBMISSION_EVALUATION_QUEUE_BACKEND='thread',
)
client = app.test_client()
try:
with app.app_context():
db.drop_all()
db.create_all()
student = User(
student_id='task4-student',
username='task4-student',
usertype='学生',
full_name='任务四学生',
)
student.password = 'password'
db.session.add(student)
assignment = Assignment(title='任务四作业', creator_id=student.student_id)
db.session.add(assignment)
db.session.commit()
assignment_id = assignment.id

client.post('/login', data={'username': 'task4-student', 'password': 'password'})
with patch(
'routes.api.evaluate_cpp_code',
side_effect=RuntimeError('temporary evaluator outage'),
):
response = client.post(
'/api/submit',
json={'code': 'int main() { return 0; }', 'assignment_id': assignment_id},
)

assert response.status_code == 500
assert response.get_json()['success'] is False
with app.app_context():
submission = Submission.query.one()
assert submission.status == 'failed'
assert 'temporary evaluator outage' in submission.feedback
assert 'submission_id=' in caplog.text
finally:
with app.app_context():
db.session.remove()
db.drop_all()
os.close(fd)
os.unlink(database_path)
Loading