$paths Candidate files in the caller's oldest-first order. * @param array $knownFailingIds Cross-journal failed request IDs. * @return array{paths: array, manifest: array} */ public static function select(array $paths, array $knownFailingIds = array()): array { $files = self::classifyFiles($paths, $knownFailingIds); $knownFailureFiles = 0; $serverFailureFiles = 0; $classificationIssueFiles = 0; $pinnedFiles = 0; foreach ($files as $file) { if ($file['knownFailure']) { $knownFailureFiles++; } if ($file['serverFailure']) { $serverFailureFiles++; } if ($file['classificationIssues'] !== array()) { $classificationIssueFiles++; } if ($file['pinned']) { $pinnedFiles++; } } $selectedIndexes = self::selectedIndexes($files, $pinnedFiles); return self::selectionResult( $files, $selectedIndexes, $knownFailureFiles, $serverFailureFiles, $classificationIssueFiles, $pinnedFiles ); } /** * @param array $paths * @param array $knownFailingIds * @return array, knownFailure: bool, serverFailure: bool, * classificationIssues: array, pinned: bool, * existenceProven: bool}> */ private static function classifyFiles(array $paths, array $knownFailingIds): array { $files = self::oldestFirstExistingFiles($paths); $requestGroups = array(); foreach ($files as $index => $file) { $facts = self::requestFactsInFile( $file['path'], $knownFailingIds, $requestGroups, $file['preclassificationIssues'] ?? array() ); $files[$index]['requestIds'] = $facts['requestIds']; $files[$index]['knownFailure'] = $facts['knownFailure']; $files[$index]['classificationIssues'] = array_values(array_unique(array_merge( $file['preclassificationIssues'] ?? array(), $facts['classificationIssues'] ))); } $serverFailingIds = array(); foreach ($requestGroups as $requestId => $group) { if ($group->isFailing()) { $serverFailingIds[(string)$requestId] = true; } } foreach ($files as $index => $file) { $serverFailure = false; foreach ($file['requestIds'] as $requestId) { if (isset($serverFailingIds[$requestId])) { $serverFailure = true; break; } } $files[$index]['serverFailure'] = $serverFailure; $files[$index]['pinned'] = $file['knownFailure'] || $serverFailure || $file['classificationIssues'] !== array(); } return $files; } /** * @param array $files * @return array */ private static function selectedIndexes(array $files, int $pinnedFiles): array { $ordinarySlots = max(0, self::MAX_RECENT_FILES - $pinnedFiles); $ordinaryIndexes = array(); foreach ($files as $index => $file) { if (!$file['pinned']) { $ordinaryIndexes[] = $index; } } $keptOrdinary = $ordinarySlots === 0 ? array() : array_slice($ordinaryIndexes, -$ordinarySlots); $selectedIndexes = array_fill_keys($keptOrdinary, true); foreach ($files as $index => $file) { if ($file['pinned']) { $selectedIndexes[$index] = true; } } return $selectedIndexes; } /** * @param array, * classificationIssues: array, existenceProven: bool}> $files * @param array $selectedIndexes * @return array{paths: array, manifest: array} */ private static function selectionResult( array $files, array $selectedIndexes, int $knownFailureFiles, int $serverFailureFiles, int $classificationIssueFiles, int $pinnedFiles ): array { $selectedPaths = array(); $existingFiles = 0; $droppedNames = array(); $droppedRequestIds = array(); $seenDroppedRequestIds = array(); $classificationIssues = array(); foreach ($files as $index => $file) { if (($file['existenceProven'] ?? false) === true) { $existingFiles++; } if ($file['classificationIssues'] !== array()) { $classificationIssues[] = array( 'name' => basename($file['path']), 'reasons' => $file['classificationIssues'], ); } if (isset($selectedIndexes[$index])) { $selectedPaths[] = $file['path']; continue; } $droppedNames[] = basename($file['path']); foreach ($file['requestIds'] as $requestId) { if (!isset($seenDroppedRequestIds[$requestId])) { $seenDroppedRequestIds[$requestId] = true; $droppedRequestIds[] = $requestId; } } } return array( 'paths' => $selectedPaths, 'manifest' => array( 'policy' => 'failure_and_uncertain_evidence_then_newest', 'max_recent_files' => self::MAX_RECENT_FILES, 'existing_files' => $existingFiles, 'selected_files' => count($selectedPaths), 'known_failure_files' => $knownFailureFiles, 'server_failure_files' => $serverFailureFiles, 'classification_issue_files' => $classificationIssueFiles, 'pinned_files' => $pinnedFiles, 'classification_issues' => array_slice( $classificationIssues, 0, self::MAX_MANIFEST_CLASSIFICATION_ISSUES), 'classification_issues_omitted' => max( 0, count($classificationIssues) - self::MAX_MANIFEST_CLASSIFICATION_ISSUES), 'dropped_files' => count($droppedNames), 'dropped_file_names' => array_slice( $droppedNames, 0, self::MAX_MANIFEST_DROPPED_FILE_NAMES), 'dropped_file_names_omitted' => max( 0, count($droppedNames) - self::MAX_MANIFEST_DROPPED_FILE_NAMES), 'dropped_request_ids' => array_slice( $droppedRequestIds, 0, self::MAX_MANIFEST_DROPPED_REQUEST_IDS), 'dropped_request_ids_omitted' => max( 0, count($droppedRequestIds) - self::MAX_MANIFEST_DROPPED_REQUEST_IDS), ), ); } /** * Existing paths ordered by mtime, with caller order breaking ties. * * @param array $paths * @return array */ private static function oldestFirstExistingFiles(array $paths): array { $files = array(); foreach (array_values($paths) as $order => $path) { $fileResult = ABJ_404_Solution_HostFilesystemCall::isFile($path); if ($fileResult['warning'] !== null) { self::reportFailure('Diagnostic journal file check warning: ' . $path . ' (' . $fileResult['warning'] . ')'); $files[] = array( 'path' => $path, 'modified' => 0, 'order' => $order, 'preclassificationIssues' => array('is_file_warning'), 'existenceProven' => false, ); continue; } if (!$fileResult['result']) { continue; } $modifiedResult = ABJ_404_Solution_HostFilesystemCall::filemtime($path); if ($modifiedResult['warning'] !== null) { self::reportFailure('Diagnostic journal mtime warning: ' . $path . ' (' . $modifiedResult['warning'] . ')'); $files[] = array( 'path' => $path, 'modified' => 0, 'order' => $order, 'preclassificationIssues' => array('filemtime_warning'), 'existenceProven' => true, ); continue; } $modified = $modifiedResult['result']; if (is_int($modified)) { $files[] = array( 'path' => $path, 'modified' => $modified, 'order' => $order, 'preclassificationIssues' => array(), 'existenceProven' => true, ); } } usort($files, static function (array $left, array $right): int { if ($left['modified'] === $right['modified']) { return $left['order'] <=> $right['order']; } return $left['modified'] <=> $right['modified']; }); return $files; } /** * Fold one file into the shared request lifecycle index. * * @param array $knownFailingIds * @param array $requestGroups * @param list $preclassificationIssues * @return array{requestIds: array, knownFailure: bool, * classificationIssues: array} */ private static function requestFactsInFile( string $path, array $knownFailingIds, array &$requestGroups, array $preclassificationIssues = array() ): array { if ($preclassificationIssues !== array()) { return array( 'requestIds' => array(), 'knownFailure' => false, 'classificationIssues' => $preclassificationIssues, ); } $openResult = ABJ_404_Solution_HostFilesystemCall::fopen($path, 'rb'); if ($openResult['warning'] !== null) { self::reportFailure('Diagnostic journal open warning: ' . $path . ' (' . $openResult['warning'] . ')'); if (is_resource($openResult['result'])) { $closeResult = ABJ_404_Solution_HostFilesystemCall::fclose($openResult['result']); if ($closeResult['warning'] !== null) { self::reportFailure('Diagnostic journal close warning: ' . $path . ' (' . $closeResult['warning'] . ')'); } } return array( 'requestIds' => array(), 'knownFailure' => false, 'classificationIssues' => array('fopen_warning'), ); } $handle = $openResult['result']; if (!is_resource($handle)) { self::reportFailure('Diagnostic journal could not be scanned: ' . $path); return array( 'requestIds' => array(), 'knownFailure' => false, 'classificationIssues' => array('unreadable'), ); } $requestIds = array(); $knownFailure = false; $classificationIssues = array(); $nonEmptyLines = 0; try { while (true) { $lineResult = ABJ_404_Solution_HostFilesystemCall::fgets($handle); if ($lineResult['warning'] !== null) { self::reportFailure('Diagnostic journal read warning: ' . $path . ' (' . $lineResult['warning'] . ')'); $classificationIssues['fgets_warning'] = true; break; } $line = $lineResult['result']; if ($line === false) { break; } if (trim($line) === '') { continue; } $nonEmptyLines++; $record = json_decode($line, true); if (!is_array($record)) { $classificationIssues['invalid_json'] = true; continue; } $requestId = is_scalar($record['request_id'] ?? null) ? (string)$record['request_id'] : ''; if ($requestId === '') { $classificationIssues['missing_request_id'] = true; continue; } $requestIds[$requestId] = true; if (isset($knownFailingIds[$requestId])) { $knownFailure = true; } if (!isset($requestGroups[$requestId])) { $requestGroups[$requestId] = new ABJ_404_Solution_DiagnosticRequestGroup($requestId, true); } $requestGroups[$requestId]->applyRecord($record); } } finally { $closeResult = ABJ_404_Solution_HostFilesystemCall::fclose($handle); if ($closeResult['warning'] !== null) { self::reportFailure('Diagnostic journal close warning: ' . $path . ' (' . $closeResult['warning'] . ')'); $classificationIssues['fclose_warning'] = true; } } if ($nonEmptyLines === 0) { $classificationIssues['no_records'] = true; } return array( 'requestIds' => array_keys($requestIds), 'knownFailure' => $knownFailure, 'classificationIssues' => array_keys($classificationIssues), ); } private static function reportFailure(string $message): void { abj404_logPhpFallback('ajax-trace', $message); } }