Add tests for handling ambiguous timeout errors and deferred relay side effects in Self-serve entrance start logic

- Introduced `SelfserveLaneStartEntranceTimeoutHarness` class and supporting tests to validate ambiguous relay timeout handling during entrance operations.
- Added `defer_relay_side_effects` parameter to `selfserve_lane_command_arguments` for improved relay control during wash start.
- Enhanced lane start routine to support conditional relay side effects and timeout handling with detailed logging.
This commit is contained in:
Jeppe Bundgaard
2026-04-28 17:10:04 +02:00
parent acdff75311
commit 4dd00cd7a2
5 changed files with 212 additions and 8 deletions
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ class selfserve_lane_command_arguments
{
public ?string $license_plate = null;
public ?int $customer_number = null;
public bool $defer_relay_side_effects = false;
/**
* Set the license plate of the vehicle currently in the lane
@@ -24,6 +25,12 @@ class selfserve_lane_command_arguments
return $this;
}
public function setDeferRelaySideEffects(bool $defer_relay_side_effects): self
{
$this->defer_relay_side_effects = $defer_relay_side_effects;
return $this;
}
public function setParameters($params): self
{
if (is_array($params)) {
@@ -33,7 +40,13 @@ class selfserve_lane_command_arguments
if (array_key_exists('customer_number', $params)) {
$this->setCustomerNumber($params['customer_number']);
}
if (array_key_exists('defer_relay_side_effects', $params)) {
$this->setDeferRelaySideEffects(filter_var(
$params['defer_relay_side_effects'],
FILTER_VALIDATE_BOOLEAN
));
}
}
return $this;
}
}
}
@@ -122,6 +122,66 @@ trait selfserve_lane_command_t
}
}
protected function openEntrancePortForWashStart(): void
{
try {
$this->open(selfserve_lane_port::ENTRANCE);
} catch (\Throwable $e) {
if ($this->isAmbiguousGatewayTimeout($e)) {
$this->reportWashStartEntranceTimeout($e);
return;
}
throw $e;
}
}
protected function isAmbiguousGatewayTimeout(\Throwable $e): bool
{
$current = $e;
while ($current !== null) {
$message = strtolower(trim($current->getMessage()));
if (
str_contains($message, 'edge gateway command timed out') ||
str_contains($message, 'command timed out') ||
str_contains($message, 'timed out') ||
str_contains($message, 'timeout')
) {
return true;
}
$current = $current->getPrevious();
}
return false;
}
protected function reportWashStartEntranceTimeout(\Throwable $e): void
{
try {
$laneId = isset($this->id) ? (string)$this->id : 'unknown';
error_log(
'Self-serve START entrance gate dispatch timed out for lane ' .
$laneId .
'; continuing wash start because the gateway command may already have reached the relay: ' .
$e->getMessage()
);
} catch (\Throwable) {
// Diagnostics must not block the user wash start flow.
}
}
protected function runRelaySideEffectsForWashStart(selfserve_lane_command_arguments $arguments): void
{
if ($arguments->defer_relay_side_effects) {
return;
}
// Ensure cleaner relay is enabled whenever wash starts.
$this->turnOnCleanerRelayForWashStart();
// Ensure the machine relay is ON when a wash starts, when it is allowed.
$this->setMachineRelayStatusForWashStart();
}
/**
* Disable relays after STOP in deterministic order:
@@ -293,21 +353,28 @@ trait selfserve_lane_command_t
// Validate customer number
if (!is_numeric($customer_number) || (int)$customer_number <= 0) throw new \InvalidArgumentException("Invalid customer number: " . $customer_number);
if (!(new users_o())->getUserByCustomerNumber((int)$customer_number)->exists()) throw new \InvalidArgumentException("Customer number does not exist: " . $customer_number);
$previous_customer_number = $this->getCustomerNumber();
$previous_license_plate = $this->getLicensePlate();
// Set the customer number and license plate
$this->setCustomerNumber($customer_number);
$this->setLicensePlate($license_plate);
try {
// Open the entrance port before marking the lane occupied. Gateway timeouts are
// ambiguous because the relay may already have received the pulse.
$this->openEntrancePortForWashStart();
} catch (\Throwable $e) {
$this->setCustomerNumber($previous_customer_number);
$this->setLicensePlate($previous_license_plate);
$this->setLaneState(selfserve_lane_state::IDLE);
throw $e;
}
// Set the lane status to OCCUPIED when started
$this->setLaneStatus(selfserve_lane_status::OCCUPIED);
// Set the lane state to IN_WASH
$this->setLaneState(selfserve_lane_state::IN_WASH);
// Open the entrance port
$this->open(selfserve_lane_port::ENTRANCE);
// Start the wash timer
$this->setWashStartTime(time());
// Ensure cleaner relay is enabled whenever wash starts.
$this->turnOnCleanerRelayForWashStart();
// Ensure the machine relay is ON when a wash starts, when it is allowed.
$this->setMachineRelayStatusForWashStart();
$this->runRelaySideEffectsForWashStart($arguments);
// Log the lane start event
$this->logLaneAction(selfserve_lane_log_action::START_WASH);
break;
+7
View File
@@ -8900,6 +8900,13 @@ paths:
license_plate:
type: string
description: Required for START command
customer_number:
type: integer
description: Required for START and RESERVE commands. The authenticated customer's number is applied server-side when omitted by user clients.
defer_relay_side_effects:
type: boolean
default: false
description: For START, open the entrance gate as part of the command but defer cleaner and machine relay activation to explicit relay endpoints.
responses:
'200':
description: Command sent successfully
@@ -0,0 +1,117 @@
<?php
app_require('modules/selfserve/traits/selfserve_lane_command_t.php');
app_require('modules/selfserve/classes/selfserve_lane_command_arguments.php');
app_require('modules/selfserve/helpers/selfserve_lane_port.php');
app_require('modules/selfserve/helpers/selfserve_lane_state.php');
use modules\selfserve\classes\selfserve_lane_command_arguments;
use modules\selfserve\helpers\selfserve_lane_port;
use modules\selfserve\helpers\selfserve_lane_state;
use modules\selfserve\traits\selfserve_lane_command_t;
class SelfserveLaneStartEntranceTimeoutHarness
{
use selfserve_lane_command_t;
public int $id = 771;
/** @var array<int,selfserve_lane_port> */
public array $openCalls = [];
public ?\Throwable $openThrowable = null;
public ?\Throwable $reportedTimeout = null;
public ?selfserve_lane_state $laneState = null;
public int $cleanerRelayCalls = 0;
public int $machineRelayCalls = 0;
public function open(selfserve_lane_port $port, ?int $toggle_after_seconds = null): bool
{
unset($toggle_after_seconds);
$this->openCalls[] = $port;
if ($this->openThrowable !== null) {
throw $this->openThrowable;
}
return true;
}
public function setLaneState(selfserve_lane_state $state): void
{
$this->laneState = $state;
}
protected function reportWashStartEntranceTimeout(\Throwable $e): void
{
$this->reportedTimeout = $e;
}
protected function turnOnCleanerRelayForWashStart(): void
{
$this->cleanerRelayCalls++;
}
protected function setMachineRelayStatusForWashStart(): void
{
$this->machineRelayCalls++;
}
public function runEntranceOpenForStart(): void
{
$this->openEntrancePortForWashStart();
}
public function runStartRelaySideEffects(bool $defer): void
{
$arguments = (new selfserve_lane_command_arguments())->setDeferRelaySideEffects($defer);
$this->runRelaySideEffectsForWashStart($arguments);
}
}
it('continues start when entrance relay dispatch times out ambiguously', function (): void {
$lane = new SelfserveLaneStartEntranceTimeoutHarness();
$timeout = new \RuntimeException('Edge gateway command timed out');
$lane->openThrowable = $timeout;
$lane->runEntranceOpenForStart();
expect($lane->openCalls)->toBe([selfserve_lane_port::ENTRANCE]);
expect($lane->reportedTimeout)->toBe($timeout);
});
it('still fails start for non-timeout entrance relay errors', function (): void {
$lane = new SelfserveLaneStartEntranceTimeoutHarness();
$lane->openThrowable = new \RuntimeException('Invalid relay ID for port ENTRANCE');
expect(fn() => $lane->runEntranceOpenForStart())
->toThrow(\RuntimeException::class, 'Invalid relay ID');
expect($lane->reportedTimeout)->toBeNull();
});
it('parses deferred relay side effects on start command arguments', function (): void {
$arguments = (new selfserve_lane_command_arguments())->setParameters([
'license_plate' => 'ab12345',
'customer_number' => 12345679,
'defer_relay_side_effects' => true,
]);
expect($arguments->license_plate)->toBe('AB12345');
expect($arguments->customer_number)->toBe(12345679);
expect($arguments->defer_relay_side_effects)->toBeTrue();
});
it('skips cleaner and machine relay side effects when start asks to defer them', function (): void {
$lane = new SelfserveLaneStartEntranceTimeoutHarness();
$lane->runStartRelaySideEffects(true);
expect($lane->cleanerRelayCalls)->toBe(0);
expect($lane->machineRelayCalls)->toBe(0);
});
it('keeps cleaner and machine relay side effects for normal start commands', function (): void {
$lane = new SelfserveLaneStartEntranceTimeoutHarness();
$lane->runStartRelaySideEffects(false);
expect($lane->cleanerRelayCalls)->toBe(1);
expect($lane->machineRelayCalls)->toBe(1);
});