diff --git a/services/nginx/app/modules/selfserve/classes/selfserve_wash_flow.php b/services/nginx/app/modules/selfserve/classes/selfserve_wash_flow.php index af9f7014..cffca074 100644 --- a/services/nginx/app/modules/selfserve/classes/selfserve_wash_flow.php +++ b/services/nginx/app/modules/selfserve/classes/selfserve_wash_flow.php @@ -483,8 +483,8 @@ class selfserve_wash_flow implements selfserve_wash_flow_i if (!$session->markForceStoppedIfOpen($orderId, $eventPayload)) { $summary = $this->getSessionSummary((int)$session->id); } else { - $this->logSessionEvent((int)$session->id, selfserve_wash_event_type::SESSION_FORCE_STOPPED, $eventPayload); - $summary = $this->getSessionSummary((int)$session->id); + $this->logSessionEvent((int)$session->id, selfserve_wash_event_type::SESSION_FORCE_STOPPED, $eventPayload); + $summary = $this->getSessionSummary((int)$session->id); } } diff --git a/services/nginx/app/tests/Unit/Selfserve/SelfserveSessionRaceWiringTest.php b/services/nginx/app/tests/Unit/Selfserve/SelfserveSessionRaceWiringTest.php new file mode 100644 index 00000000..a6e4ce9c --- /dev/null +++ b/services/nginx/app/tests/Unit/Selfserve/SelfserveSessionRaceWiringTest.php @@ -0,0 +1,46 @@ +not->toBeFalse() + ->and($washFlow)->toContain('withSessionMutationLock') + ->and($washFlow)->toContain('sessionMutationLockKey') + ->and($washFlow)->toContain('set_if_absent_with_expiration') + ->and($washFlow)->toContain('GET_LOCK'); + + $syncOffset = strpos($washFlow, 'public function synchronizeSession'); + expect($syncOffset)->not->toBeFalse(); + + $syncMethod = substr($washFlow, (int)$syncOffset, 9000); + $snapshotOffset = strpos($syncMethod, '$snapshot = $this->buildEligibilitySnapshot'); + $lockOffset = strpos($syncMethod, '$mutationResult = $this->withSessionMutationLock'); + $findOffset = strpos($syncMethod, '$session = $this->findLatestOpenSession'); + $addOffset = strpos($syncMethod, '$session = (new selfserve_wash_sessions_o())->add'); + $relayOffset = strpos($syncMethod, '$this->syncMachineRelayFromVisibleServices'); + + expect($snapshotOffset)->not->toBeFalse() + ->and($lockOffset)->not->toBeFalse() + ->and($findOffset)->not->toBeFalse() + ->and($addOffset)->not->toBeFalse() + ->and($relayOffset)->not->toBeFalse() + ->and($snapshotOffset)->toBeLessThan($lockOffset) + ->and($lockOffset)->toBeLessThan($findOffset) + ->and($findOffset)->toBeLessThan($addOffset) + ->and($addOffset)->toBeLessThan($relayOffset); +}); + +it('closes self-serve wash sessions with an atomic open-session guard', function (): void { + $sessionObject = file_get_contents(app_path('objects/selfserve_wash_sessions_o.php')); + $washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php')); + + expect($sessionObject)->not->toBeFalse() + ->and($sessionObject)->toContain('markCompletedIfOpen') + ->and($sessionObject)->toContain('markForceStoppedIfOpen') + ->and($sessionObject)->toContain('AND `completed_at` IS NULL') + ->and($sessionObject)->toContain('AND UPPER(TRIM(`status`)) NOT IN ($terminalStatuses)'); + + expect($washFlow)->not->toBeFalse() + ->and($washFlow)->toContain('if (!$session->markCompletedIfOpen($orderId))') + ->and($washFlow)->toContain('if (!$session->markForceStoppedIfOpen($orderId, $eventPayload))'); +});