Handle orphaned edge gateways for non-existent departments and improve error handling

This commit is contained in:
Jeppe Bundgaard
2026-04-27 10:07:48 +02:00
parent 1dffbe16c4
commit 99a85878cd
4 changed files with 166 additions and 3 deletions
File diff suppressed because one or more lines are too long
@@ -41,7 +41,18 @@ class edge_gateway_department_workspace_service
continue;
}
$workspace = $this->buildDepartmentWorkspace($departmentId, false, $departmentRow);
try {
$workspace = $this->buildDepartmentWorkspace($departmentId, false, $departmentRow);
} catch (Exception $exception) {
if ($exception->getMessage() === 'Department not found') {
$this->manager()->deleteOrphanedGatewaysForDepartment($departmentId);
$this->clearDepartmentCache();
continue;
}
throw $exception;
}
$summaries[] = $workspace['summary'];
}
@@ -897,4 +908,19 @@ class edge_gateway_department_workspace_service
{
return $this->manager ?? new edge_gateway_manager();
}
private function clearDepartmentCache(): void
{
try {
if (!defined('redis')) {
return;
}
$redis = constant('redis');
if (is_object($redis) && method_exists($redis, 'clear_departments')) {
$redis->clear_departments();
}
} catch (\Throwable) {
}
}
}
@@ -289,7 +289,16 @@ class edge_gateway_manager
$gatewayIds = [];
foreach ($rows as $row) {
$gatewayId = (int)$row['id'];
$gateway = $this->requireGateway($gatewayId);
try {
$gateway = $this->requireGateway($gatewayId);
} catch (Exception $exception) {
if ($exception->getMessage() === 'Edge gateway not found') {
continue;
}
throw $exception;
}
$gateways[] = $this->buildGatewayPayload($gateway, $includeDetail);
$gatewayIds[] = $gatewayId;
}
@@ -709,6 +718,34 @@ class edge_gateway_manager
];
}
public function deleteOrphanedGatewaysForDepartment(int $departmentId): void
{
if ($departmentId <= 0 || $this->departmentExists($departmentId)) {
return;
}
$rows = (new edge_gateways_o())->getFieldsWhere([
'department_id' => $departmentId,
'deleted_at' => null,
], ['id']);
foreach ($rows as $row) {
$gatewayId = (int)($row['id'] ?? 0);
if ($gatewayId <= 0) {
continue;
}
try {
$gateway = (new edge_gateways_o())->select($gatewayId);
if ($gateway->exists()) {
$this->softDeleteOrphanedGateway($gateway);
}
} catch (\Throwable) {
edge_gateway_view_cache::removeGateway($gatewayId, $departmentId);
}
}
}
/**
* @throws Exception
*/
@@ -2339,9 +2376,47 @@ BASH;
throw new Exception('Edge gateway not found');
}
if (!$this->departmentExists((int)$gateway->department_id->value())) {
$this->softDeleteOrphanedGateway($gateway);
throw new Exception('Edge gateway not found');
}
return $gateway;
}
private function departmentExists(int $departmentId): bool
{
if ($departmentId <= 0) {
return false;
}
try {
return (new departments_o())->select($departmentId)->exists();
} catch (\Throwable) {
return true;
}
}
private function softDeleteOrphanedGateway(edge_gateways_o $gateway): void
{
$gatewayId = (int)$gateway->id;
$departmentId = (int)$gateway->department_id->value();
try {
$this->softDeleteGatewayRelations($gatewayId);
} catch (\Throwable) {
}
try {
if ($gateway->deleted_at->value() === null) {
$gateway->deleted_at->set($this->now());
}
} catch (\Throwable) {
}
edge_gateway_view_cache::removeGateway($gatewayId, $departmentId > 0 ? $departmentId : null);
}
private function softDeleteGatewayRelations(int $gatewayId): void
{
$tables = [
@@ -379,6 +379,68 @@ it('rejects operator edge routes when module permission or department access is
->assertMissingPermissions(['department_access_' . (int)$department['id']]);
});
it('ignores and soft-deletes edge gateways whose department no longer exists', function (): void {
$session = api_fixtures()->createUserSession(['modules_shelly_config']);
$missingDepartmentId = 2147483000;
$listOrphan = api_fixtures()->createClaimedEdgeGateway([
'department_id' => $missingDepartmentId,
'label' => 'Orphaned Fleet Gateway',
]);
api_fixtures()->clearEdgeGatewayViewCache();
$listResponse = api_client()->get('/edge-gateways', $session['headers']);
$listResponse
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
expect(collect_gateway_ids_from_api_response($listResponse->data()))
->not->toContain((int)$listOrphan['id'])
->and(api_fixtures()->fetchRowById('edge_gateways', (int)$listOrphan['id'])['deleted_at'] ?? null)
->not->toBeNull();
$workspaceMissingDepartmentId = $missingDepartmentId - 1;
$workspaceOrphan = api_fixtures()->createClaimedEdgeGateway([
'department_id' => $workspaceMissingDepartmentId,
'label' => 'Orphaned Workspace Gateway',
]);
api_fixtures()->clearEdgeGatewayViewCache();
$redis = api_test_runtime()->redis();
expect($redis)->not->toBeNull();
$redis->set('departments', json_encode([[
'id' => $workspaceMissingDepartmentId,
'name' => 'Stale Department Cache',
'description' => 'This department row is no longer in the database.',
'order_priority' => 0,
'visible' => 1,
]], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
try {
$workspaceResponse = api_client()->get(
'/modules/edge-gateways/workspace/departments',
$session['headers']
);
} finally {
$redis->del(['departments']);
}
$workspaceResponse
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
expect($workspaceResponse->data())
->toBeArray()
->and(collect_gateway_ids_from_api_response($workspaceResponse->data()))
->not->toContain((int)$workspaceOrphan['id'])
->and(api_fixtures()->fetchRowById('edge_gateways', (int)$workspaceOrphan['id'])['deleted_at'] ?? null)
->not->toBeNull();
});
function edge_operator_test_inventory(string $suffix): array
{
return [[