runs = $runs; $this->tasks = $tasks; $this->findings = $findings; $this->files = $files; $this->builder = $builder; $this->analyzers = $analyzers; $this->registry = $registry; $this->coordinator = $coordinator; $this->transaction = $transaction; } public function run_one( string $run_uuid ): array { $run = $this->runs->get_by_uuid( $run_uuid ); if ( null === $run ) { throw new StorageException( 'run_not_found' ); } if ( RunStatus::RUNNING !== $run->status() ) { return $this->result( 'idle', $run->status(), null ); } $lease = $this->tasks->acquire_next( $run->id(), 60 ); if ( null === $lease ) { $settled = $this->coordinator->settle( $run_uuid ); return $this->result( 'idle', $settled->status(), null ); } if ( ! in_array( $lease->check_id(), $this->analyzers->ids(), true ) ) { $this->tasks->fail( $lease, 'checker_capability_denied' ); $settled = $this->coordinator->settle( $run_uuid ); return $this->result( 'failed', $settled->status(), $lease->check_id() ); } $started = microtime( true ); $memory_start = memory_get_usage( true ); try { $this->transaction->begin(); $state = self::SNAPSHOT_TASK_ID === $lease->check_id() ? $this->run_snapshot( $run->id(), $lease, $started, $memory_start ) : $this->run_analyzer( $run->id(), $lease, $started, $memory_start ); $this->transaction->commit(); } catch ( Throwable $exception ) { $this->transaction->rollback(); $this->files->clear_pending( $run->id(), 'run_failed' ); $this->tasks->fail( $lease, $exception instanceof BudgetExceeded ? 'checker_budget_exceeded' : 'checker_failed' ); $settled = $this->coordinator->settle( $run_uuid ); return $this->result( 'failed', $settled->status(), $lease->check_id() ); } $settled = $this->coordinator->settle( $run_uuid ); return $this->result( $state, $settled->status(), $lease->check_id() ); } private function run_snapshot( int $run_id, TaskLease $lease, float $started, int $memory_start ): string { $task_cursor = $this->normalize_task_cursor( $lease->cursor() ); $chunk = $this->builder->process_one( $run_id, $task_cursor['builder'] ); $usage = $this->measured_usage( $chunk->usage(), $started, $memory_start ); $next_usage = $this->add_usage( $task_cursor['usage'], $usage ); $within = $this->within_budget( $this->registry->local_budget_for( self::SNAPSHOT_TASK_ID ), $next_usage ); $cursor = array( 'version' => 1, 'builder' => $chunk->cursor(), 'usage' => $next_usage, ); $this->runs->consume_budget( $run_id, $usage ); if ( 'active' === $chunk->state() && $within ) { if ( ! $this->tasks->checkpoint_progress( $lease, $cursor ) ) { throw new StorageException( 'lease_expired_before_checkpoint' ); } return 'progress'; } if ( ! $within ) { $this->files->clear_pending( $run_id, 'code_budget_exhausted' ); $cursor['budget_exhausted'] = true; } $finding = $this->analyzers->analyze( self::SNAPSHOT_TASK_ID, $run_id, $this->timestamp() ); $this->findings->save( $run_id, $finding ); if ( ! $this->tasks->complete( $lease, $cursor ) ) { throw new StorageException( 'lease_expired_before_completion' ); } return 'completed'; } private function run_analyzer( int $run_id, TaskLease $lease, float $started, int $memory_start ): string { $finding = $this->analyzers->analyze( $lease->check_id(), $run_id, $this->timestamp() ); $encoded = wp_json_encode( $finding, JSON_UNESCAPED_SLASHES ); if ( false === $encoded ) { throw new StorageException( 'finding_encode_failed' ); } $usage = $this->measured_usage( array( 'milliseconds' => 0, 'items' => max( 1, (int) $finding['coverage']['observed'] ), 'requests' => 0, 'bytes' => strlen( $encoded ), ), $started, $memory_start ); if ( ! $this->within_budget( $this->registry->local_budget_for( $lease->check_id() ), $usage ) ) { throw new BudgetExceeded( 'analyzer' ); } $this->runs->consume_budget( $run_id, $usage ); $this->findings->save( $run_id, $finding ); if ( ! $this->tasks->complete( $lease, array( 'finding_count' => 1 ) ) ) { throw new StorageException( 'lease_expired_before_completion' ); } return 'completed'; } /** * @param array $cursor Task cursor. * @return array{version:int,builder:array,usage:array{milliseconds:int,items:int,requests:int,bytes:int}} */ private function normalize_task_cursor( array $cursor ): array { if ( array() === $cursor ) { return array( 'version' => 1, 'builder' => array(), 'usage' => $this->zero_usage(), ); } if ( ! isset( $cursor['version'], $cursor['builder'], $cursor['usage'] ) || 1 !== $cursor['version'] || ! is_array( $cursor['builder'] ) || ! is_array( $cursor['usage'] ) ) { throw new InvalidArgumentException( 'invalid_code_task_cursor' ); } foreach ( $this->zero_usage() as $dimension => $zero ) { if ( ! isset( $cursor['usage'][ $dimension ] ) || ! is_int( $cursor['usage'][ $dimension ] ) || $cursor['usage'][ $dimension ] < $zero ) { throw new InvalidArgumentException( 'invalid_code_task_cursor' ); } } return array( 'version' => 1, 'builder' => $cursor['builder'], 'usage' => $cursor['usage'], ); } /** * @param array{milliseconds:int,items:int,requests:int,bytes:int} $usage Declared usage. * @return array{milliseconds:int,items:int,requests:int,bytes:int} */ private function measured_usage( array $usage, float $started, int $memory_start ): array { $usage['milliseconds'] = max( $usage['milliseconds'], (int) ceil( ( microtime( true ) - $started ) * 1000 ) ); $usage['bytes'] = max( $usage['bytes'], max( 0, memory_get_usage( true ) - $memory_start ) ); return $usage; } /** * @param array{milliseconds:int,items:int,requests:int,bytes:int} $left Left. * @param array{milliseconds:int,items:int,requests:int,bytes:int} $right Right. * @return array{milliseconds:int,items:int,requests:int,bytes:int} */ private function add_usage( array $left, array $right ): array { foreach ( $left as $dimension => $amount ) { $left[ $dimension ] = $amount + $right[ $dimension ]; } return $left; } /** @param array{milliseconds:int,items:int,requests:int,bytes:int} $usage Usage. */ private function within_budget( Budget $budget, array $usage ): bool { try { foreach ( $usage as $dimension => $amount ) { $budget = $budget->consume( $dimension, $amount ); } return true; } catch ( BudgetExceeded $exception ) { return false; } } /** @return array{milliseconds:int,items:int,requests:int,bytes:int} */ private function zero_usage(): array { return array( 'milliseconds' => 0, 'items' => 0, 'requests' => 0, 'bytes' => 0, ); } private function timestamp(): string { return mysql_to_rfc3339( current_time( 'mysql', true ) ); } /** @return array{state:string,run_status:string,check_id:string|null} */ private function result( string $state, string $run_status, ?string $check_id ): array { return array( 'state' => $state, 'run_status' => $run_status, 'check_id' => $check_id, ); } }