From 02534eaca454511e2db53b98210c80edc125d2dc Mon Sep 17 00:00:00 2001 From: corgab Date: Tue, 8 Sep 2026 17:20:53 +0000 Subject: [PATCH 1/2] perf: mirror an unchanged poll status with one conditional update Every non-terminal poll read the quantum_tasks row and saved it back; Eloquent already skipped the UPDATE when nothing changed, but the SELECT ran on each of the up to 720 polls of a queued task. A status-only update is now a single conditional UPDATE keyed on the task ARN and the previous status, so an unchanged poll costs one query and writes no row, and updated_at only moves when the status does. Terminal transitions keep the model path, which also records counts, errors and timestamps. Closes #39 --- README.md | 2 +- src/Jobs/PollQuantumTask.php | 21 ++++++++++++++++--- tests/Feature/PersistenceTest.php | 34 +++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a59f1ac..eeb05c6 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ php artisan migrate Set `AETHER_PERSIST_TASKS=true` in your `.env`. -When enabled, Aether inserts a row into `quantum_tasks` containing the circuit, shots, and driver when a task is dispatched, and updates its `status` and `counts` as the polling job progresses. The `status` always mirrors the backend's real state. Polling problems (like exhaustion or malformed responses) are logged in `error` and `failed_at`. +When enabled, Aether inserts a row into `quantum_tasks` containing the circuit, shots, and driver when a task is dispatched, and updates its `status` and `counts` as the polling job progresses. The `status` always mirrors the backend's real state. Polling problems (like exhaustion or malformed responses) are logged in `error` and `failed_at`. While a task is queued or running, each poll issues a single conditional update that writes nothing when the status is unchanged, so an hour of polling a queued QPU task costs one query per poll and no row churn. Since persistence is strictly best-effort, a database failure never affects queue behaviour or prevents the `CircuitCompleted` event from being emitted. diff --git a/src/Jobs/PollQuantumTask.php b/src/Jobs/PollQuantumTask.php index 9a4c0b2..0836a92 100644 --- a/src/Jobs/PollQuantumTask.php +++ b/src/Jobs/PollQuantumTask.php @@ -126,9 +126,11 @@ public function handle(QuantumManager $manager, Dispatcher $events): void * * The status column always reflects what the backend last reported; our * own polling problems (exhausted budget, malformed response) only ever - * populate error and failed_at. Persistence is best-effort: a database - * failure is reported and swallowed so it can never fail the job or - * suppress the CircuitCompleted event. + * populate error and failed_at. A status-only update, the common case + * while a task is queued or running, is a single conditional query that + * touches no row when the status has not changed. Persistence is + * best-effort: a database failure is reported and swallowed so it can + * never fail the job or suppress the CircuitCompleted event. * * @param array|null $counts */ @@ -139,6 +141,19 @@ private function persist(TaskStatus $status, ?array $counts = null, ?string $err } try { + if ($counts === null && $error === null) { + // An intermediate poll only mirrors the status: one conditional + // UPDATE instead of a SELECT per poll, and no row is written + // (nor updated_at bumped) while the backend reports the same + // status as before. + QuantumTask::query() + ->where('task_arn', $this->taskArn) + ->where('status', '!=', $status->value) + ->update(['status' => $status->value, 'updated_at' => now()]); + + return; + } + $task = QuantumTask::query()->where('task_arn', $this->taskArn)->first(); if ($task === null) { diff --git a/tests/Feature/PersistenceTest.php b/tests/Feature/PersistenceTest.php index 34e4bc4..3532712 100644 --- a/tests/Feature/PersistenceTest.php +++ b/tests/Feature/PersistenceTest.php @@ -135,6 +135,40 @@ expect(QuantumTask::query()->firstOrFail()->status)->toBe(TaskStatus::Running); }); +it('issues a single conditional update and writes nothing when the status is unchanged', function () { + $this->device->snapshotToReturn = new TaskSnapshot(TaskStatus::Running); + $job = ($this->submit)()->withFakeQueueInteractions(); + + ($this->poll)($job); + $before = QuantumTask::query()->firstOrFail(); + $this->travel(5)->seconds(); + + DB::enableQueryLog(); + ($this->poll)($job); + $queries = DB::getQueryLog(); + DB::disableQueryLog(); + + expect($queries)->toHaveCount(1) + ->and(strtolower($queries[0]['query']))->toStartWith('update') + ->and(QuantumTask::query()->firstOrFail()->updated_at->equalTo($before->updated_at))->toBeTrue(); +}); + +it('records a changed intermediate status without reading the row first', function () { + $this->device->snapshotToReturn = new TaskSnapshot(TaskStatus::Queued); + $job = ($this->submit)()->withFakeQueueInteractions(); + ($this->poll)($job); + + $this->device->snapshotToReturn = new TaskSnapshot(TaskStatus::Running); + + DB::enableQueryLog(); + ($this->poll)($job); + $queries = DB::getQueryLog(); + DB::disableQueryLog(); + + expect($queries)->toHaveCount(1) + ->and(QuantumTask::query()->firstOrFail()->status)->toBe(TaskStatus::Running); +}); + it('keeps the backend status and records the error when polling is exhausted', function () { config()->set('aether.max_poll_attempts', 1); $this->device->snapshotToReturn = new TaskSnapshot(TaskStatus::Running); From 2ed5cc0d8f4ca55c7f8edabd5ee617365acf745b Mon Sep 17 00:00:00 2001 From: corgab Date: Tue, 8 Sep 2026 17:24:16 +0000 Subject: [PATCH 2/2] docs: let Eloquent stamp updated_at and state that intermediate mirrors skip model events Builder::update() already adds updated_at, so the explicit value only tied the job to the column name. The docblock and README now say that the status-only update bypasses the QuantumTask model events, while terminal transitions still fire them. --- README.md | 2 +- src/Jobs/PollQuantumTask.php | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eeb05c6..0c746ac 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ php artisan migrate Set `AETHER_PERSIST_TASKS=true` in your `.env`. -When enabled, Aether inserts a row into `quantum_tasks` containing the circuit, shots, and driver when a task is dispatched, and updates its `status` and `counts` as the polling job progresses. The `status` always mirrors the backend's real state. Polling problems (like exhaustion or malformed responses) are logged in `error` and `failed_at`. While a task is queued or running, each poll issues a single conditional update that writes nothing when the status is unchanged, so an hour of polling a queued QPU task costs one query per poll and no row churn. +When enabled, Aether inserts a row into `quantum_tasks` containing the circuit, shots, and driver when a task is dispatched, and updates its `status` and `counts` as the polling job progresses. The `status` always mirrors the backend's real state. Polling problems (like exhaustion or malformed responses) are logged in `error` and `failed_at`. While a task is queued or running, each poll issues a single conditional update that writes nothing when the status is unchanged, so an hour of polling a queued QPU task costs one query per poll and no row churn. Those intermediate updates go straight to the query builder, so `QuantumTask` model observers only see the terminal transition (completed, failed, cancelled, or a polling error); listen to `CircuitCompleted` or query the table for anything earlier. Since persistence is strictly best-effort, a database failure never affects queue behaviour or prevents the `CircuitCompleted` event from being emitted. diff --git a/src/Jobs/PollQuantumTask.php b/src/Jobs/PollQuantumTask.php index 0836a92..17c8417 100644 --- a/src/Jobs/PollQuantumTask.php +++ b/src/Jobs/PollQuantumTask.php @@ -128,7 +128,8 @@ public function handle(QuantumManager $manager, Dispatcher $events): void * own polling problems (exhausted budget, malformed response) only ever * populate error and failed_at. A status-only update, the common case * while a task is queued or running, is a single conditional query that - * touches no row when the status has not changed. Persistence is + * touches no row when the status has not changed and does not fire the + * QuantumTask model events; the terminal transitions do. Persistence is * best-effort: a database failure is reported and swallowed so it can * never fail the job or suppress the CircuitCompleted event. * @@ -145,11 +146,12 @@ private function persist(TaskStatus $status, ?array $counts = null, ?string $err // An intermediate poll only mirrors the status: one conditional // UPDATE instead of a SELECT per poll, and no row is written // (nor updated_at bumped) while the backend reports the same - // status as before. + // status as before. This bypasses the model's events; the + // terminal transitions below still go through save(). QuantumTask::query() ->where('task_arn', $this->taskArn) ->where('status', '!=', $status->value) - ->update(['status' => $status->value, 'updated_at' => now()]); + ->update(['status' => $status->value]); return; }