0 ? 'manual' : 'cron'; $deactivateDays = $this->settingsGateway->getUserInactivityDeactivateDays(); $deleteDays = $this->settingsGateway->getUserInactivityDeleteDays(); if ($deactivateDays <= 0) { $deleteDays = 0; } $privilegedUserIds = $this->userReadRepository->listPrivilegedUserIdsByPermissionKeys([ PermissionService::SETTINGS_UPDATE, PermissionService::TENANTS_UPDATE, ]); $result = [ 'ok' => true, 'locked' => false, 'error' => null, 'run_uuid' => $runUuid, 'deactivated_count' => 0, 'deleted_count' => 0, 'skipped_count' => 0, 'skipped_privileged_count' => count($privilegedUserIds), 'policy' => [ 'deactivate_days' => $deactivateDays, 'delete_days' => $deleteDays, ], 'duration_ms' => 0, ]; if (!$this->acquireLock()) { $result['ok'] = false; $result['locked'] = true; $result['error'] = 'lock_not_acquired'; $result['duration_ms'] = $this->durationMs($startedAt); return $result; } try { if ($deactivateDays > 0) { while (true) { $ids = $this->userReadRepository->listIdsForAutoDeactivate($deactivateDays, $privilegedUserIds, self::BATCH_SIZE); if (!$ids) { break; } $usersById = []; foreach ($ids as $id) { $user = $this->userReadRepository->find((int) $id); if ($user) { $usersById[(int) $id] = $user; } } $updated = $this->userWriteRepository->setInactiveByIds( $ids, $actorUserId && $actorUserId > 0 ? $actorUserId : null ); if ($updated <= 0) { $result['ok'] = false; $result['error'] = 'deactivate_failed'; break; } $result['deactivated_count'] += $updated; $this->userWriteRepository->bumpAuthzVersionByUserIds($ids); foreach ($ids as $id) { if (!isset($usersById[(int) $id])) { continue; } $this->userLifecycleAuditService->logDeactivate( $runUuid, $triggerType, $result['policy'], $actorUserId, $usersById[(int) $id], 'success', null ); } if (count($ids) < self::BATCH_SIZE) { break; } } } if ($deleteDays > 0) { while (true) { $ids = $this->userReadRepository->listIdsForAutoDelete($deleteDays, $privilegedUserIds, self::BATCH_SIZE); if (!$ids) { break; } foreach ($ids as $id) { $user = $this->userReadRepository->find((int) $id); if (!$user) { continue; } $auditId = $this->userLifecycleAuditService->logDeleteWithSnapshot( $runUuid, $triggerType, $result['policy'], $actorUserId, $user ); if (!$auditId) { $result['skipped_count']++; $this->userLifecycleAuditService->logDeleteFailure( $runUuid, $triggerType, $result['policy'], $actorUserId, $user, 'audit_snapshot_failed' ); continue; } $deleted = $this->userWriteRepository->deleteByIds([(int) $id]); if ($deleted <= 0) { $result['skipped_count']++; $this->userLifecycleAuditService->updateStatus((int) $auditId, 'failed', 'delete_failed'); continue; } $result['deleted_count'] += $deleted; } if (count($ids) < self::BATCH_SIZE) { break; } } } } catch (\Throwable $exception) { $result['ok'] = false; $result['error'] = 'unexpected_error'; } finally { $this->releaseLock(); $result['duration_ms'] = $this->durationMs($startedAt); } return $result; } private function acquireLock(): bool { $gotLock = DB::selectValue('select GET_LOCK(?, 0) as got_lock', self::LOCK_NAME); return (int) $gotLock === 1; } private function releaseLock(): void { try { DB::selectValue('select RELEASE_LOCK(?) as released_lock', self::LOCK_NAME); } catch (\Throwable $exception) { // no-op } } private function durationMs(float $startedAt): int { return (int) max(0, round((microtime(true) - $startedAt) * 1000)); } }