|
|
@@ -424,11 +424,40 @@ namespace ClankerSaddle.Services {
|
|
|
return null;
|
|
|
}
|
|
|
if (((!)task).status != status) {
|
|
|
- ((!)task).status = status;
|
|
|
- if (status == STATUS_DONE && ((!)task).completed_at == null) {
|
|
|
- ((!)task).completed_at = new DateTime.now_local();
|
|
|
+ if (status != STATUS_DONE && status != STATUS_FAILED) {
|
|
|
+ // Terminal states are sticky (plan 13 B2's
|
|
|
+ // invariant): a NON-terminal transition must never
|
|
|
+ // downgrade a done/failed task. The entity writes
|
|
|
+ // below interleave with completions on the main
|
|
|
+ // loop (the terminal checks in the callers have
|
|
|
+ // already yielded by then — a late 'working' push
|
|
|
+ // racing a 'done' push used to resurrect the task
|
|
|
+ // as working AFTER its session closed done), so
|
|
|
+ // the guard lives in the UPDATE's WHERE: atomic.
|
|
|
+ // The row is re-read afterwards so the caller sees
|
|
|
+ // the truth (terminal included), never a stale
|
|
|
+ // entity it might write back.
|
|
|
+ var guard = connection.create_command(
|
|
|
+ "UPDATE tasks SET status = :status WHERE id = :id"
|
|
|
+ + " AND status NOT IN ('done', 'failed')");
|
|
|
+ guard.with_parameter<Invercargill.Element>("status",
|
|
|
+ elem(status));
|
|
|
+ guard.with_parameter<Invercargill.Element>("id",
|
|
|
+ elem<int64?>(id));
|
|
|
+ yield guard.execute_non_query_async();
|
|
|
+ task = yield db.query<TaskEntity>()
|
|
|
+ .where(expr("id == $0", elem<int64?>(id)))
|
|
|
+ .first_async();
|
|
|
+ if (task == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ((!)task).status = status;
|
|
|
+ if (status == STATUS_DONE && ((!)task).completed_at == null) {
|
|
|
+ ((!)task).completed_at = new DateTime.now_local();
|
|
|
+ }
|
|
|
+ yield db.update_async<TaskEntity>((!)task);
|
|
|
}
|
|
|
- yield db.update_async<TaskEntity>(task);
|
|
|
yield pulse.bump(id, @"task $(status)");
|
|
|
}
|
|
|
return task;
|
|
|
@@ -853,9 +882,19 @@ namespace ClankerSaddle.Services {
|
|
|
.first_async();
|
|
|
if (task != null && (((!)task).status == STATUS_ASSIGNED || ((!)task).status == STATUS_WORKING
|
|
|
|| ((!)task).status == STATUS_BLOCKED || ((!)task).status == STATUS_REVIEW_REQUESTED)) {
|
|
|
- ((!)task).status = block.block_type == BLOCK_REVIEW
|
|
|
- ? STATUS_REVIEW_REQUESTED : STATUS_BLOCKED;
|
|
|
- yield db.update_async<TaskEntity>((!)task);
|
|
|
+ // The park flip is a guarded update: a completion
|
|
|
+ // interleaving the raise keeps its terminal state (the
|
|
|
+ // read above already yielded — the entity is stale by
|
|
|
+ // write time under load; plan 13 B2's sticky rule).
|
|
|
+ var guard = connection.create_command(
|
|
|
+ "UPDATE tasks SET status = :status WHERE id = :id"
|
|
|
+ + " AND status IN ('assigned', 'working', 'blocked', 'review_requested')");
|
|
|
+ guard.with_parameter<Invercargill.Element>("status",
|
|
|
+ elem(block.block_type == BLOCK_REVIEW
|
|
|
+ ? STATUS_REVIEW_REQUESTED : STATUS_BLOCKED));
|
|
|
+ guard.with_parameter<Invercargill.Element>("id",
|
|
|
+ elem<int64?>(task_id));
|
|
|
+ yield guard.execute_non_query_async();
|
|
|
}
|
|
|
yield pulse.bump(task_id, "block raised");
|
|
|
// Web Push (plan 11 task E): every raised block notifies all
|