diff --git a/.env b/.env new file mode 100644 index 00000000..949df516 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +COMPOSE_FILE=compose.yaml:compose.dev.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..68c86bf2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI Pipeline + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + workflow_dispatch: + inputs: + run_long_tests: + description: 'Run long tests' + required: false + default: false + type: boolean + +permissions: + contents: read + +jobs: + setup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Build with cache + run: | + docker buildx create --use + docker buildx build \ + --cache-from type=gha \ + --cache-to type=gha,mode=max \ + --load \ + --tag recruiter-php \ + . + + test-fast: + needs: setup + runs-on: ubuntu-latest + env: + COMPOSE_FILE: compose.yaml + steps: + - uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Restore cache and build + run: | + docker buildx create --use + docker buildx build \ + --cache-from type=gha \ + --load \ + --tag recruiter-php \ + . + - name: Run fast tests + run: make test + + test-long: + needs: setup + runs-on: ubuntu-latest + timeout-minutes: 90 + if: github.event.inputs.run_long_tests == 'true' + env: + COMPOSE_FILE: compose.yaml + steps: + - uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Restore cache and build + run: | + docker buildx create --use + docker buildx build \ + --cache-from type=gha \ + --load \ + --tag recruiter-php \ + . + - name: Run long tests + run: make test-long diff --git a/.gitignore b/.gitignore index ff2779ce..01beab6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -.* +.*.cache +phpunit.xml composer.phar composer.lock vendor/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 00000000..d1055d88 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,25 @@ +setRiskyAllowed(true) + ->setRules([ + '@PSR12' => true, + '@Symfony' => true, + 'array_indentation' => true, + 'array_syntax' => ['syntax' => 'short'], + 'concat_space' => ['spacing' => 'one'], + /* 'declare_strict_types' => true, */ + 'string_implicit_backslashes' => true, + 'list_syntax' => ['syntax' => 'short'], + 'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'], + 'ordered_imports' => true, + 'phpdoc_to_comment' => false, + 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']], + 'visibility_required' => ['elements' => ['property', 'method', 'const']], + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/spec') + ) +; diff --git a/Dockerfile b/Dockerfile index 7214a211..2f486df5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,41 @@ -FROM php:7.2-cli +FROM php:8.4-cli AS base -RUN apt-get update \ - && apt-get install -y mongodb git +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + unzip \ + libssl-dev \ + libcurl4-openssl-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* +# Install MongoDB extension RUN pecl install mongodb \ - && docker-php-ext-install bcmath pdo_mysql mbstring opcache pcntl \ - && docker-php-ext-enable mongodb + && docker-php-ext-enable mongodb \ + && docker-php-ext-install -j$(nproc) \ + bcmath \ + pdo_mysql \ + opcache \ + pcntl -RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer && composer global require hirak/prestissimo --no-plugins --no-scripts +# Copy Composer from official image +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer +# Set working directory WORKDIR /app -COPY . /app +FROM base AS code -RUN composer install +# Set environment variable for Composer +ENV COMPOSER_ALLOW_SUPERUSER=1 -ENTRYPOINT /etc/init.d/mongodb start && vendor/bin/phpunit +# Copy composer files +COPY composer.json composer.lock* ./ + +# Install dependencies including dev dependencies for testing +RUN composer install --optimize-autoloader + +# Copy application code +COPY . . + +CMD ["tail", "-f", "/dev/null"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..df96bd4d --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +.PHONY: build up down test test-long phpstan rector fix-cs install update shell logs clean + +# Build the Docker image +build: + docker compose build + +# Start the services +up: + docker compose up -d + +# Stop the services +down: + docker compose down + +# Install dependencies +install: + docker compose run --rm php composer install + +# Update dependencies +update: + docker compose run --rm php composer update + +# Run all tests except the long ones +test: up + docker compose exec php vendor/bin/phpunit --exclude-group=long + +# Run long tests specifically +test-long: up + docker compose exec php vendor/bin/phpunit --group=long + +phpstan: up + docker compose exec php vendor/bin/phpstan --memory-limit=2G + +rector: up + docker compose exec php vendor/bin/rector + +fix-cs: up + docker compose exec php vendor/bin/php-cs-fixer fix -v + +# Open a shell in the PHP container +shell: + docker compose exec php bash + +# View logs +logs: + docker compose logs -f php + +# Clean up containers and volumes +clean: + docker compose down -v + docker compose rm -f diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 00000000..e985c6b9 --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,4 @@ +services: + php: + volumes: + - .:/app diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 00000000..e0724f21 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,19 @@ +services: + php: + build: . + working_dir: /app + environment: + - COMPOSER_ALLOW_SUPERUSER=1 + - MONGODB_URI=mongodb://mongodb:27017/recruiter + depends_on: + - mongodb + + mongodb: + image: mongo:8 + container_name: recruiter_mongodb + restart: unless-stopped + volumes: + - mongodb_data:/data/db + +volumes: + mongodb_data: diff --git a/composer.json b/composer.json index 95fb02e3..3953b885 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,7 @@ { "name": "recruiterphp/recruiter", "description": "Job Queue Manager: high performance, high volume, persistent, fault tolerant. 100% PHP/MongoDB, 100% Awesome", + "license": "MIT", "type": "project", "keywords": [ "job", @@ -13,8 +14,6 @@ "manager", "mongodb" ], - "homepage": "https://github.com/recruiterphp/recruiter", - "license": "MIT", "authors": [ { "name": "gabriele.lana", @@ -25,44 +24,57 @@ "homepage": "https://github.com/recruiterphp/recruiter/graphs/contributors" } ], + "homepage": "https://github.com/recruiterphp/recruiter", "require": { - "php": "~7.2", + "php": "^8.4", + "ext-bcmath": "*", "ext-mongodb": ">=1.1", - "alcaeus/mongo-php-adapter": "^1.1", - "recruiterphp/geezer": "^5", - "gabrielelana/byte-units": "~0.1", - "monolog/monolog": ">=1", - "recruiterphp/concurrency": "^3.0", - "psr/log": "^1.0", - "symfony/console": "^4.2", - "symfony/event-dispatcher": "^3.4|^4.0", - "ulrichsg/getopt-php": "~2.1", - "mongodb/mongodb": "^1.4", - "mtdowling/cron-expression": "^1.2" - }, - "suggest": { - "symfony/console": "In order to use Recruiter\\Command\\RecruiterJobCommand." + "ext-posix": "*", + "dragonmantank/cron-expression": "^3.4", + "gabrielelana/byte-units": "^0.5", + "mongodb/mongodb": "^2.1", + "monolog/monolog": "^3.9", + "psr/log": "^3.0", + "recruiterphp/concurrency": "^5.0", + "recruiterphp/geezer": "^7.0", + "symfony/console": "^7.3", + "symfony/event-dispatcher": "^7.3", + "ulrichsg/getopt-php": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^8", + "ext-pcntl": "*", + "dms/phpunit-arraysubset-asserts": "^0.5", + "ergebnis/composer-normalize": "^2.47", + "friendsofphp/php-cs-fixer": "^3.85", + "giorgiosironi/eris": "^1.0", "phpstan/phpstan": "*", - "giorgiosironi/eris": "dev-master", - "dms/phpunit-arraysubset-asserts": "^0.1.0" + "phpunit/phpunit": "^10.0", + "rector/rector": "^2.1", + "symfony/var-dumper": "^7.3" + }, + "suggest": { + "symfony/console": "In order to use Recruiter\\Command\\RecruiterJobCommand." }, "minimum-stability": "dev", "prefer-stable": true, - "bin": [ - "bin/recruiter" - ], "autoload": { "psr-4": { "Recruiter\\": "src/Recruiter", - "Timeless\\": "src/Timeless", - "Sink\\": "src/Sink" + "Sink\\": "src/Sink", + "Timeless\\": "src/Timeless" }, "files": [ "src/Timeless/functions.php", "src/Recruiter/functions.php" ] + }, + "bin": [ + "bin/recruiter" + ], + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + }, + "sort-packages": true } } diff --git a/examples/bootstrap.php b/examples/bootstrap.php index 5e278a1e..1911e47f 100644 --- a/examples/bootstrap.php +++ b/examples/bootstrap.php @@ -1,6 +1,12 @@ getEventDispatcher()->addListener('job.failure.last', function($event) { - error_log("Job definitively failed: " . var_export($event->export(), true)); + +assert(isset($recruiter)); +assert($recruiter instanceof Recruiter); + +$recruiter->getEventDispatcher()->addListener('job.failure.last', function ($event): void { + error_log('Job definitively failed: ' . var_export($event->export(), true)); }); diff --git a/examples/jobAndWorkerTagged.php b/examples/jobAndWorkerTagged.php index 632e2238..d8795837 100755 --- a/examples/jobAndWorkerTagged.php +++ b/examples/jobAndWorkerTagged.php @@ -3,17 +3,16 @@ require __DIR__ . '/../vendor/autoload.php'; -use Recruiter\Recruiter; use Recruiter\Factory; +use Recruiter\Infrastructure\Memory\MemoryLimit; +use Recruiter\Infrastructure\Persistence\Mongodb\URI as MongoURI; +use Recruiter\Recruiter; use Recruiter\Workable\LazyBones; -use Recruiter\Worker; -use Recruiter\Option\MemoryLimit; $factory = new Factory(); $db = $factory->getMongoDb( - $hosts = 'localhost:27017', + MongoURI::fromEnvironment(), $options = [], - $dbName = 'recruiter' ); $db->drop(); @@ -23,9 +22,10 @@ ->asJobOf($recruiter) ->inGroup('mail') ->inBackground() - ->execute(); + ->execute() +; -$memoryLimit = new MemoryLimit('memory-limit', '64MB'); +$memoryLimit = new MemoryLimit('64MB'); $worker = $recruiter->hire($memoryLimit); $worker->workOnJobsGroupedAs('mail'); $assignments = $recruiter->assignJobsToWorkers(); diff --git a/examples/jobFailedBecauseOfNonRetriableException.php b/examples/jobFailedBecauseOfNonRetriableException.php index a1e452c3..18e088bf 100755 --- a/examples/jobFailedBecauseOfNonRetriableException.php +++ b/examples/jobFailedBecauseOfNonRetriableException.php @@ -3,37 +3,37 @@ require __DIR__ . '/../vendor/autoload.php'; -use Timeless as T; - -use Recruiter\Recruiter; use Recruiter\Factory; +use Recruiter\Infrastructure\Memory\MemoryLimit; +use Recruiter\Infrastructure\Persistence\Mongodb\URI as MongoURI; +use Recruiter\Recruiter; use Recruiter\Workable\AlwaysFail; -use Recruiter\RetryPolicy; -use Recruiter\Worker; -use Recruiter\Option\MemoryLimit; +use Timeless as T; $factory = new Factory(); $db = $factory->getMongoDb( - $hosts = 'localhost:27017', + MongoURI::fromEnvironment(), $options = [], - $dbName = 'recruiter' ); $db->drop(); $recruiter = new Recruiter($db); -(new AlwaysFail()) +new AlwaysFail() ->asJobOf($recruiter) - ->retryManyTimes(5, T\seconds(1), 'DomainException') + ->retryManyTimes(5, T\seconds(1), DomainException::class) ->inBackground() - ->execute(); + ->execute() +; -$memoryLimit = new MemoryLimit('memory-limit', '64MB'); +$memoryLimit = new MemoryLimit('64MB'); $worker = $recruiter->hire($memoryLimit); while (true) { printf("Try to do my work\n"); $assignments = $recruiter->assignJobsToWorkers(); - if ($assignments === 0) break; + if (0 === $assignments) { + break; + } $worker->work(); usleep(1200 * 1000); } diff --git a/examples/jobRetriedManyTimesUntilArchived.php b/examples/jobRetriedManyTimesUntilArchived.php index e713e515..8972afcc 100755 --- a/examples/jobRetriedManyTimesUntilArchived.php +++ b/examples/jobRetriedManyTimesUntilArchived.php @@ -3,37 +3,37 @@ require __DIR__ . '/../vendor/autoload.php'; -use Timeless as T; - -use Recruiter\Recruiter; use Recruiter\Factory; +use Recruiter\Infrastructure\Memory\MemoryLimit; +use Recruiter\Infrastructure\Persistence\Mongodb\URI as MongoURI; +use Recruiter\Recruiter; use Recruiter\Workable\AlwaysFail; -use Recruiter\RetryPolicy; -use Recruiter\Worker; -use Recruiter\Option\MemoryLimit; +use Timeless as T; $factory = new Factory(); $db = $factory->getMongoDb( - $hosts = 'localhost:27017', + MongoURI::fromEnvironment(), $options = [], - $dbName = 'recruiter' ); $db->drop(); $recruiter = new Recruiter($db); -(new AlwaysFail()) +new AlwaysFail() ->asJobOf($recruiter) ->retryManyTimes(5, T\second(1)) ->inBackground() - ->execute(); + ->execute() +; -$memoryLimit = new MemoryLimit('memory-limit', '64MB'); +$memoryLimit = new MemoryLimit('64MB'); $worker = $recruiter->hire($memoryLimit); while (true) { printf("Try to do my work\n"); $assignments = $recruiter->assignJobsToWorkers(); - if ($assignments === 0) break; + if (0 === count($assignments)) { + break; + } $worker->work(); usleep(1200 * 1000); } diff --git a/examples/oneTimeJob.php b/examples/oneTimeJob.php index 16561ff9..54a15ecb 100755 --- a/examples/oneTimeJob.php +++ b/examples/oneTimeJob.php @@ -3,17 +3,16 @@ require __DIR__ . '/../vendor/autoload.php'; -use Recruiter\Recruiter; use Recruiter\Factory; +use Recruiter\Infrastructure\Memory\MemoryLimit; +use Recruiter\Infrastructure\Persistence\Mongodb\URI as MongoURI; +use Recruiter\Recruiter; use Recruiter\Workable\LazyBones; -use Recruiter\Worker; -use Recruiter\Option\MemoryLimit; $factory = new Factory(); $db = $factory->getMongoDb( - $hosts = 'localhost:27017', + MongoURI::fromEnvironment(), $options = [], - $dbName = 'recruiter' ); $db->drop(); @@ -22,9 +21,10 @@ LazyBones::waitForMs(200, 100) ->asJobOf($recruiter) ->inBackground() - ->execute(); + ->execute() +; -$memoryLimit = new MemoryLimit('memory-limit', '64MB'); +$memoryLimit = new MemoryLimit('64MB'); $worker = $recruiter->hire($memoryLimit); $assignments = $recruiter->assignJobsToWorkers(); $worker->work(); diff --git a/phpstan.neon b/phpstan.neon index 4e803c3c..2db8cff2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,18 +1,13 @@ parameters: - level: max + level: 2 paths: - src - excludes_analyse: - - %currentWorkingDirectory%/vendor/ - autoload_directories: - - %currentWorkingDirectory%/spec + - spec + scanDirectories: + - %currentWorkingDirectory%/vendor/giorgiosironi/eris/src/ ignoreErrors: - - '#Instantiated class Recruiter\\Workable\\ThisClassDoesnNotExists not found.#' - - '#Constructor of class Recruiter\\Workable\\FailsInConstructor has an unused parameter \$parameters.#' - - '#Static call to instance method stdClass::import()#' - - '#Call to function is_callable\(\) with .recruiter_stept_back. will always evaluate to false.#' - - '#Call to function is_callable\(\) with .recruiter_became…. will always evaluate to false.#' - '#Function recruiter_became_master not found.#' - '#Function recruiter_stept_back not found.#' - - '#Call to an undefined method Traversable::toArray().#' + - '#Unsafe usage of new static\(\).#' + - '#to an undefined .* Sink\\BlackHole#' inferPrivatePropertyTypeFromConstructor: true diff --git a/phpunit.xml b/phpunit.dist.xml similarity index 100% rename from phpunit.xml rename to phpunit.dist.xml diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..bfef9bcc --- /dev/null +++ b/rector.php @@ -0,0 +1,18 @@ +withPaths([ + __DIR__ . '/examples', + __DIR__ . '/spec', + __DIR__ . '/src', + ]) + // uncomment to reach your current PHP version + ->withPhpSets() + ->withTypeCoverageLevel(4) + ->withDeadCodeLevel(0) + ->withCodeQualityLevel(1) +; diff --git a/spec/Recruiter/Acceptance/AssignmentTest.php b/spec/Recruiter/Acceptance/AssignmentTest.php index c7f08a3a..4c764bb7 100644 --- a/spec/Recruiter/Acceptance/AssignmentTest.php +++ b/spec/Recruiter/Acceptance/AssignmentTest.php @@ -1,21 +1,23 @@ asJobOf($this->recruiter) ->inBackground() - ->execute(); + ->execute() + ; $worker = $this->recruiter->hire($memoryLimit); - list ($assignments, $totalNumber) = $this->recruiter->assignJobsToWorkers(); + [$assignments, $totalNumber] = $this->recruiter->assignJobsToWorkers(); $this->assertEquals(1, count($assignments)); $this->assertEquals(1, $totalNumber); $this->assertTrue((bool) $worker->work()); diff --git a/spec/Recruiter/Acceptance/BaseAcceptanceTest.php b/spec/Recruiter/Acceptance/BaseAcceptanceTestCase.php similarity index 69% rename from spec/Recruiter/Acceptance/BaseAcceptanceTest.php rename to spec/Recruiter/Acceptance/BaseAcceptanceTestCase.php index 347be835..244b10ac 100644 --- a/spec/Recruiter/Acceptance/BaseAcceptanceTest.php +++ b/spec/Recruiter/Acceptance/BaseAcceptanceTestCase.php @@ -1,30 +1,37 @@ recruiterDb = $factory->getMongoDb(MongoURI::from('mongodb://localhost:27017/recruiter'), []); + $this->recruiterDb = $factory->getMongoDb(URI::fromEnvironment(), []); $this->cleanDb(); $this->files = ['/tmp/recruiter.log', '/tmp/worker.log']; $this->cleanLogs(); @@ -39,17 +46,17 @@ public function setUp(): void $this->processWorkers = []; } - public function tearDown(): void + protected function tearDown(): void { $this->terminateProcesses(SIGKILL); } - protected function cleanDb() + protected function cleanDb(): void { $this->recruiterDb->drop(); } - protected function clean() + protected function clean(): void { $this->terminateProcesses(SIGKILL); $this->cleanLogs(); @@ -57,7 +64,7 @@ protected function clean() $this->jobs = 0; } - public function cleanLogs() + public function cleanLogs(): void { foreach ($this->files as $file) { if (file_exists($file)) { @@ -66,21 +73,23 @@ public function cleanLogs() } } - protected function numberOfWorkers() + protected function numberOfWorkers(): int { - return $this->roster->count(); + return $this->roster->countDocuments(); } - protected function waitForNumberOfWorkersToBe($expectedNumber, $howManySeconds = 1) + protected function waitForNumberOfWorkersToBe($expectedNumber, $howManySeconds = 1): void { Timeout::inSeconds($howManySeconds, "workers to be $expectedNumber") ->until(function () use ($expectedNumber) { - $this->recruiter->retireDeadWorkers(new DateTimeImmutable(), T\seconds(0)); + $this->recruiter->retireDeadWorkers(new \DateTimeImmutable(), T\seconds(0)); + return $this->numberOfWorkers() == $expectedNumber; - }); + }) + ; } - protected function startRecruiter() + protected function startRecruiter(): array { $descriptors = [ 0 => ['pipe', 'r'], @@ -92,17 +101,20 @@ protected function startRecruiter() $process = proc_open('exec php bin/recruiter start:recruiter --backoff-to 5000ms --lease-time 10s --considered-dead-after 20s >> /tmp/recruiter.log 2>&1', $descriptors, $pipes, $cwd); - Timeout::inSeconds(1, "recruiter to be up") + Timeout::inSeconds(1, 'recruiter to be up') ->until(function () use ($process) { $status = proc_get_status($process); + return $status['running']; - }); + }) + ; $this->processRecruiter = [$process, $pipes, 'recruiter']; + return $this->processRecruiter; } - protected function startCleaner() + protected function startCleaner(): array { $descriptors = [ 0 => ['pipe', 'r'], @@ -111,11 +123,13 @@ protected function startCleaner() ]; $cwd = __DIR__ . '/../../../'; $process = proc_open('exec php bin/recruiter start:cleaner --wait-at-least=5s --wait-at-most=1m --lease-time 20s >> /tmp/cleaner.log 2>&1', $descriptors, $pipes, $cwd); - Timeout::inSeconds(1, "cleaner to be up") + Timeout::inSeconds(1, 'cleaner to be up') ->until(function () use ($process) { $status = proc_get_status($process); + return $status['running']; - }); + }) + ; $this->processCleaner = [$process, $pipes, 'cleaner']; return $this->processCleaner; @@ -138,67 +152,71 @@ protected function startWorker(array $additionalOptions = []) $cwd = __DIR__ . '/../../../'; $process = proc_open("exec php bin/recruiter start:worker $options >> /tmp/worker.log 2>&1", $descriptors, $pipes, $cwd); - Timeout::inSeconds(1, "worker to be up") + Timeout::inSeconds(1, 'worker to be up') ->until(function () use ($process) { $status = proc_get_status($process); + return $status['running']; - }); + }) + ; // proc_get_status($process); $this->processWorkers[] = [$process, $pipes, 'worker']; + return end($this->processWorkers); } - protected function stopProcessWithSignal(array $processAndPipes, $signal) + protected function stopProcessWithSignal(array $processAndPipes, int $signal): void { - list($process, $pipes, $name) = $processAndPipes; + [$process, $pipes, $name] = $processAndPipes; proc_terminate($process, $signal); $this->lastStatus = proc_get_status($process); - Timeout - ::inSeconds(30, function () use ($signal) { - return 'termination of process: ' . var_export($this->lastStatus, true) . " after sending the `$signal` signal to it"; - }) + Timeout::inSeconds(30, fn () => 'termination of process: ' . var_export($this->lastStatus, true) . " after sending the `$signal` signal to it") ->until(function () use ($process) { $this->lastStatus = proc_get_status($process); - return $this->lastStatus['running'] == false; - }); + + return false == $this->lastStatus['running']; + }) + ; } /** - * @param integer $duration milliseconds + * @param int $duration milliseconds */ - protected function enqueueJob($duration = 10, $tag = 'generic') + protected function enqueueJob(int $duration = 10, $tag = 'generic'): void { - $workable = ShellCommand::fromCommandLine("sleep " . ($duration / 1000)); + $workable = ShellCommand::fromCommandLine('sleep ' . ($duration / 1000)); $workable ->asJobOf($this->recruiter) ->inGroup($tag) ->inBackground() - ->execute(); - $this->jobs++; + ->execute() + ; + ++$this->jobs; } - protected function enqueueJobWithRetryPolicy($duration = 10, RetryPolicy $retryPolicy) + protected function enqueueJobWithRetryPolicy(int $duration, RetryPolicy $retryPolicy): void { - $workable = ShellCommand::fromCommandLine("sleep " . ($duration / 1000)); + $workable = ShellCommand::fromCommandLine('sleep ' . ($duration / 1000)); $workable ->asJobOf($this->recruiter) ->retryWithPolicy($retryPolicy) ->inBackground() - ->execute(); - $this->jobs++; + ->execute() + ; + ++$this->jobs; } - protected function start($workers) + protected function start(int $workers): void { $this->startRecruiter(); $this->startCleaner(); - for ($i = 0; $i < $workers; $i++) { + for ($i = 0; $i < $workers; ++$i) { $this->startWorker(); } } - private function terminateProcesses($signal) + private function terminateProcesses(int $signal): void { if ($this->processRecruiter) { $this->stopProcessWithSignal($this->processRecruiter, $signal); @@ -214,51 +232,52 @@ private function terminateProcesses($signal) $this->processWorkers = []; } - protected function restartWorkerGracefully($workerIndex) + protected function restartWorkerGracefully($workerIndex): void { $this->stopProcessWithSignal($this->processWorkers[$workerIndex], SIGTERM); $this->processWorkers[$workerIndex] = $this->startWorker(); } - protected function restartWorkerByKilling($workerIndex) + protected function restartWorkerByKilling($workerIndex): void { $this->stopProcessWithSignal($this->processWorkers[$workerIndex], SIGKILL); $this->processWorkers[$workerIndex] = $this->startWorker(); } - protected function restartRecruiterGracefully() + protected function restartRecruiterGracefully(): void { $this->stopProcessWithSignal($this->processRecruiter, SIGTERM); $this->startRecruiter(); } - protected function restartRecruiterByKilling() + protected function restartRecruiterByKilling(): void { $this->stopProcessWithSignal($this->processRecruiter, SIGKILL); $this->startRecruiter(); } - protected function files() + protected function files(): string { $logs = ''; if (getenv('TEST_DUMP')) { foreach ($this->files as $file) { - $logs .= $file. ":". PHP_EOL; + $logs .= $file . ':' . PHP_EOL; $logs .= file_get_contents($file); } } else { $logs .= var_export($this->files, true); } + return $logs; } - private function optionsToString(array $options = []) + private function optionsToString(array $options = []): string { $optionsString = ''; foreach ($options as $option => $value) { $optionsString .= " --$option=$value"; - }; + } return $optionsString; } diff --git a/spec/Recruiter/Acceptance/EnduranceTest.php b/spec/Recruiter/Acceptance/EnduranceTest.php index 3ef3a43c..650bb6ee 100644 --- a/spec/Recruiter/Acceptance/EnduranceTest.php +++ b/spec/Recruiter/Acceptance/EnduranceTest.php @@ -1,22 +1,26 @@ jobRepository = new Repository($this->recruiterDb); @@ -24,57 +28,50 @@ public function setUp(): void $this->files[] = $this->actionLog; } - public function testNotWithstandingCrashesJobsAreEventuallyPerformed() + public function testNotWithstandingCrashesJobsAreEventuallyPerformed(): void { $this ->limitTo(100) ->forAll( Generator\bind( Generator\choose(1, 4), - function ($workers) { - return Generator\tuple( - Generator\constant($workers), - Generator\seq(Generator\oneOf( - Generator\map( - function ($durationAndTag) { - list($duration, $tag) = $durationAndTag; - return ['enqueueJob', $duration, $tag]; - }, - Generator\tuple( - Generator\nat(), - Generator\elements(['generic', 'fast-lane']) - ) - ), - Generator\map( - function ($workerIndex) { - return ['restartWorkerGracefully', $workerIndex]; - }, - Generator\choose(0, $workers - 1) - ), - Generator\map( - function ($workerIndex) { - return ['restartWorkerByKilling', $workerIndex]; - }, - Generator\choose(0, $workers - 1) + fn ($workers) => Generator\tuple( + Generator\constant($workers), + Generator\seq(Generator\oneOf( + Generator\map( + function ($durationAndTag) { + [$duration, $tag] = $durationAndTag; + + return ['enqueueJob', $duration, $tag]; + }, + Generator\tuple( + Generator\nat(), + Generator\elements(['generic', 'fast-lane']), ), - Generator\constant('restartRecruiterGracefully'), - Generator\constant('restartRecruiterByKilling'), - Generator\map( - function ($milliseconds) { - return ['sleep', $milliseconds]; - }, - Generator\choose(1, 1000) - ) - )) - ); - } - ) + ), + Generator\map( + fn ($workerIndex) => ['restartWorkerGracefully', $workerIndex], + Generator\choose(0, $workers - 1), + ), + Generator\map( + fn ($workerIndex) => ['restartWorkerByKilling', $workerIndex], + Generator\choose(0, $workers - 1), + ), + Generator\constant('restartRecruiterGracefully'), + Generator\constant('restartRecruiterByKilling'), + Generator\map( + fn ($milliseconds) => ['sleep', $milliseconds], + Generator\choose(1, 1000), + ), + )), + ), + ), ) ->hook(Listener\log('/tmp/recruiter-test-iterations.log')) ->hook(Listener\collectFrequencies()) ->disableShrinking() - ->then(function ($tuple) { - list ($workers, $actions) = $tuple; + ->then(function ($tuple): void { + [$workers, $actions] = $tuple; $this->clean(); $this->start($workers); foreach ($actions as $action) { @@ -84,7 +81,7 @@ function ($milliseconds) { $method = array_shift($arguments); call_user_func_array( [$this, $method], - $arguments + $arguments, ); } else { $this->$action(); @@ -94,13 +91,10 @@ function ($milliseconds) { $estimatedTime = max(count($actions) * 4, 60); Timeout::inSeconds( $estimatedTime, - function () { - return "all $this->jobs jobs to be performed. Now is " . date('c') . " Logs: " . $this->files(); - } + fn () => "all $this->jobs jobs to be performed. Now is " . date('c') . ' Logs: ' . $this->files(), ) - ->until(function () { - return $this->jobRepository->countArchived() === $this->jobs; - }); + ->until(fn () => $this->jobRepository->countArchived() === $this->jobs) + ; $at = T\now(); $statistics = $this->recruiter->statistics($tag = null, $at); @@ -115,7 +109,8 @@ function () { } // TODO: add tolerance $this->assertEquals($statistics['throughput']['value'], $cumulativeThroughput); - }); + }) + ; } private function logAction($action) @@ -123,11 +118,11 @@ private function logAction($action) file_put_contents( $this->actionLog, sprintf( - "[ACTIONS][PHPUNIT][%s] %s" . PHP_EOL, + '[ACTIONS][PHPUNIT][%s] %s' . PHP_EOL, date('c'), - json_encode($action) + json_encode($action), ), - FILE_APPEND + FILE_APPEND, ); } diff --git a/spec/Recruiter/Acceptance/FaultToleranceTest.php b/spec/Recruiter/Acceptance/FaultToleranceTest.php index d71afafe..0faeb0b8 100644 --- a/spec/Recruiter/Acceptance/FaultToleranceTest.php +++ b/spec/Recruiter/Acceptance/FaultToleranceTest.php @@ -1,72 +1,74 @@ enqueueJob(); $worker = $this->recruiter->hire($memoryLimit); $this->recruiter->bookJobsForWorkers(); $this->recruiter->rollbackLockedJobs(); - list ($assignments, $totalNumber) = $this->recruiter->assignJobsToWorkers(); - $this->assertEquals(1, count($assignments)); + [$assignments, $totalNumber] = $this->recruiter->assignJobsToWorkers(); + $this->assertCount(1, $assignments); $this->assertEquals(1, $totalNumber); } - public function testRetryPolicyMustBeAppliedEvenWhenWorkerDiesInConstructor() + public function testRetryPolicyMustBeAppliedEvenWhenWorkerDiesInConstructor(): void { - (new FailsInConstructor([], false)) + new FailsInConstructor([], false) ->asJobOf($this->recruiter) ->inBackground() ->retryWithPolicy(RetryManyTimes::forTimes(1, 0)) - ->execute(); + ->execute() + ; $worker = $this->startWorker(); - $this->waitForNumberOfWorkersToBe(1); + $this->waitForNumberOfWorkersToBe(1, 5); - list ($assignments, $_) = $this->recruiter->assignJobsToWorkers(); - $this->assertEquals(1, count($assignments)); + [$assignments, $_] = $this->recruiter->assignJobsToWorkers(); + $this->assertCount(1, $assignments); sleep(2); $jobDocument = current($this->scheduled->find()->toArray()); $this->assertEquals(1, $jobDocument['attempts']); - $this->assertEquals('Recruiter\\Workable\\FailsInConstructor', $jobDocument['workable']['class']); + $this->assertEquals(FailsInConstructor::class, $jobDocument['workable']['class']); $this->assertStringContainsString('This job failed while instantiating a workable', $jobDocument['last_execution']['message']); $this->assertStringContainsString('I am supposed to fail in constructor code for testing purpose', $jobDocument['last_execution']['message']); - list ($assignments, $_) = $this->recruiter->assignJobsToWorkers(); - $this->assertEquals(1, count($assignments)); + [$assignments, $_] = $this->recruiter->assignJobsToWorkers(); + $this->assertCount(1, $assignments); sleep(2); $jobDocument = current($this->archived->find()->toArray()); $this->assertEquals(2, $jobDocument['attempts']); - $this->assertEquals('Recruiter\\Workable\\FailsInConstructor', $jobDocument['workable']['class']); + $this->assertEquals(FailsInConstructor::class, $jobDocument['workable']['class']); $this->assertStringContainsString('This job failed while instantiating a workable', $jobDocument['last_execution']['message']); $this->assertStringContainsString('I am supposed to fail in constructor code for testing purpose', $jobDocument['last_execution']['message']); - list ($assignments, $_) = $this->recruiter->assignJobsToWorkers(); - $this->assertEquals(0, count($assignments)); + [$assignments, $_] = $this->recruiter->assignJobsToWorkers(); + $this->assertCount(0, $assignments); } - public function testRetryPolicyMustBeAppliedEvenWhenWorkerDies() + public function testRetryPolicyMustBeAppliedEvenWhenWorkerDies(): void { // This job will fail with a fatal error and we want it to be // retried at most 1 time, this means at most 2 // executions. The problem is that the retry policy is // evaluated after the execution but fatal errors are not // catchable and so the job will stay scheduled forever - (new ThrowsFatalError()) + new ExitsAbruptly() ->asJobOf($this->recruiter) ->inBackground() ->retryWithPolicy(RetryManyTimes::forTimes(1, 0)) - ->execute(); + ->execute() + ; // Right now we recover for dead jobs when we // Recruiter::retireDeadWorkers and when we @@ -78,8 +80,8 @@ public function testRetryPolicyMustBeAppliedEvenWhenWorkerDies() // First execution of the job $worker = $this->startWorker(); $this->waitForNumberOfWorkersToBe(1); - list ($assignments, $_) = $this->recruiter->assignJobsToWorkers(); - $this->assertEquals(1, count($assignments)); + [$assignments, $_] = $this->recruiter->assignJobsToWorkers(); + $this->assertCount(1, $assignments); sleep(2); // The worker is dead and the job is not properly scheduled $this->recruiter->retireDeadWorkers(new \DateTimeImmutable(), T\seconds(0)); @@ -89,12 +91,12 @@ public function testRetryPolicyMustBeAppliedEvenWhenWorkerDies() $worker = $this->startWorker(); $this->waitForNumberOfWorkersToBe(1); // Here the job is assigned and rescheduled by the retry policy because found crashed - list ($assignments, $_) = $this->recruiter->assignJobsToWorkers(); - $this->assertEquals(1, count($assignments)); + [$assignments, $_] = $this->recruiter->assignJobsToWorkers(); + $this->assertCount(1, $assignments); sleep(2); // The worker is dead and the job is not properly scheduled $this->recruiter->retireDeadWorkers(new \DateTimeImmutable(), T\seconds(0)); - $this->waitForNumberOfWorkersToBe(0); + $this->waitForNumberOfWorkersToBe(0, 2); $this->assertJobIsMarkedAsCrashed(); // Third execution of the job @@ -103,17 +105,17 @@ public function testRetryPolicyMustBeAppliedEvenWhenWorkerDies() // Here the job is assigned and archived by the retry policy // because found crashed and because it has been already // executed 2 times - list ($assignments, $_) = $this->recruiter->assignJobsToWorkers(); - $this->assertEquals(1, count($assignments)); + [$assignments, $_] = $this->recruiter->assignJobsToWorkers(); + $this->assertCount(1, $assignments); sleep(1); // The worker is not dead and the job is not scheduled anymore $this->assertEquals(0, $this->recruiter->queued()); } - private function assertJobIsMarkedAsCrashed() + private function assertJobIsMarkedAsCrashed(): void { $jobs = iterator_to_array($this->recruiterDb->selectCollection('scheduled')->find()); - $this->assertEquals(1, count($jobs)); + $this->assertCount(1, $jobs); foreach ($jobs as $job) { $this->assertArrayHasKey('last_execution', $job); $this->assertArrayHasKey('crashed', $job['last_execution']); diff --git a/spec/Recruiter/Acceptance/HooksTest.php b/spec/Recruiter/Acceptance/HooksTest.php index 05266dcf..f52bb3ce 100644 --- a/spec/Recruiter/Acceptance/HooksTest.php +++ b/spec/Recruiter/Acceptance/HooksTest.php @@ -1,70 +1,79 @@ memoryLimit = new MemoryLimit('64MB'); parent::setUp(); } - public function testAfterFailureWithoutRetryEventIsFired() + public function testAfterFailureWithoutRetryEventIsFired(): void { $this->events = []; $this->recruiter ->getEventDispatcher() ->addListener( 'job.failure.last', - function (Event $event) { + function (Event $event): void { $this->events[] = $event; - } - ); + }, + ) + ; - $job = (new AlwaysFail()) + $job = new AlwaysFail() ->asJobOf($this->recruiter) ->inBackground() - ->execute(); + ->execute() + ; $worker = $this->recruiter->hire($this->memoryLimit); $this->recruiter->assignJobsToWorkers(); $worker->work(); $this->assertEquals(1, count($this->events)); - $this->assertInstanceOf('Recruiter\Job\Event', $this->events[0]); + $this->assertInstanceOf(Event::class, $this->events[0]); $this->assertEquals('not-scheduled-by-retry-policy', $this->events[0]->export()['why']); } - public function testAfterLastFailureEventIsFired() + public function testAfterLastFailureEventIsFired(): void { $this->events = []; $this->recruiter ->getEventDispatcher() ->addListener( 'job.failure.last', - function (Event $event) { + function (Event $event): void { $this->events[] = $event; - } - ); + }, + ) + ; - $job = (new AlwaysFail()) + $job = new AlwaysFail() ->asJobOf($this->recruiter) ->retryWithPolicy(RetryManyTimes::forTimes(1, 0)) ->inBackground() - ->execute(); + ->execute() + ; - $runAJob = function ($howManyTimes, $worker) { + $runAJob = function ($howManyTimes, $worker): void { for ($i = 0; $i < $howManyTimes;) { - list($_, $assigned) = $this->recruiter->assignJobsToWorkers(); + [$_, $assigned] = $this->recruiter->assignJobsToWorkers(); $worker->work(); if ($assigned > 0) { - $i++; + ++$i; } } }; @@ -73,56 +82,61 @@ function (Event $event) { $runAJob(2, $worker); $this->assertEquals(1, count($this->events)); - $this->assertInstanceOf('Recruiter\Job\Event', $this->events[0]); + $this->assertInstanceOf(Event::class, $this->events[0]); $this->assertEquals('tried-too-many-times', $this->events[0]->export()['why']); } - public function testJobStartedIsFired() + public function testJobStartedIsFired(): void { $this->events = []; $this->recruiter ->getEventDispatcher() ->addListener( 'job.started', - function (Event $event) { + function (Event $event): void { $this->events[] = $event; - } - ); + }, + ) + ; - $job = (new AlwaysSucceed()) + $job = new AlwaysSucceed() ->asJobOf($this->recruiter) ->inBackground() - ->execute(); + ->execute() + ; $worker = $this->recruiter->hire($this->memoryLimit); $this->recruiter->assignJobsToWorkers(); $worker->work(); $this->assertEquals(1, count($this->events)); - $this->assertInstanceOf('Recruiter\Job\Event', $this->events[0]); + $this->assertInstanceOf(Event::class, $this->events[0]); } - public function testJobEndedIsFired() + public function testJobEndedIsFired(): void { $this->events = []; $this->recruiter ->getEventDispatcher() ->addListener( 'job.ended', - function (Event $event) { + function (Event $event): void { $this->events[] = $event; - } - ); + }, + ) + ; - (new AlwaysSucceed()) + new AlwaysSucceed() ->asJobOf($this->recruiter) ->inBackground() - ->execute(); + ->execute() + ; - (new AlwaysFail()) + new AlwaysFail() ->asJobOf($this->recruiter) ->inBackground() - ->execute(); + ->execute() + ; $worker = $this->recruiter->hire($this->memoryLimit); $this->recruiter->assignJobsToWorkers(); @@ -131,7 +145,7 @@ function (Event $event) { $worker->work(); $this->assertEquals(2, count($this->events)); - $this->assertInstanceOf('Recruiter\Job\Event', $this->events[0]); - $this->assertInstanceOf('Recruiter\Job\Event', $this->events[1]); + $this->assertInstanceOf(Event::class, $this->events[0]); + $this->assertInstanceOf(Event::class, $this->events[1]); } } diff --git a/spec/Recruiter/Acceptance/RepeatableJobsAreScheduledTest.php b/spec/Recruiter/Acceptance/RepeatableJobsAreScheduledTest.php index 7d1a29b4..f5d692cc 100644 --- a/spec/Recruiter/Acceptance/RepeatableJobsAreScheduledTest.php +++ b/spec/Recruiter/Acceptance/RepeatableJobsAreScheduledTest.php @@ -1,22 +1,21 @@ 0, 'group' => 'generic', 'workable' => [ - 'class' => 'Recruiter\\Workable\\SampleRepeatableCommand', + 'class' => SampleRepeatableCommand::class, 'parameters' => [], 'method' => 'execute', ], @@ -48,16 +47,16 @@ public function testARepeatableJobIsScheduledAtExpectedScheduledTime() 'executions' => 1, ], 'retry_policy' => [ - 'class' => 'Recruiter\\RetryPolicy\\ExponentialBackoff', + 'class' => ExponentialBackoff::class, 'parameters' => [ 'retry_how_many_times' => 2, 'seconds_to_initially_wait_before_retry' => 5, ], - ] + ], ], $jobData); } - public function testOnlyASingleJobAreScheduledForTheSameSchedulingTime() + public function testOnlyASingleJobAreScheduledForTheSameSchedulingTime(): void { $expectedScheduleDate = strtotime('2019-05-16T14:00:00'); $schedulePolicy = new FixedSchedulePolicy($expectedScheduleDate); @@ -72,11 +71,11 @@ public function testOnlyASingleJobAreScheduledForTheSameSchedulingTime() $this->assertEquals( T\MongoDate::from(Moment::fromTimestamp($expectedScheduleDate)), - $jobs[0]->export()['scheduled_at'] + $jobs[0]->export()['scheduled_at'], ); } - public function testAJobIsScheduledForEverySchedulingTime() + public function testAJobIsScheduledForEverySchedulingTime(): void { $expectedScheduleDates = [ strtotime('2019-05-16T14:00:00'), @@ -94,7 +93,7 @@ public function testAJobIsScheduledForEverySchedulingTime() $this->assertEquals(1, $jobs[1]->export()['scheduled']['executions']); } - public function testANewJobIsNotScheduledIfItShouldBeUniqueAndTheOldOneIsStillRunning() + public function testANewJobIsNotScheduledIfItShouldBeUniqueAndTheOldOneIsStillRunning(): void { $schedulePolicy = new FixedSchedulePolicy([ strtotime('2019-05-16T14:00:00'), @@ -109,7 +108,7 @@ public function testANewJobIsNotScheduledIfItShouldBeUniqueAndTheOldOneIsStillRu $this->assertEquals(1, count($jobs)); } - public function testSchedulersAreUniqueOnUrn() + public function testSchedulersAreUniqueOnUrn(): void { $aSchedulerAlreadyHaveSomeAttempts = 3; $this->IHaveAScheduleWithALongStory('unique-urn', $aSchedulerAlreadyHaveSomeAttempts); @@ -134,8 +133,8 @@ public function testSchedulersAreUniqueOnUrn() private function IHaveAScheduleWithALongStory(string $urn, $attempts) { $scheduleTimes = []; - for ($i = 1; $i <= $attempts; $i++) { - $scheduleTimes[] = strtotime("2018-05-" . $i . "T15:00:00"); + for ($i = 1; $i <= $attempts; ++$i) { + $scheduleTimes[] = strtotime('2018-05-' . $i . 'T15:00:00'); } $schedulePolicy = new FixedSchedulePolicy($scheduleTimes); @@ -144,13 +143,13 @@ private function IHaveAScheduleWithALongStory(string $urn, $attempts) $this->recruiterScheduleJobsNTimes($attempts); } - private function scheduleAJob(string $urn, SchedulePolicy $schedulePolicy = null, bool $unique = false) + private function scheduleAJob(string $urn, ?SchedulePolicy $schedulePolicy = null, bool $unique = false) { if (is_null($schedulePolicy)) { $schedulePolicy = new FixedSchedulePolicy(strtotime('2023-02-18T17:00:00')); } - $scheduler = (new SampleRepeatableCommand()) + $scheduler = new SampleRepeatableCommand() ->asRepeatableJobOf($this->recruiter) ->repeatWithPolicy($schedulePolicy) ->retryWithPolicy(ExponentialBackoff::forTimes(2, 5)) @@ -160,7 +159,7 @@ private function scheduleAJob(string $urn, SchedulePolicy $schedulePolicy = null return $scheduler->create(); } - private function recruiterScheduleJobsNTimes(int $nth = 1) + private function recruiterScheduleJobsNTimes(int $nth = 1): void { $i = 0; while ($i++ < $nth) { @@ -171,29 +170,29 @@ private function recruiterScheduleJobsNTimes(int $nth = 1) private function fetchScheduledJobs() { $jobsRepository = new JobsRepository($this->recruiterDb); + return $jobsRepository->all(); } private function fetchSchedulers() { $schedulersRepository = new SchedulersRepository($this->recruiterDb); + return $schedulersRepository->all(); } } class FixedSchedulePolicy implements SchedulePolicy { - private $timestamp; - private $index; + private array $timestamps; - public function __construct($timestamps, $index = 0) + public function __construct($timestamps, private int $index = 0) { if (!is_array($timestamps)) { $timestamps = [$timestamps]; } $this->timestamps = $timestamps; - $this->index = $index; } public function next(): Moment diff --git a/spec/Recruiter/Acceptance/SyncronousExecutionTest.php b/spec/Recruiter/Acceptance/SyncronousExecutionTest.php index e1898b5b..382c0524 100644 --- a/spec/Recruiter/Acceptance/SyncronousExecutionTest.php +++ b/spec/Recruiter/Acceptance/SyncronousExecutionTest.php @@ -1,13 +1,14 @@ enqueueAnAnswerJob(43, T\now()->after(T\seconds(30))); @@ -21,12 +22,13 @@ public function testJobsAreExecutedInOrderOfScheduling() $this->assertEquals(43, end($results)->result()); } - public function testAReportIsReturnedInOrderToSortOutIfAnErrorOccured() + public function testAReportIsReturnedInOrderToSortOutIfAnErrorOccured(): void { - (new AlwaysFail()) + new AlwaysFail() ->asJobOf($this->recruiter) ->inBackground() - ->execute(); + ->execute() + ; $report = $this->recruiter->flushJobsSynchronously(); @@ -40,7 +42,8 @@ private function enqueueAnAnswerJob($answer, $scheduledAt) ->asJobOf($this->recruiter) ->scheduleAt($scheduledAt) ->inBackground() - ->execute(); + ->execute() + ; } } diff --git a/spec/Recruiter/Acceptance/WorkerGuaranteedToExitWhenAMemoryLeakOccurs.php b/spec/Recruiter/Acceptance/WorkerGuaranteedToExitWhenAMemoryLeakOccurs.php index f6121ab2..301363c5 100644 --- a/spec/Recruiter/Acceptance/WorkerGuaranteedToExitWhenAMemoryLeakOccurs.php +++ b/spec/Recruiter/Acceptance/WorkerGuaranteedToExitWhenAMemoryLeakOccurs.php @@ -2,25 +2,27 @@ namespace Recruiter\Acceptance; -Recruiter\Concurrency\Timeout; +use Recruiter\Concurrency\Timeout; use Recruiter\Workable\ConsumingMemoryCommand; use Timeless as T; -class WorkerGuaranteedToExitWhenAMemoryLeakOccurs extends BaseAcceptanceTest +class WorkerGuaranteedToExitWhenAMemoryLeakOccurs extends BaseAcceptanceTestCase { /** * @group acceptance + * * @dataProvider provideMemoryConsumptions */ - public function testWorkerKillItselfAfterAMemoryLeakButNotAfterABigMemoryConsumptionWithoutLeak($withMemoryLeak, $howManyItems, $memoryLimit, $expectedWorkerAlive) + public function testWorkerKillItselfAfterAMemoryLeakButNotAfterABigMemoryConsumptionWithoutLeak($withMemoryLeak, $howManyItems, $memoryLimit, $expectedWorkerAlive): void { - (new ConsumingMemoryCommand([ + new ConsumingMemoryCommand([ 'withMemoryLeak' => $withMemoryLeak, 'howManyItems' => $howManyItems, - ])) + ]) ->asJobOf($this->recruiter) ->inBackground() - ->execute(); + ->execute() + ; $this->startRecruiter(); @@ -30,13 +32,15 @@ public function testWorkerKillItselfAfterAMemoryLeakButNotAfterABigMemoryConsump ]); $this->waitForNumberOfWorkersToBe($numberOfWorkersBefore + 1, 5); - Timeout::inSeconds(5, function () { + Timeout::inSeconds(5, function (): void { }) ->until(function () { $at = T\now(); $statistics = $this->recruiter->statistics($tag = null, $at); - return $statistics['jobs']['queued'] == 0; - }); + + return 0 == $statistics['jobs']['queued']; + }) + ; $numberOfWorkersCurrently = $this->numberOfWorkers(); @@ -49,14 +53,14 @@ public function testWorkerKillItselfAfterAMemoryLeakButNotAfterABigMemoryConsump $this->assertEquals( $numberOfExpectedWorkers, $numberOfWorkersCurrently, - "The number of workers before was $numberOfWorkersBefore and now after starting 1 and execute a job we have $numberOfWorkersCurrently" + "The number of workers before was $numberOfWorkersBefore and now after starting 1 and execute a job we have $numberOfWorkersCurrently", ); } public static function provideMemoryConsumptions() { return [ - //legend: [$withMemoryLeak, $howManyItems, $memoryLimit, $expectedWorkerAlive], + // legend: [$withMemoryLeak, $howManyItems, $memoryLimit, $expectedWorkerAlive], [false, 2000000, '20MB', true], [true, 2000000, '20MB', false], [true, 2000000, '128MB', true], diff --git a/spec/Recruiter/Acceptance/WorkerGuaranteedToExitWithFailureCodeInCaseOfExceptionTest.php b/spec/Recruiter/Acceptance/WorkerGuaranteedToExitWithFailureCodeInCaseOfExceptionTest.php index e066a577..e683558b 100644 --- a/spec/Recruiter/Acceptance/WorkerGuaranteedToExitWithFailureCodeInCaseOfExceptionTest.php +++ b/spec/Recruiter/Acceptance/WorkerGuaranteedToExitWithFailureCodeInCaseOfExceptionTest.php @@ -2,17 +2,16 @@ namespace Recruiter\Acceptance; -use Recruiter\Workable\FactoryMethodCommand; -use Recruiter\Workable\ThrowsFatalError; +use Recruiter\Workable\ExitsAbruptly; -class WorkerGuaranteedToExitWithFailureCodeInCaseOfExceptionTest extends BaseAcceptanceTest +class WorkerGuaranteedToExitWithFailureCodeInCaseOfExceptionTest extends BaseAcceptanceTestCase { /** * @group acceptance */ - public function testInCaseOfExceptionTheExitCodeOfWorkerProcessIsNotZero() + public function testInCaseOfExceptionTheExitCodeOfWorkerProcessIsNotZero(): void { - (new ThrowsFatalError()) + new ExitsAbruptly() ->asJobOf($this->recruiter) ->inBackground() ->execute() @@ -21,7 +20,7 @@ public function testInCaseOfExceptionTheExitCodeOfWorkerProcessIsNotZero() $worker = $this->startWorker(); $workerProcess = $worker[0]; $this->waitForNumberOfWorkersToBe(1); - list ($assignments, $_) = $this->recruiter->assignJobsToWorkers(); + [$assignments, $_] = $this->recruiter->assignJobsToWorkers(); $this->assertEquals(1, count($assignments)); $this->waitForNumberOfWorkersToBe(0, $seconds = 10); diff --git a/spec/Recruiter/Acceptance/WorkerGuaranteedToRetireAfterDeathTest.php b/spec/Recruiter/Acceptance/WorkerGuaranteedToRetireAfterDeathTest.php index 95ca3c9f..8e6c93fe 100644 --- a/spec/Recruiter/Acceptance/WorkerGuaranteedToRetireAfterDeathTest.php +++ b/spec/Recruiter/Acceptance/WorkerGuaranteedToRetireAfterDeathTest.php @@ -2,12 +2,12 @@ namespace Recruiter\Acceptance; -class WorkerGuaranteedToRetireAfterDeathTest extends BaseAcceptanceTest +class WorkerGuaranteedToRetireAfterDeathTest extends BaseAcceptanceTestCase { /** * @group acceptance */ - public function testRetireAfterAskedToStop() + public function testRetireAfterAskedToStop(): void { $numberOfWorkersBefore = $this->numberOfWorkers(); $processAndPipes = $this->startWorker(); @@ -17,7 +17,7 @@ public function testRetireAfterAskedToStop() $this->assertEquals( $numberOfWorkersBefore, $numberOfWorkersCurrently, - "The number of workers before was $numberOfWorkersBefore and now after starting and stopping 1 we have $numberOfWorkersCurrently" + "The number of workers before was $numberOfWorkersBefore and now after starting and stopping 1 we have $numberOfWorkersCurrently", ); } } diff --git a/spec/Recruiter/Acceptance/WorkerRepositoryTest.php b/spec/Recruiter/Acceptance/WorkerRepositoryTest.php index 3364f238..0c27dd63 100644 --- a/spec/Recruiter/Acceptance/WorkerRepositoryTest.php +++ b/spec/Recruiter/Acceptance/WorkerRepositoryTest.php @@ -3,23 +3,25 @@ namespace Recruiter\Acceptance; use Recruiter\Worker\Repository; -use Recruiter\Recruiter; -class WorkerRepositoryTest extends BaseAcceptanceTest +class WorkerRepositoryTest extends BaseAcceptanceTestCase { - public function setUp(): void + private Repository $repository; + + #[\Override] + protected function setUp(): void { parent::setUp(); $this->repository = new Repository( $this->recruiterDb, - $this->recruiter + $this->recruiter, ); } /** * @group acceptance */ - public function testRetireWorkerWithPid() + public function testRetireWorkerWithPid(): void { $this->givenWorkerWithPid(10); $this->assertEquals(1, $this->numberOfWorkers()); diff --git a/spec/Recruiter/CleanerTest.php b/spec/Recruiter/CleanerTest.php index a1a70f64..8ae984a4 100644 --- a/spec/Recruiter/CleanerTest.php +++ b/spec/Recruiter/CleanerTest.php @@ -2,38 +2,48 @@ namespace Recruiter; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Timeless\Interval; +use Recruiter\Job\Repository; use Timeless as T; +use Timeless\Interval; +use Timeless\Moment; class CleanerTest extends TestCase { - public function setUp(): void + private T\ClockInterface $clock; + private Moment $now; + private MockObject $jobRepository; + private Cleaner $cleaner; + private Interval $interval; + + protected function setUp(): void { $this->clock = T\clock()->stop(); $this->now = $this->clock->now(); $this->jobRepository = $this - ->getMockBuilder('Recruiter\Job\Repository') + ->getMockBuilder(Repository::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; $this->cleaner = new Cleaner($this->jobRepository); $this->interval = Interval::parse('10s'); } - public function tearDown(): void + protected function tearDown(): void { T\clock()->start(); } - public function testShouldCreateCleaner() + public function testShouldCreateCleaner(): void { - $this->assertInstanceOf('Recruiter\Cleaner', $this->cleaner); + $this->assertInstanceOf(Cleaner::class, $this->cleaner); } - public function testDelegatesTheCleanupOfArchivedJobsToTheJobsRepository() + public function testDelegatesTheCleanupOfArchivedJobsToTheJobsRepository(): void { $expectedUpperLimit = $this->now->before($this->interval); @@ -41,11 +51,12 @@ public function testDelegatesTheCleanupOfArchivedJobsToTheJobsRepository() ->expects($this->once()) ->method('cleanArchived') ->with($expectedUpperLimit) - ->will($this->returnValue($jobsCleaned = 10)); + ->will($this->returnValue($jobsCleaned = 10)) + ; $this->assertEquals( $jobsCleaned, - $this->cleaner->cleanArchived($this->interval) + $this->cleaner->cleanArchived($this->interval), ); } } diff --git a/spec/Recruiter/ExampleTest.php b/spec/Recruiter/ExampleTest.php index 9b9b279f..9b4eecf5 100644 --- a/spec/Recruiter/ExampleTest.php +++ b/spec/Recruiter/ExampleTest.php @@ -6,7 +6,7 @@ class ExampleTest extends TestCase { - public function testMustPass() + public function testMustPass(): void { $this->assertTrue(true); } diff --git a/spec/Recruiter/FactoryTest.php b/spec/Recruiter/FactoryTest.php index 293f1623..9f7fa61d 100644 --- a/spec/Recruiter/FactoryTest.php +++ b/spec/Recruiter/FactoryTest.php @@ -2,69 +2,56 @@ namespace Recruiter; -use MongoDB; +use MongoDB\Database; use PHPUnit\Framework\TestCase; use Recruiter\Infrastructure\Persistence\Mongodb\URI as MongoURI; class FactoryTest extends TestCase { - /** - * @var Factory - */ - private $factory; - - /** - * @var string - */ - private $dbHost; - - /** - * @var string - */ - private $dbName; + private Factory $factory; + private MongoURI $mongoURI; protected function setUp(): void { $this->factory = new Factory(); - $this->dbHost = 'localhost:27017'; - $this->dbName = 'recruiter'; + $this->mongoURI = MongoURI::fromEnvironment(); } - public function testShouldCreateAMongoDatabaseConnection() + public function testShouldCreateAMongoDatabaseConnection(): void { $this->assertInstanceOf( - 'MongoDB\Database', - $this->creationOfDefaultMongoDb() + Database::class, + $this->creationOfDefaultMongoDb(), ); } - public function testWriteConcernIsMajorityByDefault() + public function testWriteConcernIsMajorityByDefault(): void { $mongoDb = $this->creationOfDefaultMongoDb(); $this->assertEquals('majority', $mongoDb->getWriteConcern()->getW()); } - public function testShouldOverwriteTheWriteConcernPassedInTheOptions() + public function testShouldOverwriteTheWriteConcernPassedInTheOptions(): void { $mongoDb = $this->factory->getMongoDb( - MongoURI::from('mongodb://localhost:27017/recruiter'), + $this->mongoURI, [ 'connectTimeoutMS' => 1000, 'w' => '0', - ] + ], ); $this->assertEquals('majority', $mongoDb->getWriteConcern()->getW()); } - private function creationOfDefaultMongoDb() + private function creationOfDefaultMongoDb(): Database { return $this->factory->getMongoDb( - MongoURI::from(sprintf('mongodb://%s/%s', $this->dbHost, $this->dbName)), + $this->mongoURI, [ 'connectTimeoutMS' => 1000, 'w' => '0', - ] + ], ); } } diff --git a/spec/Recruiter/FinalizerMethodsAreCalledWhenWorkableImplementsFinalizerInterfaceTest.php b/spec/Recruiter/FinalizerMethodsAreCalledWhenWorkableImplementsFinalizerInterfaceTest.php index 3a60e5fc..a5891c64 100644 --- a/spec/Recruiter/FinalizerMethodsAreCalledWhenWorkableImplementsFinalizerInterfaceTest.php +++ b/spec/Recruiter/FinalizerMethodsAreCalledWhenWorkableImplementsFinalizerInterfaceTest.php @@ -2,59 +2,77 @@ namespace Recruiter; -use Exception; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Recruiter\Job\Repository; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; class FinalizerMethodsAreCalledWhenWorkableImplementsFinalizerInterfaceTest extends TestCase { - public function setUp(): void + private MockObject&Repository $repository; + private MockObject&EventDispatcherInterface $dispatcher; + private ListenerSpy $listener; + + /** + * @throws \PHPUnit\Framework\MockObject\Exception + */ + protected function setUp(): void { $this->repository = $this - ->getMockBuilder('Recruiter\Job\Repository') + ->getMockBuilder(Repository::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; - $this->dispatcher = $this->createMock( - 'Symfony\Component\EventDispatcher\EventDispatcherInterface' - ); + $this->dispatcher = $this->createMock(EventDispatcherInterface::class); + $this->listener = new ListenerSpy(); } - public function testFinalizableFailureMethodsAreCalledWhenJobFails() + public function testFinalizableFailureMethodsAreCalledWhenJobFails(): void { $exception = new \Exception('job was failed'); - $listener = $this->createPartialMock('StdClass', ['methodWasCalled']); - $listener - ->expects($this->exactly(3)) - ->method('methodWasCalled') - ->withConsecutive( - [$this->equalTo('afterFailure'), $exception], - [$this->equalTo('afterLastFailure'), $exception], - [$this->equalTo('finalize'), $exception] - ); - $workable = new FinalizableWorkable(function () use ($exception) { + + $workable = new FinalizableWorkable(function () use ($exception): void { throw $exception; - }, $listener); + }, $this->listener); $job = Job::around($workable, $this->repository); $job->execute($this->dispatcher); + + $calls = $this->listener->calls; + $this->assertCount(3, $calls); + + $this->assertSame('afterFailure', $calls[0][0]); + $this->assertSame($exception, $calls[0][1]); + + $this->assertSame('afterLastFailure', $calls[1][0]); + $this->assertSame($exception, $calls[1][1]); + + $this->assertSame('finalize', $calls[2][0]); + $this->assertSame($exception, $calls[2][1]); } - public function testFinalizableSuccessfullMethodsAreCalledWhenJobIsDone() + public function testFinalizableSuccessfullMethodsAreCalledWhenJobIsDone(): void { - $listener = $this->createPartialMock('StdClass', ['methodWasCalled']); - $listener - ->expects($this->exactly(2)) - ->method('methodWasCalled') - ->withConsecutive( - [$this->equalTo('afterSuccess')], - [$this->equalTo('finalize')] - ); - $workable = new FinalizableWorkable(function () { - return true; - }, $listener); + $workable = new FinalizableWorkable(fn () => true, $this->listener); $job = Job::around($workable, $this->repository); $job->execute($this->dispatcher); + + $calls = $this->listener->calls; + $this->assertCount(2, $calls); + $this->assertSame('afterSuccess', $calls[0][0]); + $this->assertSame('finalize', $calls[1][0]); + } +} + +class ListenerSpy +{ + public array $calls = []; + + public function methodWasCalled(string $name, ?\Throwable $exception = null): void + { + $this->calls[] = [$name, $exception]; } } @@ -65,36 +83,35 @@ class FinalizableWorkable implements Workable, Finalizable private $whatToDo; - private $listener; - - public function __construct(callable $whatToDo, $listener) + public function __construct(callable $whatToDo, private $listener) { - $this->listener = $listener; + $this->parameters = []; $this->whatToDo = $whatToDo; } - public function execute() + public function execute(): mixed { $whatToDo = $this->whatToDo; + return $whatToDo(); } - public function afterSuccess() + public function afterSuccess(): void { $this->listener->methodWasCalled(__FUNCTION__); } - public function afterFailure(Exception $e) + public function afterFailure(\Exception $e): void { $this->listener->methodWasCalled(__FUNCTION__, $e); } - public function afterLastFailure(Exception $e) + public function afterLastFailure(\Exception $e): void { $this->listener->methodWasCalled(__FUNCTION__, $e); } - public function finalize(?Exception $e = null) + public function finalize(?\Exception $e = null): void { $this->listener->methodWasCalled(__FUNCTION__, $e); } diff --git a/spec/Recruiter/Infrastructure/Memory/MemoryLimitTest.php b/spec/Recruiter/Infrastructure/Memory/MemoryLimitTest.php index d552d154..52012ec7 100644 --- a/spec/Recruiter/Infrastructure/Memory/MemoryLimitTest.php +++ b/spec/Recruiter/Infrastructure/Memory/MemoryLimitTest.php @@ -1,4 +1,5 @@ expectException(MemoryLimitExceededException::class); $memoryLimit = new MemoryLimit(1); diff --git a/spec/Recruiter/Job/EventTest.php b/spec/Recruiter/Job/EventTest.php index bf81513a..33e5cb62 100644 --- a/spec/Recruiter/Job/EventTest.php +++ b/spec/Recruiter/Job/EventTest.php @@ -1,15 +1,16 @@ 'generic', - 'tags' =>[ + 'tags' => [ 1 => 'billing-notification', ], ]); @@ -17,11 +18,11 @@ public function testHasTagReturnsTrueWhenTheExportedJobContainsTheTag() $this->assertTrue($event->hasTag('billing-notification')); } - public function testHasTagReturnsFalseWhenTheExportedJobDoesNotContainTheTag() + public function testHasTagReturnsFalseWhenTheExportedJobDoesNotContainTheTag(): void { $event = new Event([ 'group' => 'generic', - 'tags' =>[ + 'tags' => [ 1 => 'billing-notification', ], ]); @@ -29,7 +30,7 @@ public function testHasTagReturnsFalseWhenTheExportedJobDoesNotContainTheTag() $this->assertFalse($event->hasTag('inexistant-tag')); } - public function testHasTagReturnsFalseWhenTheExportedJobDoesNotContainTags() + public function testHasTagReturnsFalseWhenTheExportedJobDoesNotContainTags(): void { $event = new Event([ ]); diff --git a/spec/Recruiter/Job/RepositoryTest.php b/spec/Recruiter/Job/RepositoryTest.php index 39153943..547d5212 100644 --- a/spec/Recruiter/Job/RepositoryTest.php +++ b/spec/Recruiter/Job/RepositoryTest.php @@ -1,38 +1,50 @@ recruiterDb = $factory->getMongoDb(MongoURI::from('mongodb://localhost:27017/recruiter'), []); + $this->recruiterDb = $factory->getMongoDb(MongoURI::fromEnvironment(), []); $this->recruiterDb->drop(); $this->repository = new Repository($this->recruiterDb); $this->clock = T\clock()->stop(); - $this->eventDispatcher = $this->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); } - public function tearDown(): void + protected function tearDown(): void { T\clock()->start(); } - public function testCountsQueuedJobsAsOfNow() + public function testCountsQueuedJobsAsOfNow(): void { $this->aJobToSchedule()->inGroup('generic')->inBackground()->execute(); $this->aJobToSchedule()->inGroup('generic')->inBackground()->execute(); @@ -42,7 +54,7 @@ public function testCountsQueuedJobsAsOfNow() $this->assertEquals(1, $this->repository->queued('fast-lane')); } - public function testCountsQueuedJobsWithCornerCaseTagging() + public function testCountsQueuedJobsWithCornerCaseTagging(): void { $this->aJobToSchedule()->inBackground()->execute(); $this->aJobToSchedule()->inGroup([])->inBackground()->execute(); @@ -52,7 +64,7 @@ public function testCountsQueuedJobsWithCornerCaseTagging() $this->assertEquals(4, $this->repository->queued('generic')); } - public function testCountsQueudJobsWithScheduledAtGreatherThanASpecificDate() + public function testCountsQueudJobsWithScheduledAtGreatherThanASpecificDate(): void { $this->aJobToSchedule()->inBackground()->execute(); $time1 = $this->clock->now(); @@ -63,19 +75,19 @@ public function testCountsQueudJobsWithScheduledAtGreatherThanASpecificDate() $this->repository->queued( 'generic', T\now(), - T\now()->before(T\hour(24)) - ) + T\now()->before(T\hour(24)), + ), ); } - public function testCountsPostponedJobs() + public function testCountsPostponedJobs(): void { $this->aJobToSchedule()->inBackground()->execute(); $this->aJobToSchedule()->scheduleIn(T\hour(24))->execute(); $this->assertEquals(1, $this->repository->postponed('generic')); } - public function testRecentHistory() + public function testRecentHistory(): void { $ed = $this->eventDispatcher; $this->repository->archive($this->aJob()->beforeExecution($ed)->afterExecution(42, $ed)); @@ -85,7 +97,7 @@ public function testRecentHistory() [ 'throughput' => [ 'value' => 3.0, - 'value_per_second' => 3/60.0, + 'value_per_second' => 3 / 60.0, ], 'latency' => [ 'average' => 5.0, @@ -94,11 +106,11 @@ public function testRecentHistory() 'average' => 0.0, ], ], - $this->repository->recentHistory() + $this->repository->recentHistory(), ); } - public function testCountQueuedJobsGroupingByASpecificKeyword() + public function testCountQueuedJobsGroupingByASpecificKeyword(): void { $workable1 = $this->workableMock(); $workable2 = $this->workableMock(); @@ -106,12 +118,14 @@ public function testCountQueuedJobsGroupingByASpecificKeyword() $workable1 ->expects($this->any()) ->method('export') - ->will($this->returnValue(['seller' => 'seller1'])); + ->will($this->returnValue(['seller' => 'seller1'])) + ; $workable2 ->expects($this->any()) ->method('export') - ->will($this->returnValue(['seller' => 'seller2'])); + ->will($this->returnValue(['seller' => 'seller2'])) + ; $job1 = $this->aJob($workable1); $job2 = $this->aJob($workable2); @@ -125,65 +139,65 @@ public function testCountQueuedJobsGroupingByASpecificKeyword() 'seller1' => '1', 'seller2' => '2', ], - $this->repository->queuedGroupedBy('workable.parameters.seller', []) + $this->repository->queuedGroupedBy('workable.parameters.seller', []), ); } - public function testGetDelayedScheduledJobs() + public function testGetDelayedScheduledJobs(): void { $workable1 = $this->workableMockWithCustomParameters([ - 'job1' => 'delayed_and_unpicked' + 'job1' => 'delayed_and_unpicked', ]); $workable2 = $this->workableMockWithCustomParameters([ - 'job2' => 'delayed_and_unpicked' + 'job2' => 'delayed_and_unpicked', ]); $workable3 = $this->workableMockWithCustomParameters([ - 'job3' => 'in_schedulation' + 'job3' => 'in_schedulation', ]); $this->aJobToSchedule($this->aJob($workable1))->inBackground()->execute(); $this->aJobToSchedule($this->aJob($workable2))->inBackground()->execute(); $lowerLimit = $this->clock->now(); - $fiveHoursInSeconds = 5*60*60; + $fiveHoursInSeconds = 5 * 60 * 60; $this->clock->driftForwardBySeconds($fiveHoursInSeconds); $this->aJobToSchedule($this->aJob($workable3))->inBackground()->execute(); $jobs = $this->repository->delayedScheduledJobs($lowerLimit); $jobsFounds = 0; foreach ($jobs as $job) { $this->assertEquals('delayed_and_unpicked', reset($job->export()['workable']['parameters'])); - $jobsFounds++; + ++$jobsFounds; } $this->assertEquals(2, $jobsFounds); } - public function testCountDelayedScheduledJobs() + public function testCountDelayedScheduledJobs(): void { $this->aJobToSchedule($this->aJob())->inBackground()->execute(); $this->aJobToSchedule($this->aJob())->inBackground()->execute(); $lowerLimit = $this->clock->now(); - $twoHoursInSeconds = 2*60*60; + $twoHoursInSeconds = 2 * 60 * 60; $this->clock->driftForwardBySeconds($twoHoursInSeconds); $this->aJobToSchedule($this->aJob())->inBackground()->execute(); $this->assertEquals(2, $this->repository->countDelayedScheduledJobs($lowerLimit)); } - public function testCountRecentJobsWithManyAttempts() + public function testCountRecentJobsWithManyAttempts(): void { $ed = $this->eventDispatcher; $this->repository->archive($this->aJob()->beforeExecution($ed)->beforeExecution($ed)->afterExecution(42, $ed)); $this->clock->now(); - $threeHoursInSeconds = 3*60*60; + $threeHoursInSeconds = 3 * 60 * 60; $this->clock->driftForwardBySeconds($threeHoursInSeconds); $lowerLimit = $this->clock->now(); $this->repository->archive($this->aJob()->beforeExecution($ed)->beforeExecution($ed)->afterExecution(42, $ed)); $this->repository->archive($this->aJob()->beforeExecution($ed)->beforeExecution($ed)->afterExecution(42, $ed)); - $oneHourInSeconds = 60*60; + $oneHourInSeconds = 60 * 60; $this->clock->driftForwardBySeconds($oneHourInSeconds); $createdAt = $endedAt = $this->clock->now(); $this->repository->save($this->jobMockWithAttemptsAndCustomParameters($createdAt, $endedAt)); $this->repository->save($this->jobMockWithAttemptsAndCustomParameters($createdAt, $endedAt)); $this->aJobToSchedule($this->aJob())->inBackground()->execute(); $upperLimit = $this->clock->now(); - $oneHourInSeconds = 60*60; + $oneHourInSeconds = 60 * 60; $this->clock->driftForwardBySeconds($oneHourInSeconds); $createdAt = $endedAt = $this->clock->now(); $this->repository->archive($this->aJob()->beforeExecution($ed)->beforeExecution($ed)->afterExecution(42, $ed)); @@ -191,35 +205,35 @@ public function testCountRecentJobsWithManyAttempts() $this->assertEquals(4, $this->repository->countRecentJobsWithManyAttempts($lowerLimit, $upperLimit)); } - public function testGetRecentJobsWithManyAttempts() + public function testGetRecentJobsWithManyAttempts(): void { $ed = $this->eventDispatcher; $workable1 = $this->workableMockWithCustomParameters([ - 'job1' => 'many_attempts_and_archived_but_too_old' + 'job1' => 'many_attempts_and_archived_but_too_old', ]); $workable2 = $this->workableMockWithCustomParameters([ - 'job2' => 'many_attempts_and_archived' + 'job2' => 'many_attempts_and_archived', ]); $workable3 = $this->workableMockWithCustomParameters([ - 'job3' => 'many_attempts_and_archived' + 'job3' => 'many_attempts_and_archived', ]); - $workable4 = [ - 'job4' => 'many_attempts_and_scheduled' + $workable4 = [ + 'job4' => 'many_attempts_and_scheduled', ]; - $workable5 = [ - 'job5' => 'many_attempts_and_scheduled' + $workable5 = [ + 'job5' => 'many_attempts_and_scheduled', ]; $workable6 = $this->workableMockWithCustomParameters([ - 'job6' => 'one_attempt_and_scheduled' + 'job6' => 'one_attempt_and_scheduled', ]); $this->repository->archive($this->aJob($workable1)->beforeExecution($ed)->beforeExecution($ed)->afterExecution(42, $ed)); $this->clock->now(); - $threeHoursInSeconds = 3*60*60; + $threeHoursInSeconds = 3 * 60 * 60; $this->clock->driftForwardBySeconds($threeHoursInSeconds); $lowerLimit = $this->clock->now(); $this->repository->archive($this->aJob($workable2)->beforeExecution($ed)->beforeExecution($ed)->afterExecution(42, $ed)); $this->repository->archive($this->aJob($workable3)->beforeExecution($ed)->beforeExecution($ed)->afterExecution(42, $ed)); - $oneHourInSeconds = 60*60; + $oneHourInSeconds = 60 * 60; $this->clock->driftForwardBySeconds($oneHourInSeconds); $createdAt = $endedAt = $this->clock->now(); $this->repository->save($this->jobMockWithAttemptsAndCustomParameters($createdAt, $endedAt, $workable4)); @@ -229,16 +243,16 @@ public function testGetRecentJobsWithManyAttempts() $jobs = $this->repository->recentJobsWithManyAttempts($lowerLimit, $upperLimit); $jobsFounds = 0; foreach ($jobs as $job) { - $this->assertRegExp( + $this->assertMatchesRegularExpression( '/many_attempts_and_archived|many_attempts_and_scheduled/', - reset($job->export()['workable']['parameters']) + reset($job->export()['workable']['parameters']), ); - $jobsFounds++; + ++$jobsFounds; } $this->assertEquals(4, $jobsFounds); } - public function testCountSlowRecentJobs() + public function testCountSlowRecentJobs(): void { $ed = $this->eventDispatcher; $elapseTimeInSecondsBeforeJobsExecutionEnd = 6; @@ -246,20 +260,20 @@ public function testCountSlowRecentJobs() $this->repository->save( $this->jobMockWithAttemptsAndCustomParameters( $createdAt, - $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')) - ) + $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')), + ), ); $archivedJobSlowExpired = $this->aJob()->beforeExecution($ed); $this->clock->driftForwardBySeconds($elapseTimeInSecondsBeforeJobsExecutionEnd); $archivedJobSlowExpired->afterExecution(42, $ed); - $threeHoursInSeconds = 3*60*60; + $threeHoursInSeconds = 3 * 60 * 60; $this->clock->driftForwardBySeconds($threeHoursInSeconds); $lowerLimit = $createdAt = $endedAt = $this->clock->now(); $this->repository->save( $this->jobMockWithAttemptsAndCustomParameters( $createdAt, - $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')) - ) + $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')), + ), ); $archivedJobSlow1 = $this->aJob()->beforeExecution($ed); $this->clock->driftForwardBySeconds($elapseTimeInSecondsBeforeJobsExecutionEnd); @@ -269,7 +283,7 @@ public function testCountSlowRecentJobs() $this->clock->driftForwardBySeconds($elapseTimeInSecondsBeforeJobsExecutionEnd); $archivedJobSlow2->afterExecution(42, $ed); $this->repository->archive($archivedJobSlow2); - $oneHourInSeconds = 60*60; + $oneHourInSeconds = 60 * 60; $this->clock->driftForwardBySeconds($oneHourInSeconds); $createdAt = $endedAt = $this->clock->now(); $archivedJobNotSlow = $this->aJob()->beforeExecution($ed)->afterExecution(42, $ed); @@ -277,31 +291,31 @@ public function testCountSlowRecentJobs() $this->repository->save( $this->jobMockWithAttemptsAndCustomParameters( $createdAt, - $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')) - ) + $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')), + ), ); - $oneHourInSeconds = 60*60; + $oneHourInSeconds = 60 * 60; $this->clock->driftForwardBySeconds($oneHourInSeconds); $upperLimit = $createdAt = $endedAt = $this->clock->now(); $this->repository->save( $this->jobMockWithAttemptsAndCustomParameters( $createdAt, - $endedAt - ) + $endedAt, + ), ); - $oneHourInSeconds = 60*60; + $oneHourInSeconds = 60 * 60; $this->clock->driftForwardBySeconds($oneHourInSeconds); $createdAt = $endedAt = $this->clock->now(); $this->repository->save( $this->jobMockWithAttemptsAndCustomParameters( $createdAt, - $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')) - ) + $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')), + ), ); $this->assertEquals(4, $this->repository->countSlowRecentJobs($lowerLimit, $upperLimit)); } - public function testGetSlowRecentJobs() + public function testGetSlowRecentJobs(): void { $ed = $this->eventDispatcher; $elapseTimeInSecondsBeforeJobsExecutionEnd = 6; @@ -310,81 +324,81 @@ public function testGetSlowRecentJobs() $this->jobMockWithAttemptsAndCustomParameters( $createdAt, $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')), - ['job_scheduled_old' => 'slow_jobs_scheduled_but_too_old'] - ) + ['job_scheduled_old' => 'slow_jobs_scheduled_but_too_old'], + ), ); $archivedJobSlowExpired = $this->aJob($this->workableMockWithCustomParameters([ - 'job_archived_old' => 'slow_job_archived_but_too_old' - ]))->beforeExecution($ed); + 'job_archived_old' => 'slow_job_archived_but_too_old', + ]))->beforeExecution($ed); $this->clock->driftForwardBySeconds($elapseTimeInSecondsBeforeJobsExecutionEnd); $archivedJobSlowExpired->afterExecution(42, $ed); - $threeHoursInSeconds = 3*60*60; + $threeHoursInSeconds = 3 * 60 * 60; $this->clock->driftForwardBySeconds($threeHoursInSeconds); $lowerLimit = $createdAt = $endedAt = $this->clock->now(); $this->repository->save( $this->jobMockWithAttemptsAndCustomParameters( $createdAt, $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')), - ['job1_scheduled' => 'slow_job_recent_scheduled'] - ) + ['job1_scheduled' => 'slow_job_recent_scheduled'], + ), ); $archivedJobSlow1 = $this->aJob($this->workableMockWithCustomParameters([ - 'job1_archived' => 'slow_job_recent_archived' - ]))->beforeExecution($ed); + 'job1_archived' => 'slow_job_recent_archived', + ]))->beforeExecution($ed); $archivedJobSlow2 = $this->aJob($this->workableMockWithCustomParameters([ - 'job2_archived' => 'slow_job_recent_archived' - ]))->beforeExecution($ed); + 'job2_archived' => 'slow_job_recent_archived', + ]))->beforeExecution($ed); $this->clock->driftForwardBySeconds($elapseTimeInSecondsBeforeJobsExecutionEnd); $archivedJobSlow1->afterExecution(41, $ed); $this->repository->archive($archivedJobSlow1); $archivedJobSlow2->afterExecution(42, $ed); $this->repository->archive($archivedJobSlow2); - $oneHourInSeconds = 60*60; + $oneHourInSeconds = 60 * 60; $this->clock->driftForwardBySeconds($oneHourInSeconds); $createdAt = $endedAt = $this->clock->now(); $archivedJobNotSlow = $this->aJob($this->workableMockWithCustomParameters([ - 'job_archived' => 'job_archived_not_slow' - ]))->beforeExecution($ed)->afterExecution(42, $ed); + 'job_archived' => 'job_archived_not_slow', + ]))->beforeExecution($ed)->afterExecution(42, $ed); $this->repository->save( $this->jobMockWithAttemptsAndCustomParameters( $createdAt, $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')), - ['job2_scheduled' => 'slow_job_recent_scheduled'] - ) + ['job2_scheduled' => 'slow_job_recent_scheduled'], + ), ); - $oneHourInSeconds = 60*60; + $oneHourInSeconds = 60 * 60; $this->clock->driftForwardBySeconds($oneHourInSeconds); $upperLimit = $createdAt = $endedAt = $this->clock->now(); $this->repository->save( $this->jobMockWithAttemptsAndCustomParameters( $createdAt, $endedAt, - ['job_scheduled' => 'job_recent_scheduled_slow'] - ) + ['job_scheduled' => 'job_recent_scheduled_slow'], + ), ); - $oneHourInSeconds = 60*60; + $oneHourInSeconds = 60 * 60; $this->clock->driftForwardBySeconds($oneHourInSeconds); $createdAt = $endedAt = $this->clock->now(); $this->repository->save( $this->jobMockWithAttemptsAndCustomParameters( $createdAt, $endedAt->after(Interval::parse($elapseTimeInSecondsBeforeJobsExecutionEnd . ' s')), - ['job3_scheduled' => 'slow_job_recent_scheduled'] - ) + ['job3_scheduled' => 'slow_job_recent_scheduled'], + ), ); $jobs = $this->repository->slowRecentJobs($lowerLimit, $upperLimit); $jobsFounds = 0; foreach ($jobs as $job) { - $this->assertRegExp( + $this->assertMatchesRegularExpression( '/slow_job_recent_archived|slow_job_recent_scheduled/', - reset($job->export()['workable']['parameters']) + reset($job->export()['workable']['parameters']), ); - $jobsFounds++; + ++$jobsFounds; } $this->assertEquals(4, $jobsFounds); } - public function testCleanOldArchived() + public function testCleanOldArchived(): void { $ed = $this->eventDispatcher; $this->repository->archive($this->aJob()->beforeExecution($ed)->afterExecution(42, $ed)); @@ -394,7 +408,7 @@ public function testCleanOldArchived() $this->assertEquals(0, $this->repository->countArchived()); } - public function testCleaningOfOldArchivedCanBeLimitedByTime() + public function testCleaningOfOldArchivedCanBeLimitedByTime(): void { $ed = $this->eventDispatcher; $this->repository->archive($this->aJob()->beforeExecution($ed)->afterExecution(42, $ed)); @@ -413,7 +427,8 @@ private function aJob($workable = null) } return Job::around($workable, $this->repository) - ->scheduleAt(T\now()->before(T\seconds(5))); + ->scheduleAt(T\now()->before(T\seconds(5))) + ; } private function aJobToSchedule($job = null) @@ -425,55 +440,60 @@ private function aJobToSchedule($job = null) return new JobToSchedule($job); } - private function workableMock() + private function workableMock(): MockObject&Workable { return $this - ->getMockBuilder('Recruiter\Workable') - ->getMock(); + ->getMockBuilder(Workable::class) + ->getMock() + ; } - private function workableMockWithCustomParameters($parameters) + private function workableMockWithCustomParameters(array $parameters): MockObject&Workable { $workable = $this->workableMock(); $workable ->expects($this->any()) ->method('export') - ->will($this->returnValue($parameters)); + ->willReturn($parameters) + ; + return $workable; } - private function jobExecutionMock($executionParameters) + private function jobExecutionMock(array $executionParameters): MockObject&JobExecution { $jobExecutionMock = $this - ->getMockBuilder('Recruiter\JobExecution') - ->getMock(); + ->getMockBuilder(JobExecution::class) + ->getMock() + ; $jobExecutionMock->expects($this->once()) ->method('export') - ->will($this->returnValue($executionParameters)); + ->will($this->returnValue($executionParameters)) + ; return $jobExecutionMock; } private function jobMockWithAttemptsAndCustomParameters( - Moment $createdAt = null, - Moment $endedAt = null, - array $workableParameters = null - ) { + ?Moment $createdAt = null, + ?Moment $endedAt = null, + ?array $workableParameters = null, + ): Job&MockObject { $parameters = [ '_id' => new ObjectId(), 'created_at' => T\MongoDate::from($createdAt), - "done" => false, - "attempts" => 10, - "group" => "generic", - "scheduled_at" => T\MongoDate::from($createdAt), - "last_execution" => [ - "started_at" => T\MongoDate::from($createdAt), - "ended_at" => T\MongoDate::from($endedAt) + 'done' => false, + 'attempts' => 10, + 'group' => 'generic', + 'scheduled_at' => T\MongoDate::from($createdAt), + 'last_execution' => [ + 'started_at' => T\MongoDate::from($createdAt), + 'ended_at' => T\MongoDate::from($endedAt), + ], + 'retry_policy' => [ + 'class' => DoNotDoItAgain::class, + 'parameters' => [], ], - "retry_policy" => [ - "class" => "Recruiter\\RetryPolicy\\DoNotDoItAgain", - "parameters" => [] - ] ]; if (!empty($workableParameters)) { @@ -482,12 +502,15 @@ private function jobMockWithAttemptsAndCustomParameters( $parameters['workable']['parameters'] = $workableParameters; } $job = $this - ->getMockBuilder('Recruiter\Job') + ->getMockBuilder(Job::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; $job->expects($this->once()) ->method('export') - ->will($this->returnValue($parameters)); + ->willReturn($parameters) + ; + return $job; } } diff --git a/spec/Recruiter/JobCallCustomMethodOnWorkableTest.php b/spec/Recruiter/JobCallCustomMethodOnWorkableTest.php index 1c3cb65e..6c8970f8 100644 --- a/spec/Recruiter/JobCallCustomMethodOnWorkableTest.php +++ b/spec/Recruiter/JobCallCustomMethodOnWorkableTest.php @@ -2,40 +2,48 @@ namespace Recruiter; -use Exception; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Recruiter\Job\Repository; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; class JobCallCustomMethodOnWorkableTest extends TestCase { - public function setUp(): void + private MockObject&Workable $workable; + private MockObject&Repository $repository; + private Job $job; + + protected function setUp(): void { $this->workable = $this - ->getMockBuilder('Recruiter\Workable') - ->setMethods(['export', 'import', 'asJobOf', 'send']) - ->getMock(); + ->getMockBuilder(DummyWorkableWithSendCustomMethod::class) + ->onlyMethods(['export', 'import', 'asJobOf', 'send']) + ->getMock() + ; $this->repository = $this - ->getMockBuilder('Recruiter\Job\Repository') + ->getMockBuilder(Repository::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; $this->job = Job::around($this->workable, $this->repository); } - public function testConfigureMethodToCallOnWorkable() + public function testConfigureMethodToCallOnWorkable(): void { $this->workable->expects($this->once())->method('send'); $this->job->methodToCallOnWorkable('send'); - $this->job->execute($this->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface')); + $this->job->execute($this->createMock(EventDispatcherInterface::class)); } - public function testRaiseExceptionWhenConfigureMethodToCallOnWorkableThatDoNotExists() + public function testRaiseExceptionWhenConfigureMethodToCallOnWorkableThatDoNotExists(): void { - $this->expectException(Exception::class); + $this->expectException(\Exception::class); $this->job->methodToCallOnWorkable('methodThatDoNotExists'); } - public function testCustomMethodIsSaved() + public function testCustomMethodIsSaved(): void { $this->job->methodToCallOnWorkable('send'); $jobExportedToDocument = $this->job->export(); @@ -44,7 +52,7 @@ public function testCustomMethodIsSaved() $this->assertEquals('send', $jobExportedToDocument['workable']['method']); } - public function testCustomMethodIsConservedAfterImport() + public function testCustomMethodIsConservedAfterImport(): void { $workable = new DummyWorkableWithSendCustomMethod(); $job = Job::around($workable, $this->repository); diff --git a/spec/Recruiter/JobSendEventsToWorkableTest.php b/spec/Recruiter/JobSendEventsToWorkableTest.php index ab72e71e..e2bc698b 100644 --- a/spec/Recruiter/JobSendEventsToWorkableTest.php +++ b/spec/Recruiter/JobSendEventsToWorkableTest.php @@ -2,39 +2,42 @@ namespace Recruiter; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Recruiter\Job\Event; use Recruiter\Job\EventListener; +use Recruiter\Job\Repository; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; class JobSendEventsToWorkableTest extends TestCase { - public function setUp(): void + private MockObject&Repository $repository; + private MockObject&EventDispatcherInterface $dispatcher; + + protected function setUp(): void { $this->repository = $this - ->getMockBuilder('Recruiter\Job\Repository') + ->getMockBuilder(Repository::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; - $this->dispatcher = $this->createMock( - 'Symfony\Component\EventDispatcher\EventDispatcherInterface' - ); + $this->dispatcher = $this->createMock(EventDispatcherInterface::class); } - public function testTakeRetryPolicyFromRetriableInstance() + public function testTakeRetryPolicyFromRetriableInstance(): void { - $listener = $this->createPartialMock('StdClass', ['onEvent']); - $listener - ->expects($this->exactly(3)) - ->method('onEvent') - ->withConsecutive( - [$this->equalTo('job.started'), $this->anything()], - [$this->equalTo('job.ended'), $this->anything()], - [$this->equalTo('job.failure.last'), $this->anything()] - ); + $listener = new EventListenerSpy(); $workable = new WorkableThatIsAlsoAnEventListener($listener); $job = Job::around($workable, $this->repository); $job->execute($this->dispatcher); + + $events = $listener->events; + $this->assertCount(3, $events); + $this->assertSame('job.started', $events[0][0]); + $this->assertSame('job.ended', $events[1][0]); + $this->assertSame('job.failure.last', $events[2][0]); } } @@ -42,18 +45,28 @@ class WorkableThatIsAlsoAnEventListener implements Workable, EventListener { use WorkableBehaviour; - public function __construct($listener) + public function __construct(private readonly EventListener $listener) { - $this->listener = $listener; + $this->parameters = []; } - public function onEvent($channel, Event $e) + public function onEvent($channel, Event $ev): void { - return $this->listener->onEvent($channel, $e); + $this->listener->onEvent($channel, $ev); } - public function execute() + public function execute(): never { throw new \Exception(); } } + +class EventListenerSpy implements EventListener +{ + public array $events = []; + + public function onEvent($channel, Event $ev): void + { + $this->events[] = [$channel, $ev]; + } +} diff --git a/spec/Recruiter/JobTakeRetryPolicyFromRetriableWorkableTest.php b/spec/Recruiter/JobTakeRetryPolicyFromRetriableWorkableTest.php index 4bafeda3..cd056f79 100644 --- a/spec/Recruiter/JobTakeRetryPolicyFromRetriableWorkableTest.php +++ b/spec/Recruiter/JobTakeRetryPolicyFromRetriableWorkableTest.php @@ -2,26 +2,35 @@ namespace Recruiter; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Timeless as T; -use Recruiter\RetryPolicy; +use Recruiter\Job\Repository; +use Recruiter\RetryPolicy\BaseRetryPolicy; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; class JobTakeRetryPolicyFromRetriableWorkableTest extends TestCase { - public function setUp(): void + private MockObject&Repository $repository; + private MockObject&EventDispatcherInterface $eventDispatcher; + + protected function setUp(): void { $this->repository = $this - ->getMockBuilder('Recruiter\Job\Repository') + ->getMockBuilder(Repository::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; - $this->eventDispatcher = $this - ->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); } - public function testTakeRetryPolicyFromRetriableInstance() + /** + * @throws Exception + */ + public function testTakeRetryPolicyFromRetriableInstance(): void { - $retryPolicy = $this->createMock('Recruiter\RetryPolicy\BaseRetryPolicy'); + $retryPolicy = $this->createMock(BaseRetryPolicy::class); $retryPolicy->expects($this->once())->method('schedule'); $workable = new WorkableThatIsAlsoRetriable($retryPolicy); @@ -35,9 +44,9 @@ class WorkableThatIsAlsoRetriable implements Workable, Retriable { use WorkableBehaviour; - public function __construct(RetryPolicy $retryWithPolicy) + public function __construct(private readonly RetryPolicy $retryWithPolicy) { - $this->retryWithPolicy = $retryWithPolicy; + $this->parameters = []; } public function retryWithPolicy(): RetryPolicy @@ -45,7 +54,7 @@ public function retryWithPolicy(): RetryPolicy return $this->retryWithPolicy; } - public function execute() + public function execute(): never { throw new \Exception(); } diff --git a/spec/Recruiter/JobTest.php b/spec/Recruiter/JobTest.php index 6df774bf..09f3cc8d 100644 --- a/spec/Recruiter/JobTest.php +++ b/spec/Recruiter/JobTest.php @@ -1,24 +1,30 @@ repository = $this - ->getMockBuilder('Recruiter\Job\Repository') + ->getMockBuilder(Repository::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; } - public function testRetryStatisticsOnFirstExecution() + public function testRetryStatisticsOnFirstExecution(): void { - $job = Job::around(new AlwaysFail, $this->repository); + $job = Job::around(new AlwaysFail(), $this->repository); $retryStatistics = $job->retryStatistics(); $this->assertIsArray($retryStatistics); $this->assertArrayHasKey('job_id', $retryStatistics); @@ -32,11 +38,11 @@ public function testRetryStatisticsOnFirstExecution() /** * @depends testRetryStatisticsOnFirstExecution */ - public function testRetryStatisticsOnSubsequentExecutions() + public function testRetryStatisticsOnSubsequentExecutions(): void { - $job = Job::around(new AlwaysFail, $this->repository); + $job = Job::around(new AlwaysFail(), $this->repository); // maybe make the argument optional - $job->execute($this->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface')); + $job->execute($this->createMock(EventDispatcherInterface::class)); $job = Job::import($job->export(), $this->repository); $retryStatistics = $job->retryStatistics(); $this->assertEquals(1, $retryStatistics['retry_number']); @@ -49,14 +55,14 @@ public function testRetryStatisticsOnSubsequentExecutions() $this->assertArrayHasKey('message', $lastExecution); $this->assertArrayHasKey('trace', $lastExecution); $this->assertEquals("Sorry, I'm good for nothing", $lastExecution['message']); - $this->assertRegexp("/.*AlwaysFail->execute.*/", $lastExecution['trace']); + $this->assertMatchesRegularExpression('/.*AlwaysFail->execute.*/', $lastExecution['trace']); } - public function testArrayAsGroupIsNotAllowed() + public function testArrayAsGroupIsNotAllowed(): void { - $this->expectException(RuntimeException::class); + $this->expectException(\RuntimeException::class); $memoryLimit = new MemoryLimit(1); - $job = Job::around(new AlwaysFail, $this->repository); + $job = Job::around(new AlwaysFail(), $this->repository); $job->inGroup(['test']); } } diff --git a/spec/Recruiter/JobToBePassedRetryStatisticsTest.php b/spec/Recruiter/JobToBePassedRetryStatisticsTest.php index cce51d76..ec72fd4f 100644 --- a/spec/Recruiter/JobToBePassedRetryStatisticsTest.php +++ b/spec/Recruiter/JobToBePassedRetryStatisticsTest.php @@ -2,27 +2,36 @@ namespace Recruiter; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Timeless as T; +use Recruiter\Job\Repository; use Recruiter\RetryPolicy\DoNotDoItAgain; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; class JobToBePassedRetryStatisticsTest extends TestCase { - public function setUp(): void + private MockObject&Repository $repository; + + protected function setUp(): void { $this->repository = $this - ->getMockBuilder('Recruiter\Job\Repository') + ->getMockBuilder(Repository::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; } - public function testTakeRetryPolicyFromRetriableInstance() + /** + * @throws Exception + */ + public function testTakeRetryPolicyFromRetriableInstance(): void { $workable = new WorkableThatUsesRetryStatistics(); $job = Job::around($workable, $this->repository); - $job->execute($this->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface')); - $this->assertTrue($job->done(), "Job requiring retry statistics was not executed correctly: " . var_export($job->export(), true)); + $job->execute($this->createMock(EventDispatcherInterface::class)); + $this->assertTrue($job->done(), 'Job requiring retry statistics was not executed correctly: ' . var_export($job->export(), true)); } } diff --git a/spec/Recruiter/JobToScheduleTest.php b/spec/Recruiter/JobToScheduleTest.php index 5de7f8f7..be23900f 100644 --- a/spec/Recruiter/JobToScheduleTest.php +++ b/spec/Recruiter/JobToScheduleTest.php @@ -2,138 +2,152 @@ namespace Recruiter; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Timeless as T; -use Recruiter\RetryPolicy; class JobToScheduleTest extends TestCase { - public function setUp(): void + private T\ClockInterface $clock; + private MockObject&Job $job; + + /** + * @throws Exception + */ + protected function setUp(): void { $this->clock = T\clock()->stop(); - $this->job = $this - ->getMockBuilder('Recruiter\Job') - ->disableOriginalConstructor() - ->getMock(); + $this->job = $this->createMock(Job::class); } - public function tearDown(): void + protected function tearDown(): void { $this->clock->start(); } - public function testInBackgroundShouldScheduleJobNow() + public function testInBackgroundShouldScheduleJobNow(): void { $this->job ->expects($this->once()) ->method('scheduleAt') ->with( - $this->equalTo($this->clock->now()) - ); + $this->equalTo($this->clock->now()), + ) + ; - (new JobToSchedule($this->job)) + new JobToSchedule($this->job) ->inBackground() - ->execute(); + ->execute() + ; } - public function testScheduledInShouldScheduleInCertainAmountOfTime() + public function testScheduledInShouldScheduleInCertainAmountOfTime(): void { $amountOfTime = T\minutes(10); $this->job ->expects($this->once()) ->method('scheduleAt') ->with( - $this->equalTo($amountOfTime->fromNow()) - ); + $this->equalTo($amountOfTime->fromNow()), + ) + ; - (new JobToSchedule($this->job)) + new JobToSchedule($this->job) ->scheduleIn($amountOfTime) - ->execute(); + ->execute() + ; } - public function testConfigureRetryPolicy() + public function testConfigureRetryPolicy(): void { $doNotDoItAgain = new RetryPolicy\DoNotDoItAgain(); $this->job ->expects($this->once()) ->method('retryWithPolicy') - ->with($doNotDoItAgain); + ->with($doNotDoItAgain) + ; - (new JobToSchedule($this->job)) + new JobToSchedule($this->job) ->inBackground() ->retryWithPolicy($doNotDoItAgain) - ->execute(); + ->execute() + ; } - public function tesShortcutToConfigureJobToNotBeRetried() + public function tesShortcutToConfigureJobToNotBeRetried(): void { $this->job ->expects($this->once()) ->method('retryWithPolicy') - ->with($this->isInstanceOf('Recruiter\RetryPolicy\DoNotDoItAgain')); + ->with($this->isInstanceOf(RetryPolicy\DoNotDoItAgain::class)) + ; - (new JobToSchedule($this->job)) + new JobToSchedule($this->job) ->inBackground() ->doNotRetry() - ->execute(); + ->execute() + ; } - public function testShouldNotExecuteJobWhenScheduled() + public function testShouldNotExecuteJobWhenScheduled(): void { $this->job ->expects($this->once()) - ->method('save'); + ->method('save') + ; $this->job ->expects($this->never()) - ->method('execute'); + ->method('execute') + ; - (new JobToSchedule($this->job)) + new JobToSchedule($this->job) ->inBackground() - ->execute(); + ->execute() + ; } - public function testShouldExecuteJobWhenNotScheduled() + public function testShouldExecuteJobWhenNotScheduled(): void { $this->job ->expects($this->never()) - ->method('scheduleAt'); + ->method('scheduleAt') + ; $this->job ->expects($this->once()) - ->method('execute'); + ->method('execute') + ; - (new JobToSchedule($this->job)) - ->execute( - $this->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface') - ); + new JobToSchedule($this->job)->execute(); } - public function testConfigureMethodToCallOnWorkableInJob() + public function testConfigureMethodToCallOnWorkableInJob(): void { $this->job ->expects($this->once()) ->method('methodToCallOnWorkable') - ->with('send'); + ->with('send') + ; - (new JobToSchedule($this->job)) - ->send(); + new JobToSchedule($this->job) + ->send() + ; } - public function testReturnsJobId() + public function testReturnsJobId(): void { $this->job ->expects($this->any()) ->method('id') - ->will($this->returnValue('42')); + ->will($this->returnValue('42')) + ; $this->assertEquals( '42', - (new JobToSchedule($this->job)) - ->execute( - $this->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface') - ) + new JobToSchedule($this->job)->execute(), ); } } diff --git a/spec/Recruiter/PickAvailableWorkersTest.php b/spec/Recruiter/PickAvailableWorkersTest.php index 5ca538a4..2170aa23 100644 --- a/spec/Recruiter/PickAvailableWorkersTest.php +++ b/spec/Recruiter/PickAvailableWorkersTest.php @@ -2,23 +2,30 @@ namespace Recruiter; -use ArrayIterator; +use MongoDB\BSON\Int64; use MongoDB\BSON\ObjectId; +use MongoDB\Collection; +use MongoDB\Driver\CursorInterface; +use MongoDB\Driver\Server; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class PickAvailableWorkersTest extends TestCase { - public function setUp(): void - { - $this->repository = $this - ->getMockBuilder('MongoDB\Collection') - ->disableOriginalConstructor() - ->getMock(); + private MockObject&Collection $repository; + private int $workersPerUnit; + /** + * @throws Exception + */ + protected function setUp(): void + { + $this->repository = $this->createMock(Collection::class); $this->workersPerUnit = 42; } - public function testNoWorkersAreFound() + public function testNoWorkersAreFound(): void { $this->withNoAvailableWorkers(); @@ -27,31 +34,31 @@ public function testNoWorkersAreFound() $this->assertEquals([], $picked); } - public function testFewWorkersWithNoSpecifiSkill() + public function testFewWorkersWithNoSpecificSkill(): void { $callbackHasBeenCalled = false; $this->withAvailableWorkers(['*' => 3]); $picked = Worker::pickAvailableWorkers($this->repository, $this->workersPerUnit); - list ($worksOn, $workers) = $picked[0]; + [$worksOn, $workers] = $picked[0]; $this->assertEquals('*', $worksOn); - $this->assertEquals(3, count($workers)); + $this->assertCount(3, $workers); } - public function testFewWorkersWithSameSkill() + public function testFewWorkersWithSameSkill(): void { $callbackHasBeenCalled = false; $this->withAvailableWorkers(['send-emails' => 3]); $picked = Worker::pickAvailableWorkers($this->repository, $this->workersPerUnit); - list ($worksOn, $workers) = $picked[0]; + [$worksOn, $workers] = $picked[0]; $this->assertEquals('send-emails', $worksOn); $this->assertEquals(3, count($workers)); } - public function testFewWorkersWithSomeDifferentSkills() + public function testFewWorkersWithSomeDifferentSkills(): void { $this->withAvailableWorkers(['send-emails' => 3, 'count-transactions' => 3]); $picked = Worker::pickAvailableWorkers($this->repository, $this->workersPerUnit); @@ -59,7 +66,7 @@ public function testFewWorkersWithSomeDifferentSkills() $allSkillsGiven = []; $totalWorkersGiven = 0; foreach ($picked as $pickedRow) { - list ($worksOn, $workers) = $pickedRow; + [$worksOn, $workers] = $pickedRow; $allSkillsGiven[] = $worksOn; $totalWorkersGiven += count($workers); } @@ -67,7 +74,7 @@ public function testFewWorkersWithSomeDifferentSkills() $this->assertEquals(6, $totalWorkersGiven); } - public function testMoreWorkersThanAllowedPerUnit() + public function testMoreWorkersThanAllowedPerUnit(): void { $this->withAvailableWorkers(['send-emails' => $this->workersPerUnit + 10]); @@ -75,21 +82,24 @@ public function testMoreWorkersThanAllowedPerUnit() $totalWorkersGiven = 0; foreach ($picked as $pickedRow) { - list ($worksOn, $workers) = $pickedRow; + [$worksOn, $workers] = $pickedRow; $totalWorkersGiven += count($workers); } $this->assertEquals($this->workersPerUnit, $totalWorkersGiven); } - private function withAvailableWorkers($workers) + /** + * @throws Exception + */ + private function withAvailableWorkers($workers): void { $workersThatShouldBeFound = []; foreach ($workers as $skill => $quantity) { - for ($counter = 0; $counter < $quantity; $counter++) { + for ($counter = 0; $counter < $quantity; ++$counter) { $workerId = new ObjectId(); - $workersThatShouldBeFound[(string)$workerId] = [ + $workersThatShouldBeFound[(string) $workerId] = [ '_id' => $workerId, - 'work_on' => $skill + 'work_on' => $skill, ]; } } @@ -97,7 +107,8 @@ private function withAvailableWorkers($workers) $this->repository ->expects($this->any()) ->method('find') - ->will($this->returnValue(new ArrayIterator($workersThatShouldBeFound))); + ->willReturn(new FakeCursor($workersThatShouldBeFound)) + ; } private function withNoAvailableWorkers() @@ -105,7 +116,8 @@ private function withNoAvailableWorkers() $this->repository ->expects($this->any()) ->method('find') - ->will($this->returnValue(new ArrayIterator([]))); + ->willReturn(new FakeCursor()) + ; } private function assertArrayAreEquals($expected, $given) @@ -115,3 +127,63 @@ private function assertArrayAreEquals($expected, $given) $this->assertEquals($expected, $given); } } + +class FakeCursor implements CursorInterface, \Iterator +{ + private array $data; + + public function __construct(array $data = []) + { + $this->data = array_values($data); + } + + public function getId(): Int64 + { + return new Int64(42); + } + + public function getServer(): Server + { + throw new \LogicException('Not implemented'); + } + + public function isDead(): bool + { + throw new \LogicException('Not implemented'); + } + + public function setTypeMap(array $typemap): void + { + throw new \LogicException('Not implemented'); + } + + public function toArray(): array + { + throw new \LogicException('Not implemented'); + } + + public function current(): object|array|null + { + return current($this->data); + } + + public function next(): void + { + next($this->data); + } + + public function key(): ?int + { + return key($this->data); + } + + public function valid(): bool + { + return null !== key($this->data); + } + + public function rewind(): void + { + reset($this->data); + } +} diff --git a/spec/Recruiter/RetryPolicy/ExponentialBackoffTest.php b/spec/Recruiter/RetryPolicy/ExponentialBackoffTest.php index fa451aad..0106c203 100644 --- a/spec/Recruiter/RetryPolicy/ExponentialBackoffTest.php +++ b/spec/Recruiter/RetryPolicy/ExponentialBackoffTest.php @@ -1,58 +1,66 @@ jobExecutedFor(1); $retryPolicy = new ExponentialBackoff(100, T\seconds(5)); $job->expects($this->once()) ->method('scheduleIn') - ->with(T\seconds(5)); + ->with(T\seconds(5)) + ; $retryPolicy->schedule($job); } - public function testAfterEachFailureDoublesTheAmountOfTimeToWaitBetweenRetries() + public function testAfterEachFailureDoublesTheAmountOfTimeToWaitBetweenRetries(): void { $job = $this->jobExecutedFor(2); $retryPolicy = new ExponentialBackoff(100, T\seconds(5)); $job->expects($this->once()) ->method('scheduleIn') - ->with(T\seconds(10)); + ->with(T\seconds(10)) + ; $retryPolicy->schedule($job); } - public function testAfterTooManyFailuresGivesUp() + public function testAfterTooManyFailuresGivesUp(): void { $job = $this->jobExecutedFor(101); $retryPolicy = new ExponentialBackoff(100, T\seconds(5)); $job->expects($this->once()) ->method('archive') - ->with('tried-too-many-times'); + ->with('tried-too-many-times') + ; $retryPolicy->schedule($job); } - public function testCanBeCreatedByTargetingAMaximumInterval() + public function testCanBeCreatedByTargetingAMaximumInterval(): void { $this->assertEquals( ExponentialBackoff::forAnInterval(1025, T\seconds(1)), - new ExponentialBackoff(10, 1) + new ExponentialBackoff(10, 1), ); } - private function jobExecutedFor($times) + private function jobExecutedFor(int $times): MockObject&JobAfterFailure { - $job = $this->getMockBuilder('Recruiter\JobAfterFailure')->disableOriginalConstructor()->getMock(); + $job = $this->getMockBuilder(JobAfterFailure::class)->disableOriginalConstructor()->getMock(); $job->expects($this->any()) ->method('numberOfAttempts') - ->will($this->returnValue($times)); + ->willReturn($times) + ; + return $job; } } diff --git a/spec/Recruiter/RetryPolicy/RetriableExceptionFilterTest.php b/spec/Recruiter/RetryPolicy/RetriableExceptionFilterTest.php index 3d2354b6..f0767844 100644 --- a/spec/Recruiter/RetryPolicy/RetriableExceptionFilterTest.php +++ b/spec/Recruiter/RetryPolicy/RetriableExceptionFilterTest.php @@ -2,132 +2,149 @@ namespace Recruiter\RetryPolicy; -use Exception; -use InvalidArgumentException; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Recruiter\JobAfterFailure; +use Recruiter\RetryPolicy; class RetriableExceptionFilterTest extends TestCase { - public function setUp(): void + private MockObject&RetryPolicy $filteredRetryPolicy; + + /** + * @throws Exception + */ + protected function setUp(): void { - $this->filteredRetryPolicy = $this->createMock('Recruiter\RetryPolicy'); + $this->filteredRetryPolicy = $this->createMock(RetryPolicy::class); } - public function testCallScheduleOnRetriableException() + /** + * @throws Exception + */ + public function testCallScheduleOnRetriableException(): void { - $exception = $this->createMock('Exception'); - $classOfException = get_class($exception); + $exception = $this->createMock(\Exception::class); + $classOfException = $exception::class; $filter = new RetriableExceptionFilter($this->filteredRetryPolicy, [$classOfException]); $this->filteredRetryPolicy ->expects($this->once()) - ->method('schedule'); + ->method('schedule') + ; $filter->schedule($this->jobFailedWithException($exception)); } - public function testDoNotCallScheduleOnNonRetriableException() + public function testDoNotCallScheduleOnNonRetriableException(): void { - $exception = $this->createMock('Exception'); - $classOfException = get_class($exception); + $exception = $this->createMock(\Exception::class); + $classOfException = $exception::class; $filter = new RetriableExceptionFilter($this->filteredRetryPolicy, [$classOfException]); $this->filteredRetryPolicy ->expects($this->never()) - ->method('schedule'); + ->method('schedule') + ; - $filter->schedule($this->jobFailedWithException(new Exception('Test'))); + $filter->schedule($this->jobFailedWithException(new \Exception('Test'))); } - public function testWhenExceptionIsNotRetriableThenArchiveTheJob() + public function testWhenExceptionIsNotRetriableThenArchiveTheJob(): void { - $exception = $this->createMock('Exception'); - $classOfException = get_class($exception); + $exception = $this->createMock(\Exception::class); + $classOfException = $exception::class; $filter = new RetriableExceptionFilter($this->filteredRetryPolicy, [$classOfException]); - $job = $this->jobFailedWithException(new Exception('Test')); + $job = $this->jobFailedWithException(new \Exception('Test')); $job->expects($this->once()) ->method('archive') - ->with('non-retriable-exception'); + ->with('non-retriable-exception') + ; $filter->schedule($job); } - public function testAllExceptionsAreRetriableByDefault() + public function testAllExceptionsAreRetriableByDefault(): void { $this->filteredRetryPolicy ->expects($this->once()) - ->method('schedule'); + ->method('schedule') + ; $filter = new RetriableExceptionFilter($this->filteredRetryPolicy); - $filter->schedule($this->jobFailedWithException(new Exception('Test'))); + $filter->schedule($this->jobFailedWithException(new \Exception('Test'))); } - public function testJobFailedWithSomethingThatIsNotAnException() + public function testJobFailedWithSomethingThatIsNotAnException(): void { $jobAfterFailure = $this->jobFailedWithException(null); $jobAfterFailure ->expects($this->once()) - ->method('archive'); + ->method('archive') + ; $filter = new RetriableExceptionFilter($this->filteredRetryPolicy); $filter->schedule($jobAfterFailure); } - public function testExportFilteredRetryPolicy() + public function testExportFilteredRetryPolicy(): void { $this->filteredRetryPolicy ->expects($this->once()) ->method('export') - ->will($this->returnValue(['key' => 'value'])); + ->will($this->returnValue(['key' => 'value'])) + ; $filter = new RetriableExceptionFilter($this->filteredRetryPolicy); $this->assertEquals( [ 'retriable_exceptions' => ['Exception'], - 'filtered_retry_policy' => [ - 'class' => get_class($this->filteredRetryPolicy), - 'parameters' => ['key' => 'value'] - ] + 'filtered_retry_policy' => [ + 'class' => $this->filteredRetryPolicy::class, + 'parameters' => ['key' => 'value'], + ], ], - $filter->export() + $filter->export(), ); } - public function testImportRetryPolicy() + public function testImportRetryPolicy(): void { $filteredRetryPolicy = new DoNotDoItAgain(); $filter = new RetriableExceptionFilter($filteredRetryPolicy); $exported = $filter->export(); $filter = RetriableExceptionFilter::import($exported); - $filter->schedule($this->jobFailedWithException(new Exception('Test'))); + $filter->schedule($this->jobFailedWithException(new \Exception('Test'))); $this->assertEquals($exported, $filter->export()); } - public function testRetriableExceptionsThatAreNotExceptions() + public function testRetriableExceptionsThatAreNotExceptions(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage("Only subclasses of Exception can be retriable exceptions, 'StdClass' is not"); $retryPolicy = new DoNotDoItAgain(); $notAnExceptionClass = 'StdClass'; new RetriableExceptionFilter($retryPolicy, [$notAnExceptionClass]); } - - private function jobFailedWithException($exception) + private function jobFailedWithException(mixed $exception): MockObject&JobAfterFailure { $jobAfterFailure = $this - ->getMockBuilder('Recruiter\JobAfterFailure') + ->getMockBuilder(JobAfterFailure::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; $jobAfterFailure ->expects($this->any()) ->method('causeOfFailure') - ->will($this->returnValue($exception)); + ->willReturn($exception) + ; return $jobAfterFailure; } diff --git a/spec/Recruiter/RetryPolicy/SelectByExceptionTest.php b/spec/Recruiter/RetryPolicy/SelectByExceptionTest.php index 06e272bf..26ef5e9d 100644 --- a/spec/Recruiter/RetryPolicy/SelectByExceptionTest.php +++ b/spec/Recruiter/RetryPolicy/SelectByExceptionTest.php @@ -1,31 +1,32 @@ when(InvalidArgumentException::class)->then(new DoNotDoItAgain()) - ->when(LogicException::class)->then(new DoNotDoItAgain()) - ->build(); + SelectByException::create() + ->when(\InvalidArgumentException::class)->then(new DoNotDoItAgain()) + ->when(\LogicException::class)->then(new DoNotDoItAgain()) + ->build() + ; - $this->assertInstanceOf(RetryPolicy::class, $retryPolicy); + $this->expectNotToPerformAssertions(); } - public function testCanBeExportedAndImported() + public function testCanBeExportedAndImported(): void { $retryPolicy = SelectByException::create() - ->when(InvalidArgumentException::class)->then(new DoNotDoItAgain()) - ->when(LogicException::class)->then(new DoNotDoItAgain()) - ->build(); + ->when(\InvalidArgumentException::class)->then(new DoNotDoItAgain()) + ->when(\LogicException::class)->then(new DoNotDoItAgain()) + ->build() + ; $retryPolicyExported = $retryPolicy->export(); $retryPolicyImported = SelectByException::import($retryPolicyExported); @@ -34,26 +35,30 @@ public function testCanBeExportedAndImported() $this->assertEquals($retryPolicyExported, $retryPolicyImported->export()); } - public function testSelectByException() + public function testSelectByException(): void { - $exception = new InvalidArgumentException('something'); + $exception = new \InvalidArgumentException('something'); $retryPolicy = new SelectByException([ - new RetriableException(get_class($exception), RetryForever::afterSeconds(10)) + new RetriableException($exception::class, RetryForever::afterSeconds(10)), ]); $job = $this->jobFailedWith($exception); $job->expects($this->once()) ->method('scheduleIn') - ->with(T\seconds(10)); + ->with(T\seconds(10)) + ; $retryPolicy->schedule($job); } - public function testDefaultDoNotSchedule() + /** + * @throws \Exception + */ + public function testDefaultDoNotSchedule(): void { - $exception = new Exception('something'); + $exception = new \Exception('something'); $retryPolicy = new SelectByException([ - new RetriableException(InvalidArgumentException::class, RetryForever::afterSeconds(10)) + new RetriableException(\InvalidArgumentException::class, RetryForever::afterSeconds(10)), ]); $job = $this->jobFailedWith($exception); @@ -63,12 +68,14 @@ public function testDefaultDoNotSchedule() $retryPolicy->schedule($job); } - private function jobFailedWith(Exception $exception) + private function jobFailedWith(\Throwable $exception): MockObject&JobAfterFailure { - $job = $this->getMockBuilder('Recruiter\JobAfterFailure')->disableOriginalConstructor()->getMock(); + $job = $this->getMockBuilder(JobAfterFailure::class)->disableOriginalConstructor()->getMock(); $job->expects($this->any()) ->method('causeOfFailure') - ->will($this->returnValue($exception)); + ->willReturn($exception) + ; + return $job; } } diff --git a/spec/Recruiter/RetryPolicy/TimeTableTest.php b/spec/Recruiter/RetryPolicy/TimeTableTest.php index d6c3fbf1..d2a8499d 100644 --- a/spec/Recruiter/RetryPolicy/TimeTableTest.php +++ b/spec/Recruiter/RetryPolicy/TimeTableTest.php @@ -2,15 +2,17 @@ namespace Recruiter\RetryPolicy; -use Exception; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Recruiter\Job; +use Recruiter\JobAfterFailure; use Timeless as T; class TimeTableTest extends TestCase { private $scheduler; - public function setUp(): void + protected function setUp(): void { $this->scheduler = new TimeTable([ '5 minutes ago' => '1 minute', @@ -19,52 +21,56 @@ public function setUp(): void ]); } - public function testShouldRescheduleInOneMinuteWhenWasCreatedLessThanFiveMinutesAgo() + public function testShouldRescheduleInOneMinuteWhenWasCreatedLessThanFiveMinutesAgo(): void { $expectedToBeScheduledAt = T\minute(1)->fromNow()->toSecondPrecision(); $wasCreatedAt = T\seconds(10)->ago(); $job = $this->givenJobThat($wasCreatedAt); $job->expects($this->once()) ->method('scheduleAt') - ->with($this->equalTo($expectedToBeScheduledAt)); + ->with($this->equalTo($expectedToBeScheduledAt)) + ; $this->scheduler->schedule($job); } - public function testShouldRescheduleInFiveMinutesWhenWasCreatedLessThanOneHourAgo() + public function testShouldRescheduleInFiveMinutesWhenWasCreatedLessThanOneHourAgo(): void { $expectedToBeScheduledAt = T\minutes(5)->fromNow()->toSecondPrecision(); $wasCreatedAt = T\minutes(30)->ago(); $job = $this->givenJobThat($wasCreatedAt); $job->expects($this->once()) ->method('scheduleAt') - ->with($this->equalTo($expectedToBeScheduledAt)); + ->with($this->equalTo($expectedToBeScheduledAt)) + ; $this->scheduler->schedule($job); } - public function testShouldRescheduleInFiveMinutesWhenWasCreatedLessThan24HoursAgo() + public function testShouldRescheduleInFiveMinutesWhenWasCreatedLessThan24HoursAgo(): void { $expectedToBeScheduledAt = T\hour(1)->fromNow()->toSecondPrecision(); $wasCreatedAt = T\hours(3)->ago(); $job = $this->givenJobThat($wasCreatedAt); $job->expects($this->once()) ->method('scheduleAt') - ->with($this->equalTo($expectedToBeScheduledAt)); + ->with($this->equalTo($expectedToBeScheduledAt)) + ; $this->scheduler->schedule($job); } - public function testShouldNotBeRescheduledWhenWasCreatedMoreThan24HoursAgo() + public function testShouldNotBeRescheduledWhenWasCreatedMoreThan24HoursAgo(): void { $job = $this->jobThatWasCreated('2 days ago'); $job->expects($this->never())->method('scheduleAt'); $this->scheduler->schedule($job); } - public function testIsLastRetryReturnTrueIfJobWasCreatedMoreThanLastTimeSpen() + public function testIsLastRetryReturnTrueIfJobWasCreatedMoreThanLastTimeSpen(): void { - $job = $this->createMock('Recruiter\Job'); + $job = $this->createMock(Job::class); $job->expects($this->any()) ->method('createdAt') - ->will($this->returnValue(T\hours(3)->ago())); + ->will($this->returnValue(T\hours(3)->ago())) + ; $tt = new TimeTable([ '1 minute ago' => '1 minute', @@ -73,60 +79,67 @@ public function testIsLastRetryReturnTrueIfJobWasCreatedMoreThanLastTimeSpen() $this->assertTrue($tt->isLastRetry($job)); } - public function testIsLastRetryReturnFalseIfJobWasCreatedLessThanLastTimeSpen() + public function testIsLastRetryReturnFalseIfJobWasCreatedLessThanLastTimeSpen(): void { - $job = $this->createMock('Recruiter\Job'); + $job = $this->createMock(Job::class); $job->expects($this->any()) ->method('createdAt') - ->will($this->returnValue(T\hours(3)->ago())); + ->will($this->returnValue(T\hours(3)->ago())) + ; $tt = new TimeTable([ '1 hour ago' => '1 minute', - '24 hours ago' => '1 minute' + '24 hours ago' => '1 minute', ]); $this->assertFalse($tt->isLastRetry($job)); } - public function testInvalidTimeTableBecauseTimeWindow() + public function testInvalidTimeTableBecauseTimeWindow(): void { - $this->expectException(Exception::class); + $this->expectException(\Exception::class); $tt = new TimeTable(['1 minute' => '1 second']); } - public function testInvalidTimeTableBecauseRescheduleTime() + public function testInvalidTimeTableBecauseRescheduleTime(): void { - $this->expectException(Exception::class); + $this->expectException(\Exception::class); $tt = new TimeTable(['1 minute ago' => '1 second ago']); } - public function testInvalidTimeTableBecauseRescheduleTimeIsGreaterThanTimeWindow() + public function testInvalidTimeTableBecauseRescheduleTimeIsGreaterThanTimeWindow(): void { - $this->expectException(Exception::class); + $this->expectException(\Exception::class); $tt = new TimeTable(['1 minute ago' => '2 minutes']); } - private function givenJobThat(T\Moment $wasCreatedAt) + private function givenJobThat(T\Moment $wasCreatedAt): MockObject&JobAfterFailure { - $job = $this->getMockBuilder('Recruiter\JobAfterFailure') + $job = $this->getMockBuilder(JobAfterFailure::class) ->disableOriginalConstructor() - ->setMethods(['createdAt', 'scheduleAt']) - ->getMock(); + ->onlyMethods(['createdAt', 'scheduleAt']) + ->getMock() + ; $job->expects($this->any()) ->method('createdAt') - ->will($this->returnValue($wasCreatedAt)); + ->willReturn($wasCreatedAt) + ; + return $job; } - private function jobThatWasCreated($relativeTime) + private function jobThatWasCreated(string $relativeTime): MockObject&JobAfterFailure { - $wasCreatedAt = T\Moment::fromTimestamp(strtotime($relativeTime), T\now()->seconds()); - $job = $this->getMockBuilder('Recruiter\JobAfterFailure') + $wasCreatedAt = T\Moment::fromTimestamp(strtotime($relativeTime)); + $job = $this->getMockBuilder(JobAfterFailure::class) ->disableOriginalConstructor() - ->setMethods(['createdAt', 'scheduleAt']) - ->getMock(); + ->onlyMethods(['createdAt', 'scheduleAt']) + ->getMock() + ; $job->expects($this->any()) ->method('createdAt') - ->will($this->returnValue($wasCreatedAt)); + ->willReturn($wasCreatedAt) + ; + return $job; } } diff --git a/spec/Recruiter/SchedulePolicy/CronTest.php b/spec/Recruiter/SchedulePolicy/CronTest.php index 51dece3b..3d6804c6 100644 --- a/spec/Recruiter/SchedulePolicy/CronTest.php +++ b/spec/Recruiter/SchedulePolicy/CronTest.php @@ -2,7 +2,6 @@ namespace Recruiter\SchedulePolicy; -use DateTime; use PHPUnit\Framework\TestCase; use Timeless\Moment; @@ -11,15 +10,15 @@ class CronTest extends TestCase /** * @dataProvider cronExpressions */ - public function testCronCanBeExportedAndImportedWithoutDataLoss(string $cronExpression, string $expectedDate) + public function testCronCanBeExportedAndImportedWithoutDataLoss(string $cronExpression, string $expectedDate): void { - $cron = new Cron($cronExpression, DateTime::createFromFormat('Y-m-d H:i:s', '2019-01-15 15:00:00')); + $cron = new Cron($cronExpression, \DateTime::createFromFormat('Y-m-d H:i:s', '2019-01-15 15:00:00')); $cron = Cron::import($cron->export()); $this->assertEquals( - Moment::fromDateTime(new DateTime($expectedDate)), + Moment::fromDateTime(new \DateTime($expectedDate)), $cron->next(), - 'calculated schedule time is: ' . $cron->next()->format() + 'calculated schedule time is: ' . $cron->next()->format(), ); } diff --git a/spec/Recruiter/TaggableWorkableTest.php b/spec/Recruiter/TaggableWorkableTest.php index 5479a37a..65e96aa4 100644 --- a/spec/Recruiter/TaggableWorkableTest.php +++ b/spec/Recruiter/TaggableWorkableTest.php @@ -2,21 +2,24 @@ namespace Recruiter; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Timeless as T; -use Recruiter\Taggable; +use Recruiter\Job\Repository; class TaggableWorkableTest extends TestCase { - public function setUp(): void + private MockObject&Repository $repository; + + protected function setUp(): void { $this->repository = $this - ->getMockBuilder('Recruiter\Job\Repository') + ->getMockBuilder(Repository::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; } - public function testWorkableExportsTags() + public function testWorkableExportsTags(): void { $workable = new WorkableTaggable(['a', 'b']); $job = Job::around($workable, $this->repository); @@ -26,7 +29,7 @@ public function testWorkableExportsTags() $this->assertEquals(['a', 'b'], $exported['tags']); } - public function testCanSetTagsOnJobs() + public function testCanSetTagsOnJobs(): void { $workable = new WorkableTaggable([]); $job = Job::around($workable, $this->repository); @@ -37,7 +40,7 @@ public function testCanSetTagsOnJobs() $this->assertEquals(['c'], $exported['tags']); } - public function testTagsAreMergedTogether() + public function testTagsAreMergedTogether(): void { $workable = new WorkableTaggable(['a', 'b']); $job = Job::around($workable, $this->repository); @@ -48,7 +51,7 @@ public function testTagsAreMergedTogether() $this->assertEquals(['a', 'b', 'c'], $exported['tags']); } - public function testTagsAreUnique() + public function testTagsAreUnique(): void { $workable = new WorkableTaggable(['c']); $job = Job::around($workable, $this->repository); @@ -59,7 +62,7 @@ public function testTagsAreUnique() $this->assertEquals(['c'], $exported['tags']); } - public function testEmptyTagsAreNotExported() + public function testEmptyTagsAreNotExported(): void { $workable = new WorkableTaggable([]); $job = Job::around($workable, $this->repository); @@ -68,7 +71,7 @@ public function testEmptyTagsAreNotExported() $this->assertArrayNotHasKey('tags', $exported); } - public function testTagsAreImported() + public function testTagsAreImported(): void { $workable = new WorkableTaggable(['a', 'b']); $job = Job::around($workable, $this->repository); @@ -93,24 +96,21 @@ class WorkableTaggable implements Workable, Taggable { use WorkableBehaviour; - private $tags; - - public function __construct(array $tags) + public function __construct(private array $tags) { - $this->tags = $tags; } - public function taggedAs() + public function taggedAs(): array { return $this->tags; } - public function export() + public function export(): array { return ['tags' => $this->tags]; } - public static function import($parameters) + public static function import(array $parameters): static { return new self($parameters['tags']); } diff --git a/spec/Recruiter/WaitStrategyTest.php b/spec/Recruiter/WaitStrategyTest.php index b721e1dd..62b3578f 100644 --- a/spec/Recruiter/WaitStrategyTest.php +++ b/spec/Recruiter/WaitStrategyTest.php @@ -7,33 +7,38 @@ class WaitStrategyTest extends TestCase { - public function setUp(): void + private T\Interval $waited; + private \Closure $howToWait; + private T\Interval $timeToWaitAtLeast; + private T\Interval $timeToWaitAtMost; + + protected function setUp(): void { - $this->waited = 0; - $this->howToWait = function($microseconds) { - $this->waited = T\milliseconds($microseconds/1000); + $this->waited = T\milliseconds(0); + $this->howToWait = function ($microseconds): void { + $this->waited = T\milliseconds($microseconds / 1000); }; $this->timeToWaitAtLeast = T\milliseconds(250); $this->timeToWaitAtMost = T\seconds(30); } - public function testStartsToWaitTheMinimumAmountOfTime() + public function testStartsToWaitTheMinimumAmountOfTime(): void { $ws = new WaitStrategy( $this->timeToWaitAtLeast, $this->timeToWaitAtMost, - $this->howToWait + $this->howToWait, ); $ws->wait(); $this->assertEquals($this->timeToWaitAtLeast, $this->waited); } - public function testBackingOffIncreasesTheIntervalExponentially() + public function testBackingOffIncreasesTheIntervalExponentially(): void { $ws = new WaitStrategy( $this->timeToWaitAtLeast, $this->timeToWaitAtMost, - $this->howToWait + $this->howToWait, ); $ws->wait(); $this->assertEquals($this->timeToWaitAtLeast, $this->waited); @@ -43,7 +48,7 @@ public function testBackingOffIncreasesTheIntervalExponentially() $this->assertEquals($this->timeToWaitAtLeast->multiplyBy(4), $this->waited); } - public function testBackingOffCannotIncreaseTheIntervalOverAMaximum() + public function testBackingOffCannotIncreaseTheIntervalOverAMaximum(): void { $ws = new WaitStrategy(T\seconds(1), T\seconds(2), $this->howToWait); $ws->backOff(); @@ -54,12 +59,12 @@ public function testBackingOffCannotIncreaseTheIntervalOverAMaximum() $this->assertEquals(T\seconds(2), $this->waited); } - public function testGoingForwardLowersTheSleepingPeriod() + public function testGoingForwardLowersTheSleepingPeriod(): void { $ws = new WaitStrategy( $this->timeToWaitAtLeast, $this->timeToWaitAtMost, - $this->howToWait + $this->howToWait, ); $ws->backOff(); $ws->goForward(); @@ -67,12 +72,12 @@ public function testGoingForwardLowersTheSleepingPeriod() $this->assertEquals($this->timeToWaitAtLeast, $this->waited); } - public function testTheSleepingPeriodCanBeResetToTheMinimum() + public function testTheSleepingPeriodCanBeResetToTheMinimum(): void { $ws = new WaitStrategy( $this->timeToWaitAtLeast, $this->timeToWaitAtMost, - $this->howToWait + $this->howToWait, ); $ws->backOff(); $ws->backOff(); @@ -83,12 +88,12 @@ public function testTheSleepingPeriodCanBeResetToTheMinimum() $this->assertEquals($this->timeToWaitAtLeast, $this->waited); } - public function testGoingForwardCannotLowerTheIntervalBelowMinimum() + public function testGoingForwardCannotLowerTheIntervalBelowMinimum(): void { $ws = new WaitStrategy( $this->timeToWaitAtLeast, $this->timeToWaitAtMost, - $this->howToWait + $this->howToWait, ); $ws->goForward(); $ws->goForward(); diff --git a/spec/Recruiter/Workable/FactoryMethodCommandTest.php b/spec/Recruiter/Workable/FactoryMethodCommandTest.php index 508e3991..05950dd4 100644 --- a/spec/Recruiter/Workable/FactoryMethodCommandTest.php +++ b/spec/Recruiter/Workable/FactoryMethodCommandTest.php @@ -1,34 +1,38 @@ myObject() - ->myMethod('answer', 42); + ->myMethod('answer', 42) + ; $this->assertEquals('42', $workable->execute()); } - public function testCanBeImportedAndExported() + public function testCanBeImportedAndExported(): void { $workable = FactoryMethodCommand::from('Recruiter\Workable\DummyFactory::create') ->myObject() - ->myMethod('answer', 42); + ->myMethod('answer', 42) + ; $this->assertEquals( $workable, - FactoryMethodCommand::import($workable->export()) + FactoryMethodCommand::import($workable->export()), ); } - public function testPassesRetryStatisticsAsAnAdditionalArgumentToTheLastMethodToCall() + public function testPassesRetryStatisticsAsAnAdditionalArgumentToTheLastMethodToCall(): void { $workable = FactoryMethodCommand::from('Recruiter\Workable\DummyFactory::create') ->myObject() - ->myNeedyMethod(); + ->myNeedyMethod() + ; $this->assertEquals(2, $workable->execute(['retry_number' => 2])); } } diff --git a/spec/Recruiter/Workable/ShellCommandTest.php b/spec/Recruiter/Workable/ShellCommandTest.php index c2ad1bc8..a5cbc362 100644 --- a/spec/Recruiter/Workable/ShellCommandTest.php +++ b/spec/Recruiter/Workable/ShellCommandTest.php @@ -1,22 +1,23 @@ assertEquals('42', $workable->execute()); } - public function testCanBeImportedAndExported() + public function testCanBeImportedAndExported(): void { $workable = ShellCommand::fromCommandLine('echo 42'); $this->assertEquals( $workable, - ShellCommand::import($workable->export()) + ShellCommand::import($workable->export()), ); } } diff --git a/spec/Recruiter/WorkablePersistenceTest.php b/spec/Recruiter/WorkablePersistenceTest.php index 95fba471..283b528f 100644 --- a/spec/Recruiter/WorkablePersistenceTest.php +++ b/spec/Recruiter/WorkablePersistenceTest.php @@ -6,12 +6,12 @@ class WorkablePersistenceTest extends TestCase { - public function testCanBeExportedAndImported() + public function testCanBeExportedAndImported(): void { $job = new SomethingWorkable(['key' => 'value']); $this->assertEquals( $job, - SomethingWorkable::import($job->export()) + SomethingWorkable::import($job->export()), ); } } diff --git a/spec/Recruiter/WorkerProcessTest.php b/spec/Recruiter/WorkerProcessTest.php index 8ef203fa..209ea21b 100644 --- a/spec/Recruiter/WorkerProcessTest.php +++ b/spec/Recruiter/WorkerProcessTest.php @@ -2,74 +2,85 @@ namespace Recruiter; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Recruiter\Worker\Process; +use Recruiter\Worker\Repository; +use Sink\BlackHole; class WorkerProcessTest extends TestCase { - public function setUp(): void + private int $pid; + private MockObject&Repository $repository; + + protected function setUp(): void { $this->pid = 4242; - $this->repository = $this->getMockBuilder('Recruiter\Worker\Repository') + $this->repository = $this->getMockBuilder(Repository::class) ->disableOriginalConstructor() - ->getMock(); + ->getMock() + ; } - public function testIfNotAliveWhenIsNotAliveReturnsItself() + public function testIfNotAliveWhenIsNotAliveReturnsItself(): void { $process = $this->givenWorkerProcessDead(); - $this->assertInstanceOf('Recruiter\Worker\Process', $process->ifDead()); + $this->assertInstanceOf(Process::class, $process->ifDead()); } - public function testIfNotAliveWhenIsAliveReturnsBlackHole() + public function testIfNotAliveWhenIsAliveReturnsBlackHole(): void { $process = $this->givenWorkerProcessAlive(); - $this->assertInstanceOf('Sink\BlackHole', $process->ifDead()); + $this->assertInstanceOf(BlackHole::class, $process->ifDead()); } - public function testRetireWorkerIfNotAlive() + public function testRetireWorkerIfNotAlive(): void { $this->repository ->expects($this->once()) ->method('retireWorkerWithPid') - ->with($this->pid); + ->with($this->pid) + ; $process = $this->givenWorkerProcessDead(); $process->cleanUp($this->repository); } - public function testDoNotRetireWorkerIfAlive() + public function testDoNotRetireWorkerIfAlive(): void { $this->repository ->expects($this->never()) ->method('retireWorkerWithPid') - ->with($this->pid); + ->with($this->pid) + ; $process = $this->givenWorkerProcessAlive(); $process->cleanUp($this->repository); } - - private function givenWorkerProcessAlive() + private function givenWorkerProcessAlive(): MockObject&Process { return $this->givenWorkerProcess(true); } - private function givenWorkerProcessDead() + private function givenWorkerProcessDead(): MockObject&Process { return $this->givenWorkerProcess(false); } - private function givenWorkerProcess($alive) + private function givenWorkerProcess(bool $alive): MockObject&Process { - $process = $this->getMockBuilder('Recruiter\Worker\Process') - ->setMethods(['isAlive']) + $process = $this->getMockBuilder(Process::class) + ->onlyMethods(['isAlive']) ->setConstructorArgs([$this->pid]) - ->getMock(); + ->getMock() + ; $process->expects($this->any()) ->method('isAlive') - ->will($this->returnValue($alive)); + ->willReturn($alive) + ; return $process; } diff --git a/spec/Sink/BlackHoleTest.php b/spec/Sink/BlackHoleTest.php index 82867707..2fa25654 100644 --- a/spec/Sink/BlackHoleTest.php +++ b/spec/Sink/BlackHoleTest.php @@ -6,61 +6,61 @@ class BlackHoleTest extends TestCase { - public function testMethodCall() + public function testMethodCall(): void { $instance = new BlackHole(); - $this->assertInstanceOf('Sink\BlackHole', $instance->whateverMethod()); + $this->assertInstanceOf(BlackHole::class, $instance->whateverMethod()); } - public function testGetter() + public function testGetter(): void { $instance = new BlackHole(); - $this->assertInstanceOf('Sink\BlackHole', $instance->whateverProperty); + $this->assertInstanceOf(BlackHole::class, $instance->whateverProperty); } - public function testSetterReturnsTheValue() + public function testSetterReturnsTheValue(): void { $instance = new BlackHole(); $this->assertEquals(42, $instance->whateverProperty = 42); } - public function testNothingIsSet() + public function testNothingIsSet(): void { $instance = new BlackHole(); $instance->whateverProperty = 42; $this->assertFalse(isset($instance->whateverProperty)); } - public function testToString() + public function testToString(): void { $instance = new BlackHole(); $this->assertEquals('', (string) $instance); } - public function testInvoke() + public function testInvoke(): void { $instance = new BlackHole(); - $this->assertInstanceOf('Sink\BlackHole', $instance()); + $this->assertInstanceOf(BlackHole::class, $instance()); } - public function testCallStatic() + public function testCallStatic(): void { $instance = BlackHole::whateverStaticMethod(); - $this->assertInstanceOf('Sink\BlackHole', $instance); + $this->assertInstanceOf(BlackHole::class, $instance); } - public function testIsIterableButItIsAlwaysEmpty() + public function testIsIterableButItIsAlwaysEmpty(): void { $instance = new BlackHole(); $this->assertEmpty(iterator_to_array($instance)); } - public function testIsAccessibleAsAnArrayAlwaysGetItself() + public function testIsAccessibleAsAnArrayAlwaysGetItself(): void { $instance = new BlackHole(); - $this->assertInstanceOf('Sink\BlackHole', $instance[42]); - $this->assertInstanceOf('Sink\BlackHole', $instance['aString']); - $this->assertInstanceOf('Sink\BlackHole', $instance[[1,2,3]]); + $this->assertInstanceOf(BlackHole::class, $instance[42]); + $this->assertInstanceOf(BlackHole::class, $instance['aString']); + $this->assertInstanceOf(BlackHole::class, $instance[[1, 2, 3]]); } /* public function testIsAccessibleAsAnArrayExists() */ diff --git a/spec/Timeless/IntervalFormatTest.php b/spec/Timeless/IntervalFormatTest.php index 5a697c11..208368bf 100644 --- a/spec/Timeless/IntervalFormatTest.php +++ b/spec/Timeless/IntervalFormatTest.php @@ -6,7 +6,7 @@ class IntervalFormatTest extends TestCase { - public function testFormatExtended() + public function testFormatExtended(): void { $this->assertEquals('4 milliseconds', milliseconds(4)->format('milliseconds')); $this->assertEquals('1 second', milliseconds(1000)->format('seconds')); @@ -19,7 +19,7 @@ public function testFormatExtended() $this->assertEquals('1 year', months(12)->format('years')); } - public function testFormatShort() + public function testFormatShort(): void { $this->assertEquals('4ms', milliseconds(4)->format('ms')); $this->assertEquals('1s', milliseconds(1000)->format('s')); diff --git a/spec/Timeless/IntervalParseTest.php b/spec/Timeless/IntervalParseTest.php index c3f8f210..bbd276d1 100644 --- a/spec/Timeless/IntervalParseTest.php +++ b/spec/Timeless/IntervalParseTest.php @@ -2,12 +2,11 @@ namespace Timeless; -use DateInterval; use PHPUnit\Framework\TestCase; class IntervalParseTest extends TestCase { - public function testParseExtendedFormat() + public function testParseExtendedFormat(): void { $this->assertEquals(milliseconds(4), Interval::parse('4 milliseconds')); $this->assertEquals(milliseconds(4), Interval::parse('4milliseconds')); @@ -66,7 +65,7 @@ public function testParseExtendedFormat() $this->assertEquals(years(1), Interval::parse('1 year')); } - public function testParseShortFormat() + public function testParseShortFormat(): void { $this->assertEquals(milliseconds(4), Interval::parse('4 ms')); $this->assertEquals(milliseconds(4), Interval::parse('4ms')); @@ -86,21 +85,21 @@ public function testParseShortFormat() $this->assertEquals(years(4), Interval::parse('4y')); } - public function testFromDateInterval() + public function testFromDateInterval(): void { - $this->assertEquals(days(2), Interval::fromDateInterval(new DateInterval('P2D'))); - $this->assertEquals(minutes(10), Interval::fromDateInterval(new DateInterval('PT10M'))); - $this->assertEquals(days(2)->add(minutes(10)), Interval::fromDateInterval(new DateInterval('P2DT10M'))); + $this->assertEquals(days(2), Interval::fromDateInterval(new \DateInterval('P2D'))); + $this->assertEquals(minutes(10), Interval::fromDateInterval(new \DateInterval('PT10M'))); + $this->assertEquals(days(2)->add(minutes(10)), Interval::fromDateInterval(new \DateInterval('P2DT10M'))); } - public function testNumberAsIntervalFormat() + public function testNumberAsIntervalFormat(): void { $this->expectException(InvalidIntervalFormat::class); $this->expectExceptionMessage("Maybe you mean '5 seconds' or something like that?"); Interval::parse(5); } - public function testBadString() + public function testBadString(): void { $this->expectException(InvalidIntervalFormat::class); Interval::parse('whatever'); diff --git a/spec/Timeless/MongoDateTest.php b/spec/Timeless/MongoDateTest.php index 093556e1..ad7bb26e 100644 --- a/spec/Timeless/MongoDateTest.php +++ b/spec/Timeless/MongoDateTest.php @@ -1,4 +1,5 @@ forAll( - Generator\choose(0, 1500 * 1000 * 1000) + Generator\choose(0, 1500 * 1000 * 1000), ) - ->then(function ($milliseconds) { + ->then(function ($milliseconds): void { $moment = new Moment($milliseconds); $this->assertEquals( $moment, - MongoDate::toMoment(MongoDate::from($moment)) + MongoDate::toMoment(MongoDate::from($moment)), ); - }); + }) + ; } } diff --git a/src/Recruiter/AlreadyRunningException.php b/src/Recruiter/AlreadyRunningException.php index a534409a..172e4bd2 100644 --- a/src/Recruiter/AlreadyRunningException.php +++ b/src/Recruiter/AlreadyRunningException.php @@ -2,8 +2,6 @@ namespace Recruiter; -use Exception; - -class AlreadyRunningException extends Exception +class AlreadyRunningException extends \Exception { } diff --git a/src/Recruiter/CannotRetireWorkerAtWorkException.php b/src/Recruiter/CannotRetireWorkerAtWorkException.php index 452d5359..5e7864cd 100644 --- a/src/Recruiter/CannotRetireWorkerAtWorkException.php +++ b/src/Recruiter/CannotRetireWorkerAtWorkException.php @@ -1,8 +1,7 @@ repository = $repository; } public function cleanArchived(Interval $gracePeriod) @@ -25,7 +19,7 @@ public function cleanArchived(Interval $gracePeriod) return $this->repository->cleanArchived($upperLimit); } - public function cleanScheduled(Interval $gracePeriod = null) + public function cleanScheduled(?Interval $gracePeriod = null) { $upperLimit = T\now(); if (!is_null($gracePeriod)) { diff --git a/src/Recruiter/Command/RecruiterJobCommand.php b/src/Recruiter/Command/RecruiterJobCommand.php index d5f12374..59854c62 100644 --- a/src/Recruiter/Command/RecruiterJobCommand.php +++ b/src/Recruiter/Command/RecruiterJobCommand.php @@ -1,25 +1,22 @@ recruiter = $recruiter; } - protected function configure() + protected function configure(): void { $this ->setName('recruiter:command') @@ -27,16 +24,19 @@ protected function configure() ->addArgument( 'shell_command', InputArgument::REQUIRED, - 'The command to run' + 'The command to run', ) ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { ShellCommand::fromCommandLine($input->getArgument('shell_command')) ->asJobOf($this->recruiter) ->inBackground() - ->execute(); + ->execute() + ; + + return self::SUCCESS; } } diff --git a/src/Recruiter/Factory.php b/src/Recruiter/Factory.php index ddf73c86..f863c0c5 100644 --- a/src/Recruiter/Factory.php +++ b/src/Recruiter/Factory.php @@ -3,13 +3,13 @@ namespace Recruiter; use MongoDB\Client; +use MongoDB\Database; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use Recruiter\Infrastructure\Persistence\Mongodb\URI; -use UnexpectedValueException; class Factory { - public function getMongoDb(URI $uri, array $options = []) + public function getMongoDb(URI $uri, array $options = []): Database { try { $optionsWithMajorityConcern = ['w' => 'majority']; @@ -22,19 +22,13 @@ public function getMongoDb(URI $uri, array $options = []) 'document' => 'array', 'root' => 'array', ], - ], $options) + ], $options), ); $client->listDatabases(); // in order to avoid lazy connections and catch eventually connection exceptions here + return $client->selectDatabase($uri->database()); } catch (DriverRuntimeException $e) { - throw new UnexpectedValueException( - sprintf( - "'No MongoDB running at '%s'", - $uri->__toString() - ), - $e->getCode(), - $e - ); + throw new \UnexpectedValueException(sprintf("'No MongoDB running at '%s'", $uri->__toString()), $e->getCode(), $e); } } } diff --git a/src/Recruiter/Finalizable.php b/src/Recruiter/Finalizable.php index ccdec82d..c7433dcc 100644 --- a/src/Recruiter/Finalizable.php +++ b/src/Recruiter/Finalizable.php @@ -1,17 +1,16 @@ factory = $factory; - $this->logger = $logger; } - protected function configure() + protected function configure(): void { $this ->setName('bko:analytics') @@ -55,18 +35,18 @@ protected function configure() 't', InputOption::VALUE_REQUIRED, 'HOSTNAME[:PORT][/DB] MongoDB coordinates', - 'mongodb://localhost:27017/recruiter' + (string) MongoURI::fromEnvironment(), ) ->addOption( 'group', 'g', InputOption::VALUE_REQUIRED, - 'limit analytics to a specific job group' + 'limit analytics to a specific job group', ) ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { /** @var string */ $target = $input->getOption('target'); @@ -88,7 +68,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ->setRows([array_values($analytic)]) ; - for ($i = 0; $i < count($analytic); $i++) { + for ($i = 0; $i < count($analytic); ++$i) { $table->setColumnStyle($i, $rightAligned); $table->setColumnWidth($i, $columnsWidth); } @@ -96,6 +76,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $table->render(); echo PHP_EOL; } + + return self::SUCCESS; } private function calculateColumnsWidth(array $analytics): int @@ -105,8 +87,8 @@ private function calculateColumnsWidth(array $analytics): int $maxColumns = max($maxColumns, count($analytic)); } - // casual constants, found by try and error - $terminalWidth = (new Terminal())->getWidth() - (($maxColumns + 2) * 2); + // casual constants, found by trial and error + $terminalWidth = new Terminal()->getWidth() - (($maxColumns + 2) * 2); return intval(floor($terminalWidth / $maxColumns)); } diff --git a/src/Recruiter/Infrastructure/Command/Bko/JobRecoverCommand.php b/src/Recruiter/Infrastructure/Command/Bko/JobRecoverCommand.php index c484c085..960ba347 100644 --- a/src/Recruiter/Infrastructure/Command/Bko/JobRecoverCommand.php +++ b/src/Recruiter/Infrastructure/Command/Bko/JobRecoverCommand.php @@ -1,9 +1,9 @@ factory = $factory; - $this->logger = $logger; } protected function configure() @@ -64,23 +39,23 @@ protected function configure() 't', InputOption::VALUE_REQUIRED, 'HOSTNAME[:PORT][/DB] MongoDB coordinates', - 'mongodb://localhost:27017/recruiter' + (string) MongoURI::fromEnvironment(), ) ->addOption( 'scheduleAt', 's', InputOption::VALUE_REQUIRED, - 're-scheduling the job at specific datetime' + 're-scheduling the job at specific datetime', ) ->addArgument( 'jobId', InputArgument::REQUIRED, - 'the id of the job in archived collection to be recovered' + 'the id of the job in archived collection to be recovered', ) ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { /** @var string */ $target = $input->getOption('target'); @@ -99,16 +74,19 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($input->getOption('scheduleAt')) { /** @var string */ $scheduleAt = $input->getOption('scheduleAt'); - $job->scheduleAt(Moment::fromDateTime(new DateTime($scheduleAt))); + $job->scheduleAt(Moment::fromDateTime(new \DateTime($scheduleAt))); } else { $job->scheduleAt(T\now()); } $job ->scheduledBy('recovering-archived-job', $archivedJobId, -1) - ->save(); + ->save() + ; $output->writeln("Job recovered, new job id is `{$job->id()}`"); + + return self::SUCCESS; } private function createJobFromAnArchivedJob(Job $archivedJob, JobRepository $repository): Job diff --git a/src/Recruiter/Infrastructure/Command/Bko/RemoveSchedulerCommand.php b/src/Recruiter/Infrastructure/Command/Bko/RemoveSchedulerCommand.php index f7798df2..df3aa187 100644 --- a/src/Recruiter/Infrastructure/Command/Bko/RemoveSchedulerCommand.php +++ b/src/Recruiter/Infrastructure/Command/Bko/RemoveSchedulerCommand.php @@ -1,4 +1,5 @@ factory = $factory; - $this->logger = $logger; } protected function configure() @@ -59,7 +40,7 @@ protected function configure() 't', InputOption::VALUE_REQUIRED, 'HOSTNAME[:PORT][/DB] MongoDB coordinates', - 'mongodb://localhost:27017/recruiter' + (string) MongoURI::fromEnvironment(), ) ; } @@ -72,12 +53,13 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->schedulerRepository = new SchedulerRepository($db); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $outputData = $this->buildOutputData(); if (!$outputData) { $output->writeln('There are no schedulers yet.'); - return null; + + return self::SUCCESS; } $this->printTable($outputData, $output); @@ -89,15 +71,18 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->schedulerRepository->deleteByUrn($selectedUrn); $this->logger->info("[Recruiter] the scheduler with urn `$selectedUrn` was deleted!"); } + + return self::SUCCESS; } private function selectUrnToDelete(array $urns, InputInterface $input, OutputInterface $output) { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ChoiceQuestion( 'Please select the scheduler which you want delete', $urns, - null + null, ); $question->setErrorMessage('scheduler %s is invalid.'); @@ -111,7 +96,7 @@ private function selectUrnToDelete(array $urns, InputInterface $input, OutputInt return $selectedUrn; } - private function printTable(array $data, OutputInterface $output) + private function printTable(array $data, OutputInterface $output): void { $rows = []; foreach ($data as $row) { @@ -129,13 +114,13 @@ private function printTable(array $data, OutputInterface $output) echo PHP_EOL; } - protected function buildOutputData() + protected function buildOutputData(): ?array { $outputData = []; $i = 0; $schedulers = $this->schedulerRepository->all(); - if (! $schedulers) { + if (!$schedulers) { return null; } @@ -144,7 +129,7 @@ protected function buildOutputData() $info = [ 'createdAt' => $data['created_at']->toDateTime()->format('c'), - 'lastScheduling' => ($data['last_scheduling']['scheduled_at'])->toDateTime()->format('c'), + 'lastScheduling' => $data['last_scheduling']['scheduled_at']->toDateTime()->format('c'), 'workable' => $data['job']['workable']['class'], 'policy' => $scheduler->schedulePolicy()->export(), ]; @@ -164,7 +149,7 @@ protected function buildOutputData() } $outputData[] = [ - '' => "" . $i++ . "", + '' => '' . $i++ . '', 'urn' => $data['urn'], 'info' => $infoString, ]; diff --git a/src/Recruiter/Infrastructure/Command/CleanerCommand.php b/src/Recruiter/Infrastructure/Command/CleanerCommand.php index 6a83f108..ee0ff3d3 100644 --- a/src/Recruiter/Infrastructure/Command/CleanerCommand.php +++ b/src/Recruiter/Infrastructure/Command/CleanerCommand.php @@ -1,74 +1,39 @@ factory = $factory; - $this->logger = $logger; } public static function toRobustCommand(Factory $factory, LoggerInterface $logger): RobustCommandRunner @@ -84,7 +49,7 @@ public function execute(): bool $this->log(sprintf( '[%s] cleaned up %d old jobs from the archive' . PHP_EOL, $memoryUsage->format(), - $numberOfJobsCleaned + $numberOfJobsCleaned, ), LogLevel::INFO); $this->log(sprintf('going to sleep for %sms', $this->waitStrategy->current()), LogLevel::DEBUG); @@ -94,9 +59,10 @@ public function execute(): bool return $numberOfJobsCleaned > 0; } - public function shutdown(?Throwable $e = null): bool + public function shutdown(?\Throwable $e = null): bool { $this->log('ok, see you space cowboy...', LogLevel::INFO); + return true; } @@ -127,8 +93,10 @@ public function description(): string public function definition(): InputDefinition { + $defaultMongoUri = (string) MongoURI::fromEnvironment(); + return new InputDefinition([ - new InputOption('target', 't', InputOption::VALUE_REQUIRED, 'HOSTNAME[:PORT][/DB] MongoDB coordinates', 'mongodb://localhost:27017/recruiter'), + new InputOption('target', 't', InputOption::VALUE_REQUIRED, 'HOSTNAME[:PORT][/DB] MongoDB coordinates', $defaultMongoUri), new InputOption('clean-after', 'c', InputOption::VALUE_REQUIRED, 'delete jobs after :period', '5days'), new InputOption('wait-at-least', null, InputOption::VALUE_REQUIRED, 'Time to wait at least before to search for jobs to clear', '1m'), new InputOption('wait-at-most', null, InputOption::VALUE_REQUIRED, 'Upper limit of time to wait before next polling', '3m'), @@ -146,7 +114,7 @@ public function init(InputInterface $input): void $this->waitStrategy = new ExponentialBackoffStrategy( Interval::parse($input->getOption('wait-at-least'))->ms(), - Interval::parse($input->getOption('wait-at-most'))->ms() + Interval::parse($input->getOption('wait-at-most'))->ms(), ); $this->memoryLimit = new MemoryLimit($input->getOption('memory-limit')); $this->gracePeriod = Interval::parse($input->getOption('clean-after')); @@ -168,7 +136,7 @@ private function log(string $message, string $level = LogLevel::DEBUG): void 'program' => $this->name(), 'datetime' => date('c'), 'pid' => posix_getpid(), - ] + ], ); } } diff --git a/src/Recruiter/Infrastructure/Command/RecruiterCommand.php b/src/Recruiter/Infrastructure/Command/RecruiterCommand.php index 8d06743c..287ab84c 100644 --- a/src/Recruiter/Infrastructure/Command/RecruiterCommand.php +++ b/src/Recruiter/Infrastructure/Command/RecruiterCommand.php @@ -1,11 +1,14 @@ factory = $factory; - $this->logger = $logger; + private Recruiter $recruiter; + private Interval $consideredDeadAfter; + private LeadershipStrategy $leadershipStrategy; + private WaitStrategy $waitStrategy; + private MemoryLimit $memoryLimit; + + public function __construct(private readonly Factory $factory, private readonly LoggerInterface $logger) + { } public static function toRobustCommand(Factory $factory, LoggerInterface $logger): RobustCommandRunner @@ -89,7 +52,7 @@ public function execute(): bool return count($assignment) > 0; } - private function rollbackLockedJobs() + private function rollbackLockedJobs(): void { $rollbackStartAt = microtime(true); $rolledBack = $this->recruiter->rollbackLockedJobs(); @@ -102,7 +65,7 @@ private function rollbackLockedJobs() private function assignJobsToWorkers(): array { $pickStartAt = microtime(true); - list($assignment, $actualNumber) = $this->recruiter->assignJobsToWorkers(); + [$assignment, $actualNumber] = $this->recruiter->assignJobsToWorkers(); $pickEndAt = microtime(true); foreach ($assignment as $worker => $job) { $this->log(sprintf(' tried to assign job `%s` to worker `%s`', $job, $worker), LogLevel::INFO); @@ -114,7 +77,7 @@ private function assignJobsToWorkers(): array $memoryUsage->format(), count($assignment), ($pickEndAt - $pickStartAt) * 1000, - $actualNumber + $actualNumber, ), LogLevel::DEBUG); $this->memoryLimit->ensure($memoryUsage); @@ -128,27 +91,27 @@ private function scheduleRepeatableJobs(): void $this->recruiter->scheduleRepeatableJobs(); $creationEndAt = microtime(true); - //FIXME:! log every job created? + // FIXME:! log every job created? /* foreach ($assignment as $worker => $job) { */ /* $this->log(sprintf(' tried to assign job `%s` to worker `%s`', $job, $worker)); */ /* } */ $this->log(sprintf( 'creation of jobs from crontab in %fms', - ($creationEndAt - $creationStartAt) * 1000 + ($creationEndAt - $creationStartAt) * 1000, )); } - private function retireDeadWorkers() + private function retireDeadWorkers(): void { $unlockedJobs = $this->recruiter->retireDeadWorkers( - new DateTimeImmutable(), - $this->consideredDeadAfter + new \DateTimeImmutable(), + $this->consideredDeadAfter, ); $this->log(sprintf('unlocked %d jobs due to dead workers', $unlockedJobs), LogLevel::DEBUG); } - public function shutdown(?Throwable $e = null): bool + public function shutdown(?\Throwable $e = null): bool { $this->recruiter->bye(); $this->log('ok, see you space cowboy...', LogLevel::INFO); @@ -183,8 +146,10 @@ public function description(): string public function definition(): InputDefinition { + $defaultMongoUri = (string) MongoURI::fromEnvironment(); + return new InputDefinition([ - new InputOption('target', 't', InputOption::VALUE_REQUIRED, 'HOSTNAME[:PORT][/DB] MongoDB coordinates', 'mongodb://localhost:27017/recruiter'), + new InputOption('target', 't', InputOption::VALUE_REQUIRED, 'HOSTNAME[:PORT][/DB] MongoDB coordinates', $defaultMongoUri), new InputOption('backoff-to', 'b', InputOption::VALUE_REQUIRED, 'Upper limit of time to wait before next polling (milliseconds)', '1600ms'), new InputOption('backoff-from', 'f', InputOption::VALUE_REQUIRED, 'Time to wait at least before to search for new jobs (milliseconds)', '200ms'), new InputOption('lease-time', 'l', InputOption::VALUE_REQUIRED, 'Maximum time to hold a lock before a refresh', '60s'), @@ -206,7 +171,7 @@ public function init(InputInterface $input): void $this->waitStrategy = new ExponentialBackoffStrategy( Interval::parse($input->getOption('backoff-from'))->ms(), - Interval::parse($input->getOption('backoff-to'))->ms() + Interval::parse($input->getOption('backoff-to'))->ms(), ); $this->consideredDeadAfter = Interval::parse($input->getOption('considered-dead-after')); @@ -233,7 +198,7 @@ private function log(string $message, string $level = LogLevel::DEBUG): void 'program' => $this->name(), 'datetime' => date('c'), 'pid' => posix_getpid(), - ] + ], ); } diff --git a/src/Recruiter/Infrastructure/Command/WorkerCommand.php b/src/Recruiter/Infrastructure/Command/WorkerCommand.php index 45de89ad..81927a4a 100644 --- a/src/Recruiter/Infrastructure/Command/WorkerCommand.php +++ b/src/Recruiter/Infrastructure/Command/WorkerCommand.php @@ -1,21 +1,20 @@ factory = $factory; - $this->logger = $logger; } public static function toRobustCommand(Factory $factory, LoggerInterface $logger): RobustCommandRunner @@ -81,7 +67,7 @@ public function execute(): bool return (bool) $doneSomeWork; } - public function shutdown(?Throwable $e = null): bool + public function shutdown(?\Throwable $e = null): bool { if ($this->worker->retireIfNotAssigned()) { $this->log(sprintf('worker `%s` retired', $this->worker->id()), LogLevel::INFO); @@ -119,8 +105,10 @@ public function description(): string public function definition(): InputDefinition { + $defaultMongoUri = (string) MongoURI::fromEnvironment(); + return new InputDefinition([ - new InputOption('target', 't', InputOption::VALUE_REQUIRED, 'HOSTNAME[:PORT][/DB] MongoDB coordinates', 'mongodb://localhost:27017/recruiter'), + new InputOption('target', 't', InputOption::VALUE_REQUIRED, 'HOSTNAME[:PORT][/DB] MongoDB coordinates', $defaultMongoUri), new InputOption('backoff-to', 'b', InputOption::VALUE_REQUIRED, 'Upper limit of time to wait before next polling', '6400ms'), new InputOption('backoff-from', null, InputOption::VALUE_REQUIRED, 'Time to wait at least before to search for new jobs', '200ms'), new InputOption('memory-limit', 'm', InputOption::VALUE_REQUIRED, 'Maximum amount of memory allocable', '64MB'), @@ -138,7 +126,7 @@ public function init(InputInterface $input): void $this->waitStrategy = new ExponentialBackoffStrategy( Interval::parse($input->getOption('backoff-from'))->ms(), - Interval::parse($input->getOption('backoff-to'))->ms() + Interval::parse($input->getOption('backoff-to'))->ms(), ); $memoryLimit = new MemoryLimit($input->getOption('memory-limit')); @@ -170,7 +158,7 @@ private function log(string $message, string $level = LogLevel::DEBUG): void 'datetime' => date('c'), 'pid' => posix_getpid(), 'workerId' => (string) $this->worker->id(), - ] + ], ); } diff --git a/src/Recruiter/Infrastructure/Filesystem/BootstrapFile.php b/src/Recruiter/Infrastructure/Filesystem/BootstrapFile.php index 9f12e88b..860d6fb1 100644 --- a/src/Recruiter/Infrastructure/Filesystem/BootstrapFile.php +++ b/src/Recruiter/Infrastructure/Filesystem/BootstrapFile.php @@ -1,29 +1,24 @@ filePath = $this->validate($filePath); + $this->validate($filePath); } - public static function fromFilePath(string $filePath): Self + public static function fromFilePath(string $filePath): self { - return new Static($filePath); + return new static($filePath); } public function load(Recruiter $recruiter) @@ -31,27 +26,19 @@ public function load(Recruiter $recruiter) return require $this->filePath; } - private function validate($filePath): string + private function validate(string $filePath): void { if (!file_exists($filePath)) { $this->throwBecauseFile($filePath, "doesn't exists"); } if (!is_readable($filePath)) { - $this->throwBecauseFile($filePath, "is not readable"); + $this->throwBecauseFile($filePath, 'is not readable'); } - - return $filePath; } - private function throwBecauseFile($filePath, $reason) + private function throwBecauseFile(string $filePath, string $reason): never { - throw new UnexpectedValueException( - sprintf( - "Bootstrap file has an invalid value: file '%s' %s", - $filePath, - $reason - ) - ); + throw new \UnexpectedValueException(sprintf("Bootstrap file has an invalid value: file '%s' %s", $filePath, $reason)); } } diff --git a/src/Recruiter/Infrastructure/Memory/MemoryLimit.php b/src/Recruiter/Infrastructure/Memory/MemoryLimit.php index e59fd636..e04e05ab 100644 --- a/src/Recruiter/Infrastructure/Memory/MemoryLimit.php +++ b/src/Recruiter/Infrastructure/Memory/MemoryLimit.php @@ -1,13 +1,13 @@ limit = ByteUnits\parse($limit); } catch (ByteUnits\ParseException $e) { - throw new UnexpectedValueException( - sprintf("Memory limit '%s' is an invalid value: %s", $limit, $e->getMessage()) - ); + throw new \UnexpectedValueException(sprintf("Memory limit '%s' is an invalid value: %s", $limit, $e->getMessage())); } } @@ -28,11 +26,7 @@ public function ensure($used) { $used = ByteUnits\box($used); if ($used->isGreaterThan($this->limit)) { - throw new MemoryLimitExceededException(sprintf( - 'Memory limit reached, %s is more than the force limit of %s', - $used->format(), - $this->limit->format() - )); + throw new MemoryLimitExceededException(sprintf('Memory limit reached, %s is more than the force limit of %s', $used->format(), $this->limit->format())); } } } diff --git a/src/Recruiter/Infrastructure/Memory/MemoryLimitExceededException.php b/src/Recruiter/Infrastructure/Memory/MemoryLimitExceededException.php index 8188be2c..4516250a 100644 --- a/src/Recruiter/Infrastructure/Memory/MemoryLimitExceededException.php +++ b/src/Recruiter/Infrastructure/Memory/MemoryLimitExceededException.php @@ -1,9 +1,9 @@ uri = $uri; + return self::from(getenv('MONGODB_URI')); } - public static function from(?string $uri): self + public static function from(string|self|null $uri): self { + if ($uri instanceof self) { + return $uri; + } + if (!$uri) { $uri = self::DEFAULT_URI; } @@ -41,7 +40,7 @@ public function database(): string return substr($parsed['path'], 1); } - public function __toString() + public function __toString(): string { return $this->uri; } diff --git a/src/Recruiter/Job.php b/src/Recruiter/Job.php index ba95b6a7..ef8af24f 100644 --- a/src/Recruiter/Job.php +++ b/src/Recruiter/Job.php @@ -1,32 +1,20 @@ retryWithPolicy() : new RetryPolicy\DoNotDoItAgain(), new JobExecution(), - $repository + $repository, ); } - public static function import($document, Repository $repository) + public static function import($document, Repository $repository): self { return new self( $document, WorkableInJob::import($document), RetryPolicyInJob::import($document), JobExecution::import($document), - $repository + $repository, ); } - public function __construct($status, Workable $workable, RetryPolicy $retryPolicy, JobExecution $lastJobExecution, Repository $repository) - { - $this->status = $status; - $this->workable = $workable; - $this->retryPolicy = $retryPolicy; - $this->lastJobExecution = $lastJobExecution; - $this->repository = $repository; + public function __construct( + private array $status, + private readonly Workable $workable, + private RetryPolicy $retryPolicy, + private JobExecution $lastJobExecution, + private readonly Repository $repository, + ) { } public function id() @@ -63,7 +51,7 @@ public function id() return $this->status['_id']; } - public function createdAt() + public function createdAt(): Moment { return T\MongoDate::toMoment($this->status['created_at']); } @@ -76,10 +64,14 @@ public function numberOfAttempts() public function retryWithPolicy(RetryPolicy $retryPolicy) { $this->retryPolicy = $retryPolicy; + return $this; } - public function taggedAs(array $tags) + /** + * @return $this + */ + public function taggedAs(array $tags): static { if (!empty($tags)) { $this->status['tags'] = $tags; @@ -88,34 +80,47 @@ public function taggedAs(array $tags) return $this; } - public function inGroup($group) + public function inGroup(array|string $group): static { if (is_array($group)) { - throw new RuntimeException( + throw new \RuntimeException( "Group can be only single string, for other uses use `taggedAs` method. Received group: `" . var_export($group, true) . "`" ); } + if (!empty($group)) { $this->status['group'] = $group; } + return $this; } - public function scheduleAt(Moment $at) + /** + * @return $this + */ + public function scheduleAt(Moment $at): static { $this->status['locked'] = false; $this->status['scheduled_at'] = T\MongoDate::from($at); + return $this; } - public function withUrn(string $urn) + /** + * @return $this + */ + public function withUrn(string $urn): static { $this->status['urn'] = $urn; + return $this; } - public function scheduledBy(string $namespace, string $id, int $executions) + /** + * @return $this + */ + public function scheduledBy(string $namespace, string $id, int $executions): static { $this->status['scheduled'] = [ 'by' => [ @@ -128,15 +133,15 @@ public function scheduledBy(string $namespace, string $id, int $executions) return $this; } - public function methodToCallOnWorkable($method) + public function methodToCallOnWorkable($method): void { if (!method_exists($this->workable, $method)) { - throw new Exception("Unknown method '$method' on workable instance"); + throw new \Exception("Unknown method '$method' on workable instance"); } $this->status['workable']['method'] = $method; } - public function execute(EventDispatcherInterface $eventDispatcher) + public function execute(EventDispatcherInterface $eventDispatcher): JobExecution { $methodToCall = $this->status['workable']['method']; try { @@ -145,14 +150,14 @@ public function execute(EventDispatcherInterface $eventDispatcher) $result = $this->workable->$methodToCall($this->retryStatistics()); $this->afterExecution($result, $eventDispatcher); } - } catch (Throwable $exception) { + } catch (\Throwable $exception) { $this->afterFailure($exception, $eventDispatcher); } return $this->lastJobExecution; } - public function retryStatistics() + public function retryStatistics(): array { return [ 'job_id' => (string) $this->id(), @@ -184,19 +189,20 @@ public function export() $this->lastJobExecution->export(), $this->tagsToUseFor($this->workable), WorkableInJob::export($this->workable, $this->status['workable']['method']), - RetryPolicyInJob::export($this->retryPolicy) + RetryPolicyInJob::export($this->retryPolicy), ); } public function beforeExecution(EventDispatcherInterface $eventDispatcher) { - $this->status['attempts'] += 1; + ++$this->status['attempts']; $this->lastJobExecution = new JobExecution(); $this->lastJobExecution->started($this->scheduledAt()); $this->emit('job.started', $eventDispatcher); if ($this->hasBeenScheduled()) { $this->save(); } + return $this; } @@ -209,6 +215,7 @@ public function afterExecution($result, EventDispatcherInterface $eventDispatche if ($this->hasBeenScheduled()) { $this->archive('done'); } + return $this; } @@ -222,6 +229,7 @@ private function recoverFromCrash(EventDispatcherInterface $eventDispatcher) if ($this->lastJobExecution->isCrashed()) { return !$archived = $this->afterFailure(new WorkerDiedInTheLineOfDutyException(), $eventDispatcher); } + return true; } @@ -238,19 +246,20 @@ private function afterFailure($exception, $eventDispatcher) $this->emit('job.failure.last', $eventDispatcher); $this->triggerOnWorkable('afterLastFailure', $exception); } + return $archived; } - private function emit($eventType, $eventDispatcher) + private function emit($eventType, EventDispatcherInterface $eventDispatcher): void { $event = new Event($this->export()); - $eventDispatcher->dispatch($eventType, $event); + $eventDispatcher->dispatch($event, $eventType); if ($this->workable instanceof EventListener) { $this->workable->onEvent($eventType, $event); } } - private function triggerOnWorkable($method, ?Throwable $e = null) + private function triggerOnWorkable($method, ?\Throwable $e = null) { if ($this->workable instanceof Finalizable) { $this->workable->$method($e); @@ -285,6 +294,7 @@ private function tagsToUseFor(Workable $workable) if (!empty($tagsToUse)) { return ['tags' => array_values(array_unique($tagsToUse))]; } + return []; } @@ -300,7 +310,7 @@ private static function initialize() 'group' => 'generic', ], WorkableInJob::initialize(), - RetryPolicyInJob::initialize() + RetryPolicyInJob::initialize(), ); } @@ -310,24 +320,22 @@ public static function pickReadyJobsForWorkers(MongoCollection $collection, $wor iterator_to_array( $collection ->find( - ( - Worker::canWorkOnAnyJobs($worksOn) ? - [ 'scheduled_at' => ['$lt' => T\MongoDate::now()], - 'locked' => false, - ] : - [ 'scheduled_at' => ['$lt' => T\MongoDate::now()], - 'locked' => false, - 'group' => $worksOn, - ] - ), + Worker::canWorkOnAnyJobs($worksOn) ? + ['scheduled_at' => ['$lt' => T\MongoDate::now()], + 'locked' => false, + ] : + ['scheduled_at' => ['$lt' => T\MongoDate::now()], + 'locked' => false, + 'group' => $worksOn, + ], [ 'projection' => ['_id' => 1], 'sort' => ['scheduled_at' => 1], 'limit' => count($workers), - ] - ) + ], + ), ), - '_id' + '_id', ); if (count($jobs) > 0) { @@ -347,13 +355,13 @@ public static function rollbackLockedNotIn(MongoCollection $collection, array $e '$set' => [ 'locked' => false, 'last_execution.crashed' => true, - ] - ] + ], + ], ); return $result->getModifiedCount(); } catch (BulkWriteException $e) { - throw new InvalidArgumentException("Not valid excluded jobs filter: " . var_export($excluded, true), -1, $e); + throw new \InvalidArgumentException('Not valid excluded jobs filter: ' . var_export($excluded, true), -1, $e); } } @@ -361,7 +369,7 @@ public static function lockAll(MongoCollection $collection, $jobs) { $collection->updateMany( ['_id' => ['$in' => array_values($jobs)]], - ['$set' => ['locked' => true]] + ['$set' => ['locked' => true]], ); } } diff --git a/src/Recruiter/Job/Event.php b/src/Recruiter/Job/Event.php index 71738ebe..946d9489 100644 --- a/src/Recruiter/Job/Event.php +++ b/src/Recruiter/Job/Event.php @@ -1,25 +1,24 @@ jobExport = $jobExport; } - public function export() + public function export(): array { return $this->jobExport; } - public function hasTag($wantedTag) + public function hasTag(string $wantedTag): bool { $tags = array_key_exists('tags', $this->jobExport) ? $this->jobExport['tags'] : []; + return in_array($wantedTag, $tags); } } diff --git a/src/Recruiter/Job/EventListener.php b/src/Recruiter/Job/EventListener.php index cc2dc6d4..f82db1d1 100644 --- a/src/Recruiter/Job/EventListener.php +++ b/src/Recruiter/Job/EventListener.php @@ -4,5 +4,5 @@ interface EventListener { - public function onEvent($channel, Event $ev); + public function onEvent($channel, Event $ev): void; } diff --git a/src/Recruiter/Job/Repository.php b/src/Recruiter/Job/Repository.php index 9ea4e305..d99fbe41 100644 --- a/src/Recruiter/Job/Repository.php +++ b/src/Recruiter/Job/Repository.php @@ -1,18 +1,18 @@ archived = $db->selectCollection('archived'); } - public function all() + public function all(): array { return $this->map( $this->scheduled->find([], [ 'sort' => ['scheduled_at' => -1], - ]) + ]), ); } - public function archiveAll() + public function archiveAll(): void { foreach ($this->all() as $job) { $this->archive($job); } } - public function scheduled($id) + public function scheduled(string|ObjectId $id): Job { if (is_string($id)) { $id = new ObjectId($id); @@ -44,14 +44,14 @@ public function scheduled($id) $found = $this->map($this->scheduled->find(['_id' => $id])); - if (count($found) === 0) { - throw new Exception("Unable to find scheduled job with ObjectId('{$id}')"); + if (0 === count($found)) { + throw new \Exception("Unable to find scheduled job with ObjectId('{$id}')"); } return $found[0]; } - public function archived($id) + public function archived(string|ObjectId $id): Job { if (is_string($id)) { $id = new ObjectId($id); @@ -59,35 +59,35 @@ public function archived($id) $found = $this->map($this->archived->find(['_id' => $id])); - if (count($found) === 0) { - throw new Exception("Unable to find archived job with ObjectId('{$id}')"); + if (0 === count($found)) { + throw new \Exception("Unable to find archived job with ObjectId('{$id}')"); } return $found[0]; } - public function save(Job $job) + public function save(Job $job): void { $document = $job->export(); $this->scheduled->replaceOne( ['_id' => $document['_id']], $document, - ['upsert' => true] + ['upsert' => true], ); } - public function archive(Job $job) + public function archive(Job $job): void { $document = $job->export(); $this->scheduled->deleteOne(['_id' => $document['_id']]); $this->archived->replaceOne(['_id' => $document['_id']], $document, ['upsert' => true]); } - public function releaseAll($jobIds) + public function releaseAll($jobIds): int { $result = $this->scheduled->updateMany( ['_id' => ['$in' => $jobIds]], - ['$set' => ['locked' => false, 'last_execution.crashed' => true]] + ['$set' => ['locked' => false, 'last_execution.crashed' => true]], ); return $result->getModifiedCount(); @@ -95,35 +95,35 @@ public function releaseAll($jobIds) public function countArchived(): int { - return $this->archived->count(); + return $this->archived->countDocuments(); } - public function cleanArchived(T\Moment $upperLimit) + public function cleanArchived(T\Moment $upperLimit): int { $documents = $this->archived->find( [ 'last_execution.ended_at' => [ '$lte' => T\MongoDate::from($upperLimit), - ] + ], ], - ['projection' => ['_id' => 1]] + ['projection' => ['_id' => 1]], ); $deleted = 0; foreach ($documents as $document) { $this->archived->deleteOne(['_id' => $document['_id']]); - $deleted++; + ++$deleted; } return $deleted; } - public function cleanScheduled(T\Moment $upperLimit) + public function cleanScheduled(T\Moment $upperLimit): int { $result = $this->scheduled->deleteMany([ 'created_at' => [ '$lte' => T\MongoDate::from($upperLimit), - ] + ], ]); return $result->getDeletedCount(); @@ -131,55 +131,55 @@ public function cleanScheduled(T\Moment $upperLimit) public function queued( $group = null, - T\Moment $at = null, - T\Moment $from = null, - array $query = [] - ) { - if ($at === null) { + ?T\Moment $at = null, + ?T\Moment $from = null, + array $query = [], + ): int { + if (null === $at) { $at = T\now(); } $query['scheduled_at']['$lte'] = T\MongoDate::from($at); - if ($from !== null) { + if (null !== $from) { $query['scheduled_at']['$gt'] = T\MongoDate::from($from); } - if ($group !== null) { + if (null !== $group) { $query['group'] = $group; } return $this->scheduled->count($query); } - public function postponed($group = null, T\Moment $at = null, array $query = []) + public function postponed($group = null, ?T\Moment $at = null, array $query = []): int { - if ($at === null) { + if (null === $at) { $at = T\now(); } $query['scheduled_at']['$gt'] = T\MongoDate::from($at); - if ($group !== null) { + if (null !== $group) { $query['group'] = $group; } - return $this->scheduled->count($query); + return $this->scheduled->countDocuments($query); } - public function scheduledCount($group = null, array $query = []) + public function scheduledCount($group = null, array $query = []): int { - if ($group !== null) { + if (null !== $group) { $query['group'] = $group; } - return $this->scheduled->count($query); + return $this->scheduled->countDocuments($query); } - public function queuedGroupedBy($field, array $query = [], $group = null) + public function queuedGroupedBy($field, array $query = [], $group = null): array { $query['scheduled_at']['$lte'] = T\MongoDate::from(T\now()); - if ($group !== null) { + if (null !== $group) { $query['group'] = $group; } @@ -199,9 +199,9 @@ public function queuedGroupedBy($field, array $query = [], $group = null) return $distinctAndCount; } - public function recentHistory($group = null, T\Moment $at = null, array $query = []) + public function recentHistory($group = null, ?T\Moment $at = null, array $query = []): array { - if ($at === null) { + if (null === $at) { $at = T\now(); } $lastMinute = array_merge( @@ -209,11 +209,11 @@ public function recentHistory($group = null, T\Moment $at = null, array $query = [ 'last_execution.ended_at' => [ '$gt' => T\MongoDate::from($at->before(T\minute(1))), - '$lte' => T\MongoDate::from($at) + '$lte' => T\MongoDate::from($at), ], - ] + ], ); - if ($group !== null) { + if (null !== $group) { $lastMinute['group'] = $group; } $cursor = $this->archived->aggregate($pipeline = [ @@ -237,22 +237,22 @@ public function recentHistory($group = null, T\Moment $at = null, array $query = ]); $documents = $cursor->toArray(); - if (count($documents) === 0) { + if (0 === count($documents)) { $throughputPerMinute = 0.0; $averageLatency = 0.0; $averageExecutionTime = 0; - } elseif (count($documents) === 1) { + } elseif (1 === count($documents)) { $throughputPerMinute = (float) $documents[0]['throughput']; $averageLatency = $documents[0]['latency'] / 1000; $averageExecutionTime = $documents[0]['execution_time'] / 1000; } else { - throw new RuntimeException("Result was not ok: " . var_export($documents, true)); + throw new \RuntimeException('Result was not ok: ' . var_export($documents, true)); } return [ 'throughput' => [ 'value' => $throughputPerMinute, - 'value_per_second' => $throughputPerMinute/60.0, + 'value_per_second' => $throughputPerMinute / 60.0, ], 'latency' => [ 'average' => $averageLatency, @@ -266,35 +266,35 @@ public function recentHistory($group = null, T\Moment $at = null, array $query = public function countSlowRecentJobs( T\Moment $lowerLimit, T\Moment $upperLimit, - $secondsToConsiderJobAsSlow = 5 + $secondsToConsiderJobAsSlow = 5, ): int { return count( $this->slowArchivedRecentJobs( $lowerLimit, $upperLimit, - $secondsToConsiderJobAsSlow - ) + $secondsToConsiderJobAsSlow, + ), ) + count( $this->slowScheduledRecentJobs( $lowerLimit, $upperLimit, - $secondsToConsiderJobAsSlow - ) + $secondsToConsiderJobAsSlow, + ), ); } public function countRecentJobsWithManyAttempts( T\Moment $lowerLimit, - T\Moment $upperLimit + T\Moment $upperLimit, ): int { return $this->countRecentArchivedOrScheduledJobsWithManyAttempts( $lowerLimit, $upperLimit, - 'archived' + 'archived', ) + $this->countRecentArchivedOrScheduledJobsWithManyAttempts( $lowerLimit, $upperLimit, - 'scheduled' + 'scheduled', ); } @@ -302,81 +302,83 @@ public function countDelayedScheduledJobs(T\Moment $lowerLimit): int { return $this->scheduled->count([ 'scheduled_at' => [ - '$lte' => T\MongoDate::from($lowerLimit) - ] + '$lte' => T\MongoDate::from($lowerLimit), + ], ]); } - public function delayedScheduledJobs(T\Moment $lowerLimit) + public function delayedScheduledJobs(T\Moment $lowerLimit): array { return $this->map( $this->scheduled->find([ 'scheduled_at' => [ - '$lte' => T\MongoDate::from($lowerLimit) - ] - ]) + '$lte' => T\MongoDate::from($lowerLimit), + ], + ]), ); } public function recentJobsWithManyAttempts( T\Moment $lowerLimit, - T\Moment $upperLimit - ) { + T\Moment $upperLimit, + ): array { $archived = $this->map( $this->recentArchivedOrScheduledJobsWithManyAttempts( $lowerLimit, $upperLimit, - 'archived' - ) + 'archived', + ), ); $scheduled = $this->map( $this->recentArchivedOrScheduledJobsWithManyAttempts( $lowerLimit, $upperLimit, - 'scheduled' - ) + 'scheduled', + ), ); + return array_merge($archived, $scheduled); } public function slowRecentJobs( T\Moment $lowerLimit, T\Moment $upperLimit, - $secondsToConsiderJobAsSlow = 5 - ) { - $archived= []; + $secondsToConsiderJobAsSlow = 5, + ): array { + $archived = []; $archivedArray = $this->slowArchivedRecentJobs( $lowerLimit, $upperLimit, - $secondsToConsiderJobAsSlow + $secondsToConsiderJobAsSlow, ); foreach ($archivedArray as $archivedJob) { $archived[] = Job::import($archivedJob, $this); } - $scheduled= []; + $scheduled = []; $scheduledArray = $this->slowScheduledRecentJobs( $lowerLimit, $upperLimit, - $secondsToConsiderJobAsSlow + $secondsToConsiderJobAsSlow, ); foreach ($scheduledArray as $scheduledJob) { $scheduled[] = Job::import($scheduledJob, $this); } + return array_merge($archived, $scheduled); } private function slowArchivedRecentJobs( T\Moment $lowerLimit, T\Moment $upperLimit, - $secondsToConsiderJobAsSlow - ) { + $secondsToConsiderJobAsSlow, + ): array { return $this->archived->aggregate([ [ '$match' => [ 'last_execution.ended_at' => [ '$gte' => T\MongoDate::from($lowerLimit), ], - ] + ], ], [ '$project' => [ @@ -384,8 +386,8 @@ private function slowArchivedRecentJobs( 'execution_time' => [ '$subtract' => [ '$last_execution.ended_at', - '$last_execution.started_at' - ] + '$last_execution.started_at', + ], ], 'done' => '$done', 'created_at' => '$created_at', @@ -397,13 +399,13 @@ private function slowArchivedRecentJobs( 'scheduled_at' => '$scheduled_at', 'last_execution' => '$last_execution', 'retry_policy' => '$retry_policy', - ] + ], ], [ '$match' => [ 'execution_time' => [ - '$gt' => $secondsToConsiderJobAsSlow*1000 - ] + '$gt' => $secondsToConsiderJobAsSlow * 1000, + ], ], ], ])->toArray(); @@ -412,14 +414,14 @@ private function slowArchivedRecentJobs( private function slowScheduledRecentJobs( T\Moment $lowerLimit, T\Moment $upperLimit, - $secondsToConsiderJobAsSlow - ) { + $secondsToConsiderJobAsSlow, + ): array { return $this->scheduled->aggregate([ [ '$match' => [ 'scheduled_at' => [ '$gte' => T\MongoDate::from($lowerLimit), - '$lte' => T\MongoDate::from($upperLimit) + '$lte' => T\MongoDate::from($upperLimit), ], 'last_execution.started_at' => [ '$exists' => true, @@ -427,7 +429,7 @@ private function slowScheduledRecentJobs( 'last_execution.ended_at' => [ '$exists' => true, ], - ] + ], ], [ '$project' => [ @@ -435,8 +437,8 @@ private function slowScheduledRecentJobs( 'execution_time' => [ '$subtract' => [ '$last_execution.ended_at', - '$last_execution.started_at' - ] + '$last_execution.started_at', + ], ], 'done' => '$done', 'created_at' => '$created_at', @@ -448,13 +450,13 @@ private function slowScheduledRecentJobs( 'scheduled_at' => '$scheduled_at', 'last_execution' => '$last_execution', 'retry_policy' => '$retry_policy', - ] + ], ], [ '$match' => [ 'execution_time' => [ - '$gt' => $secondsToConsiderJobAsSlow*1000 - ] + '$gt' => $secondsToConsiderJobAsSlow * 1000, + ], ], ], ])->toArray(); @@ -463,32 +465,35 @@ private function slowScheduledRecentJobs( private function countRecentArchivedOrScheduledJobsWithManyAttempts( T\Moment $lowerLimit, T\Moment $upperLimit, - $collectionName - ) { + $collectionName, + ): int { return count($this->recentArchivedOrScheduledJobsWithManyAttempts( $lowerLimit, $upperLimit, - $collectionName + $collectionName, )->toArray()); } private function recentArchivedOrScheduledJobsWithManyAttempts( T\Moment $lowerLimit, T\Moment $upperLimit, - $collectionName + $collectionName, ) { return $this->{$collectionName}->find([ 'last_execution.ended_at' => [ '$gte' => T\MongoDate::from($lowerLimit), - '$lte' => T\MongoDate::from($upperLimit) - ], - 'attempts' => [ - '$gt' => 1 - ] + '$lte' => T\MongoDate::from($upperLimit), + ], + 'attempts' => [ + '$gt' => 1, + ], ]); } - private function map($cursor) + /** + * @return array + */ + private function map(CursorInterface $cursor): array { $jobs = []; foreach ($cursor as $document) { diff --git a/src/Recruiter/JobAfterFailure.php b/src/Recruiter/JobAfterFailure.php index 9f1d60e7..4d552651 100644 --- a/src/Recruiter/JobAfterFailure.php +++ b/src/Recruiter/JobAfterFailure.php @@ -2,56 +2,45 @@ namespace Recruiter; -use Timeless\Moment; use Timeless\Interval; -use Timeless\MongoDate; +use Timeless\Moment; class JobAfterFailure { - /** @var Job */ - private $job; - - /** @var JobExecution */ - private $lastJobExecution; + private bool $hasBeenScheduled; - /** @var bool */ - private $hasBeenScheduled; + private bool $hasBeenArchived; - /** @var bool */ - private $hasBeenArchived; - - public function __construct(Job $job, JobExecution $lastJobExecution) + public function __construct(private readonly Job $job, private readonly JobExecution $lastJobExecution) { - $this->job = $job; - $this->lastJobExecution = $lastJobExecution; $this->hasBeenScheduled = false; $this->hasBeenArchived = false; } - public function createdAt() + public function createdAt(): Moment { return $this->job->createdAt(); } - public function inGroup($group) + public function inGroup($group): void { $this->job->inGroup($group); $this->job->save(); } - public function scheduleIn(Interval $in) + public function scheduleIn(Interval $in): void { $this->scheduleAt($in->fromNow()); } - public function scheduleAt(Moment $at) + public function scheduleAt(Moment $at): void { $this->hasBeenScheduled = true; $this->job->scheduleAt($at); $this->job->save(); } - public function archive($why) + public function archive($why): void { $this->hasBeenArchived = true; $this->job->archive($why); @@ -62,7 +51,7 @@ public function causeOfFailure() return $this->lastJobExecution->causeOfFailure(); } - public function lastExecutionDuration() + public function lastExecutionDuration(): Interval { return $this->lastJobExecution->duration(); } @@ -72,16 +61,18 @@ public function numberOfAttempts() return $this->job->numberOfAttempts(); } - public function archiveIfNotScheduled() + public function archiveIfNotScheduled(): bool { if (!$this->hasBeenScheduled && !$this->hasBeenArchived) { $this->archive('not-scheduled-by-retry-policy'); + return true; } + return false; } - public function hasBeenArchived() + public function hasBeenArchived(): bool { return $this->hasBeenArchived; } diff --git a/src/Recruiter/JobExecution.php b/src/Recruiter/JobExecution.php index 3e111d62..b84373c6 100644 --- a/src/Recruiter/JobExecution.php +++ b/src/Recruiter/JobExecution.php @@ -3,35 +3,34 @@ namespace Recruiter; use Timeless as T; -use Throwable; class JobExecution { - private $isCrashed; - private $scheduledAt; - private $startedAt; - private $endedAt; + private bool $isCrashed = false; + private ?T\Moment $scheduledAt = null; + private ?T\Moment $startedAt = null; + private ?T\Moment $endedAt = null; private $completedWith; - private $failedWith; + private ?\Throwable $failedWith = null; - public function isCrashed() + public function isCrashed(): bool { return $this->isCrashed; } - public function started($scheduledAt = null) + public function started(?T\Moment $scheduledAt = null): void { $this->scheduledAt = $scheduledAt; $this->startedAt = T\now(); } - public function failedWith(Throwable $exception) + public function failedWith(\Throwable $exception): void { $this->endedAt = T\now(); $this->failedWith = $exception; } - public function completedWith($result) + public function completedWith($result): void { $this->endedAt = T\now(); $this->completedWith = $result; @@ -42,28 +41,29 @@ public function result() return $this->completedWith; } - public function causeOfFailure() + public function causeOfFailure(): ?\Throwable { return $this->failedWith; } - public function isFailed() + public function isFailed(): bool { return !is_null($this->failedWith) || $this->isCrashed(); } - public function duration() + public function duration(): T\Interval { if ($this->startedAt && $this->endedAt && ($this->startedAt <= $this->endedAt)) { return T\seconds( $this->endedAt->seconds() - - $this->startedAt-> seconds() + $this->startedAt->seconds(), ); } + return T\seconds(0); } - public static function import($document) + public static function import(array $document): self { $lastExecution = new self(); if (array_key_exists('last_execution', $document)) { @@ -78,10 +78,11 @@ public static function import($document) $lastExecution->startedAt = T\MongoDate::toMoment($lastExecutionDocument['started_at']); } } + return $lastExecution; } - public function export() + public function export(): array { $exported = []; if ($this->scheduledAt) { @@ -94,7 +95,7 @@ public function export() $exported['ended_at'] = T\MongoDate::from($this->endedAt); } if ($this->failedWith) { - $exported['class'] = get_class($this->failedWith); + $exported['class'] = $this->failedWith::class; $exported['message'] = $this->failedWith->getMessage(); $exported['trace'] = $this->traceOf($this->failedWith); } @@ -108,18 +109,19 @@ public function export() } } - private function traceOf($result) + private function traceOf(mixed $result): string { $trace = 'ok'; - if ($result instanceof Throwable) { + if ($result instanceof \Throwable) { $trace = $result->getTraceAsString(); } elseif (is_object($result) && method_exists($result, 'trace')) { $trace = $result->trace(); } elseif (is_object($result)) { - $trace = get_class($result); + $trace = $result::class; } elseif (is_string($result) || is_numeric($result)) { $trace = $result; } - return substr($trace, 0, 4096); + + return substr((string) $trace, 0, 4096); } } diff --git a/src/Recruiter/JobToSchedule.php b/src/Recruiter/JobToSchedule.php index 880b209a..cbc48d98 100644 --- a/src/Recruiter/JobToSchedule.php +++ b/src/Recruiter/JobToSchedule.php @@ -2,71 +2,89 @@ namespace Recruiter; -use Recruiter\Job; -use Recruiter\RetryPolicy; use Symfony\Component\EventDispatcher\EventDispatcher; use Timeless as T; use Timeless\Interval; use Timeless\Moment; +/** + * @method send() to make PHPStan happy in tests + */ class JobToSchedule { - private $job; + private bool $mustBeScheduled; - /** @var bool */ - private $mustBeScheduled; - - public function __construct(Job $job) + public function __construct(private readonly Job $job) { - $this->job = $job; $this->mustBeScheduled = false; } - public function doNotRetry() + public function doNotRetry(): static { return $this->retryWithPolicy(new RetryPolicy\DoNotDoItAgain()); } - public function retryManyTimes($howManyTimes, Interval $timeToWaitBeforeRetry, $retriableExceptionTypes = []) + /** + * @return $this + */ + public function retryManyTimes($howManyTimes, Interval $timeToWaitBeforeRetry, $retriableExceptionTypes = []): static { $this->job->retryWithPolicy( $this->filterForRetriableExceptions( new RetryPolicy\RetryManyTimes($howManyTimes, $timeToWaitBeforeRetry), - $retriableExceptionTypes - ) + $retriableExceptionTypes, + ), ); + return $this; } - public function retryWithPolicy(RetryPolicy $retryPolicy, $retriableExceptionTypes = []) + /** + * @return $this + */ + public function retryWithPolicy(RetryPolicy $retryPolicy, $retriableExceptionTypes = []): static { $this->job->retryWithPolicy( $this->filterForRetriableExceptions( $retryPolicy, - $retriableExceptionTypes - ) + $retriableExceptionTypes, + ), ); + return $this; } - public function inBackground() + /** + * @return $this + */ + public function inBackground(): static { return $this->scheduleAt(T\now()); } - public function scheduleIn(Interval $duration) + /** + * @return $this + */ + public function scheduleIn(Interval $duration): static { return $this->scheduleAt($duration->fromNow()); } - public function scheduleAt(Moment $momentInTime) + /** + * @return $this + */ + public function scheduleAt(Moment $momentInTime): static { $this->mustBeScheduled = true; $this->job->scheduleAt($momentInTime); + return $this; } - public function inGroup($group) + /** + * @return $this + */ + public function inGroup(array|string|null $group): static { if (!empty($group)) { $this->job->inGroup($group); @@ -75,7 +93,7 @@ public function inGroup($group) return $this; } - public function taggedAs($tags) + public function taggedAs(array|string $tags): static { if (!empty($tags)) { $this->job->taggedAs(is_array($tags) ? $tags : [$tags]); @@ -84,47 +102,52 @@ public function taggedAs($tags) return $this; } - public function withUrn(string $urn) + public function withUrn(string $urn): static { $this->job->withUrn($urn); return $this; } - public function scheduledBy(string $namespace, string $id, int $nth) + public function scheduledBy(string $namespace, string $id, int $nth): static { $this->job->scheduledBy($namespace, $id, $nth); return $this; } - public function execute() + public function execute(): string { if ($this->mustBeScheduled) { $this->job->save(); } else { $this->job->execute($this->emptyEventDispatcher()); } + return (string) $this->job->id(); } - private function emptyEventDispatcher() + private function emptyEventDispatcher(): EventDispatcher { return new EventDispatcher(); } - public function __call($name, $arguments) + /** + * @throws \Exception + */ + public function __call(string $name, array $arguments) { $this->job->methodToCallOnWorkable($name); + return $this->execute(); } - public function export() + public function export(): array { return $this->job->export(); } - public static function import($document, $repository) + public static function import($document, $repository): self { return new self(Job::import($document, $repository)); } @@ -137,6 +160,7 @@ private function filterForRetriableExceptions($retryPolicy, $retriableExceptionT if (!empty($retriableExceptionTypes)) { $retryPolicy = new RetryPolicy\RetriableExceptionFilter($retryPolicy, $retriableExceptionTypes); } + return $retryPolicy; } } diff --git a/src/Recruiter/Recruiter.php b/src/Recruiter/Recruiter.php index 12adc429..a0138695 100644 --- a/src/Recruiter/Recruiter.php +++ b/src/Recruiter/Recruiter.php @@ -1,13 +1,9 @@ db = $db; - $this->jobs = new Job\Repository($db); - $this->workers = new Worker\Repository($db, $this); - $this->scheduler = new Scheduler\Repository($db); + $this->jobs = new Job\Repository($this->db); + $this->workers = new Worker\Repository($this->db, $this); + $this->scheduler = new Scheduler\Repository($this->db); $this->eventDispatcher = new EventDispatcher(); } - public function hire(MemoryLimit $memoryLimit) + public function hire(MemoryLimit $memoryLimit): Worker { return Worker::workFor($this, $this->workers, $memoryLimit); } - public function jobOf(Workable $workable) + public function jobOf(Workable $workable): JobToSchedule { return new JobToSchedule( - Job::around($workable, $this->jobs) + Job::around($workable, $this->jobs), ); } - public function repeatableJobOf(Repeatable $repeatable) + public function repeatableJobOf(Repeatable $repeatable): Scheduler { return Scheduler::around($repeatable, $this->scheduler, $this); } - public function queued() + public function queued(): int { return $this->jobs->queued(); } - public function scheduled() + public function scheduled(): int { return $this->jobs->scheduledCount(); } - public function queuedGroupedBy($field, array $query = [], $group = null) + public function queuedGroupedBy($field, array $query = [], $group = null): array { return $this->jobs->queuedGroupedBy($field, $query, $group); } - /** - * @deprecated use the method `analytics` instead. - */ - public function statistics($group = null, Moment $at = null, array $query = []) + #[\Deprecated(message: 'use the method `analytics` instead')] + public function statistics($group = null, ?Moment $at = null, array $query = []): array { return $this->analytics($group, $at, $query); } - public function analytics($group = null, Moment $at = null, array $query = []) + /** + * @return array + */ + public function analytics($group = null, ?Moment $at = null, array $query = []): array { $totalsScheduledJobs = $this->jobs->scheduledCount($group, $query); - $queued = $this->jobs->queued($group, $at, $at ? $at->before(T\hour(24)) : null, $query); + $queued = $this->jobs->queued($group, $at, $at?->before(T\hour(24)), $query); $postponed = $this->jobs->postponed($group, $at, $query); return array_merge( @@ -84,38 +79,40 @@ public function analytics($group = null, Moment $at = null, array $query = []) 'zombies' => $totalsScheduledJobs - ($queued + $postponed), ], ], - $this->jobs->recentHistory($group, $at, $query) + $this->jobs->recentHistory($group, $at, $query), ); } - public function getEventDispatcher() + public function getEventDispatcher(): EventDispatcher { return $this->eventDispatcher; } /** * @step - * @return integer how many + * + * @return int how many */ - public function rollbackLockedJobs() + public function rollbackLockedJobs(): int { $assignedJobs = Worker::assignedJobs($this->db->selectCollection('roster')); + return Job::rollbackLockedNotIn($this->db->selectCollection('scheduled'), $assignedJobs); } /** * @step */ - public function bye() + public function bye(): void { } - public function assignJobsToWorkers() + public function assignJobsToWorkers(): array { return $this->assignLockedJobsToWorkers($this->bookJobsForWorkers()); } - public function scheduleRepeatableJobs() + public function scheduleRepeatableJobs(): void { $schedulers = $this->scheduler->all(); foreach ($schedulers as $scheduler) { @@ -126,7 +123,7 @@ public function scheduleRepeatableJobs() /** * @step */ - public function bookJobsForWorkers() + public function bookJobsForWorkers(): array { $roster = $this->db->selectCollection('roster'); $scheduled = $this->db->selectCollection('scheduled'); @@ -134,46 +131,45 @@ public function bookJobsForWorkers() $bookedJobs = []; foreach (Worker::pickAvailableWorkers($roster, $workersPerUnit) as $resultRow) { - list ($worksOn, $workers) = $resultRow; + [$worksOn, $workers] = $resultRow; $result = Job::pickReadyJobsForWorkers($scheduled, $worksOn, $workers); if ($result) { - list($worksOn, $workers, $jobs) = $result; - list($assignments, $jobs, $workers) = $this->combineJobsWithWorkers($jobs, $workers); + [$worksOn, $workers, $jobs] = $result; + [$assignments, $jobs, $workers] = $this->combineJobsWithWorkers($jobs, $workers); Job::lockAll($scheduled, $jobs); $bookedJobs[] = [$jobs, $workers]; } } + return $bookedJobs; } /** * @step */ - public function assignLockedJobsToWorkers($bookedJobs) + public function assignLockedJobsToWorkers(array $bookedJobs): array { $assignments = []; $totalActualAssignments = 0; $roster = $this->db->selectCollection('roster'); foreach ($bookedJobs as $row) { - list ($jobs, $workers, ) = $row; - list ($newAssignments, $actualAssignmentsNumber) = Worker::tryToAssignJobsToWorkers($roster, $jobs, $workers); + [$jobs, $workers] = $row; + [$newAssignments, $actualAssignmentsNumber] = Worker::tryToAssignJobsToWorkers($roster, $jobs, $workers); if (array_intersect_key($assignments, $newAssignments)) { - throw new RuntimeException("Conflicting assignments: current were " . var_export($assignments, true) . " and we want to also assign " . var_export($newAssignments, true)); + throw new \RuntimeException('Conflicting assignments: current were ' . var_export($assignments, true) . ' and we want to also assign ' . var_export($newAssignments, true)); } $assignments = array_merge( $assignments, - $newAssignments + $newAssignments, ); $totalActualAssignments += $actualAssignmentsNumber; } return [ - array_map(function ($value) { - return (string) $value; - }, $assignments), - $totalActualAssignments + array_map(fn ($value) => (string) $value, $assignments), + $totalActualAssignments, ]; } @@ -184,12 +180,13 @@ public function scheduledJob($id) /** * @step - * @return integer how many jobs were unlocked as a result + * + * @return int how many jobs were unlocked as a result */ - public function retireDeadWorkers(DateTimeImmutable $now, Interval $consideredDeadAfter) + public function retireDeadWorkers(\DateTimeImmutable $now, Interval $consideredDeadAfter): int { return $this->jobs->releaseAll( - $jobsAssignedToDeadWorkers = Worker::retireDeadWorkers($this->workers, $now, $consideredDeadAfter) + $jobsAssignedToDeadWorkers = Worker::retireDeadWorkers($this->workers, $now, $consideredDeadAfter), ); } @@ -204,7 +201,7 @@ public function flushJobsSynchronously(): SynchronousExecutionReport return SynchronousExecutionReport::fromArray($report); } - public function createCollectionsAndIndexes() + public function createCollectionsAndIndexes(): void { $this->db->selectCollection('scheduled')->createIndex( [ @@ -212,67 +209,68 @@ public function createCollectionsAndIndexes() 'locked' => 1, 'scheduled_at' => 1, ], - ['background' => true] + ['background' => true], ); $this->db->selectCollection('scheduled')->createIndex( [ 'locked' => 1, 'scheduled_at' => 1, ], - ['background' => true] + ['background' => true], ); $this->db->selectCollection('scheduled')->createIndex( [ 'locked' => 1, ], - ['background' => true] + ['background' => true], ); $this->db->selectCollection('scheduled')->createIndex( [ 'tags' => 1, ], - ['background' => true, 'sparse' => true] + ['background' => true, 'sparse' => true], ); $this->db->selectCollection('archived')->createIndex( [ 'created_at' => 1, ], - ['background' => true] + ['background' => true], ); $this->db->selectCollection('archived')->createIndex( [ 'created_at' => 1, 'group' => 1, ], - ['background' => true] + ['background' => true], ); $this->db->selectCollection('archived')->createIndex( [ 'last_execution.ended_at' => 1, ], - ['background' => true] + ['background' => true], ); $this->db->selectCollection('roster')->createIndex( [ 'available' => 1, ], - ['background' => true] + ['background' => true], ); $this->db->selectCollection('roster')->createIndex( [ 'last_seen_at' => 1, ], - ['background' => true] + ['background' => true], ); } - private function combineJobsWithWorkers($jobs, $workers) + private function combineJobsWithWorkers($jobs, $workers): array { $assignments = min(count($workers), count($jobs)); $workers = array_slice($workers, 0, $assignments); $jobs = array_slice($jobs, 0, $assignments); + return [$assignments, $jobs, $workers]; } } diff --git a/src/Recruiter/Repeatable.php b/src/Recruiter/Repeatable.php index 54a066d9..80cc6c0b 100644 --- a/src/Recruiter/Repeatable.php +++ b/src/Recruiter/Repeatable.php @@ -6,9 +6,7 @@ interface Repeatable extends Workable { /** * Assign an unique name to the scheduler in order to handle idempotency, - * only one scheduler with the same urn can exists - * - * @return string + * only one scheduler with the same urn can exists. */ public function urn(): string; @@ -19,8 +17,6 @@ public function urn(): string; * * true: only one job at a time can be queued * false: there may be more concurrent jobs at a time - * - * @return boolean */ public function unique(): bool; } diff --git a/src/Recruiter/RepeatableInJob.php b/src/Recruiter/RepeatableInJob.php index 40f7af62..e6a23110 100644 --- a/src/Recruiter/RepeatableInJob.php +++ b/src/Recruiter/RepeatableInJob.php @@ -1,8 +1,7 @@ self::classNameOf($workable), 'parameters' => $workable->export(), 'method' => $methodToCall, - ] + ], ]; } @@ -57,10 +56,11 @@ public static function initialize(): array private static function classNameOf($repeatable): string { - $repeatableClassName = get_class($repeatable); + $repeatableClassName = $repeatable::class; if (method_exists($repeatable, 'getClass')) { $repeatableClassName = $repeatable->getClass(); } + return $repeatableClassName; } } diff --git a/src/Recruiter/Retriable.php b/src/Recruiter/Retriable.php index 89e3d369..67080a88 100644 --- a/src/Recruiter/Retriable.php +++ b/src/Recruiter/Retriable.php @@ -5,9 +5,7 @@ interface Retriable { /** - * Declare what instance of `Recruiter\RetryPolicy` should be used for a `Recruiter\Workable` - * - * @return RetryPolicy + * Declare what instance of `Recruiter\RetryPolicy` should be used for a `Recruiter\Workable`. */ public function retryWithPolicy(): RetryPolicy; } diff --git a/src/Recruiter/RetryPolicy.php b/src/Recruiter/RetryPolicy.php index 0c462c65..33f29590 100644 --- a/src/Recruiter/RetryPolicy.php +++ b/src/Recruiter/RetryPolicy.php @@ -7,36 +7,27 @@ interface RetryPolicy /** * Decide whether or not to reschedule a job. If you want to reschedule the * job use the appropriate methods on job or do nothing to if you don't - * want to execute the job again + * want to execute the job again. * * This method can * - schedule the job * - archive the job * - do nothing (and the job will be archived anyway) - * - * @param JobAfterFailure $job - * - * @return void */ - public function schedule(JobAfterFailure $job); + public function schedule(JobAfterFailure $job): void; /** - * Export retry policy parameters - * - * @return array + * Export retry policy parameters. */ public function export(): array; /** - * Import retry policy parameters + * Import retry policy parameters. * * @param array $parameters Previously exported parameters - * - * @return RetryPolicy */ public static function import(array $parameters): RetryPolicy; - /** * @return bool true if is the last retry */ diff --git a/src/Recruiter/RetryPolicy/DoNotDoItAgain.php b/src/Recruiter/RetryPolicy/DoNotDoItAgain.php index b7b28791..fafe975b 100644 --- a/src/Recruiter/RetryPolicy/DoNotDoItAgain.php +++ b/src/Recruiter/RetryPolicy/DoNotDoItAgain.php @@ -3,15 +3,15 @@ namespace Recruiter\RetryPolicy; use Recruiter\Job; +use Recruiter\JobAfterFailure; use Recruiter\RetryPolicy; use Recruiter\RetryPolicyBehaviour; -use Recruiter\JobAfterFailure; class DoNotDoItAgain implements RetryPolicy { use RetryPolicyBehaviour; - public function schedule(JobAfterFailure $job) + public function schedule(JobAfterFailure $job): void { // doing nothing means to avoid to reschedule the job } diff --git a/src/Recruiter/RetryPolicy/ExponentialBackoff.php b/src/Recruiter/RetryPolicy/ExponentialBackoff.php index 4264dd68..b1bcedb7 100644 --- a/src/Recruiter/RetryPolicy/ExponentialBackoff.php +++ b/src/Recruiter/RetryPolicy/ExponentialBackoff.php @@ -3,26 +3,24 @@ namespace Recruiter\RetryPolicy; use Recruiter\Job; +use Recruiter\JobAfterFailure; use Recruiter\RetryPolicy; use Recruiter\RetryPolicyBehaviour; -use Recruiter\JobAfterFailure; - use Timeless as T; use Timeless\Interval; class ExponentialBackoff implements RetryPolicy { - private $retryHowManyTimes; - private $timeToInitiallyWaitBeforeRetry; - use RetryPolicyBehaviour; - public static function forTimes($retryHowManyTimes, $timeToInitiallyWaitBeforeRetry = 60) + private Interval $timeToInitiallyWaitBeforeRetry; + + public static function forTimes($retryHowManyTimes, $timeToInitiallyWaitBeforeRetry = 60): static { return new static($retryHowManyTimes, $timeToInitiallyWaitBeforeRetry); } - public function atFirstWaiting($timeToInitiallyWaitBeforeRetry) + public function atFirstWaiting($timeToInitiallyWaitBeforeRetry): static { return new static($this->retryHowManyTimes, $timeToInitiallyWaitBeforeRetry); } @@ -31,31 +29,31 @@ public function atFirstWaiting($timeToInitiallyWaitBeforeRetry) * @params integer $interval in seconds * @params integer $timeToWaitBeforeRetry in seconds */ - public static function forAnInterval($interval, $timeToInitiallyWaitBeforeRetry) + public static function forAnInterval($interval, $timeToInitiallyWaitBeforeRetry): static { if (!($timeToInitiallyWaitBeforeRetry instanceof Interval)) { $timeToInitiallyWaitBeforeRetry = T\seconds($timeToInitiallyWaitBeforeRetry); } $numberOfRetries = round( log($interval / $timeToInitiallyWaitBeforeRetry->seconds()) - / log(2) + / log(2), ); + return new static($numberOfRetries, $timeToInitiallyWaitBeforeRetry); } - public function __construct($retryHowManyTimes, $timeToInitiallyWaitBeforeRetry) + public function __construct(private $retryHowManyTimes, int|Interval $timeToInitiallyWaitBeforeRetry) { if (!($timeToInitiallyWaitBeforeRetry instanceof Interval)) { $timeToInitiallyWaitBeforeRetry = T\seconds($timeToInitiallyWaitBeforeRetry); } - $this->retryHowManyTimes = $retryHowManyTimes; $this->timeToInitiallyWaitBeforeRetry = $timeToInitiallyWaitBeforeRetry; } - public function schedule(JobAfterFailure $job) + public function schedule(JobAfterFailure $job): void { if ($job->numberOfAttempts() <= $this->retryHowManyTimes) { - $retryInterval = T\seconds(pow(2, $job->numberOfAttempts() - 1) * $this->timeToInitiallyWaitBeforeRetry->seconds()); + $retryInterval = T\seconds(2 ** ($job->numberOfAttempts() - 1) * $this->timeToInitiallyWaitBeforeRetry->seconds()); $job->scheduleIn($retryInterval); } else { $job->archive('tried-too-many-times'); @@ -66,7 +64,7 @@ public function export(): array { return [ 'retry_how_many_times' => $this->retryHowManyTimes, - 'seconds_to_initially_wait_before_retry' => $this->timeToInitiallyWaitBeforeRetry->seconds() + 'seconds_to_initially_wait_before_retry' => $this->timeToInitiallyWaitBeforeRetry->seconds(), ]; } @@ -74,7 +72,7 @@ public static function import(array $parameters): RetryPolicy { return new self( $parameters['retry_how_many_times'], - T\seconds($parameters['seconds_to_initially_wait_before_retry']) + T\seconds($parameters['seconds_to_initially_wait_before_retry']), ); } diff --git a/src/Recruiter/RetryPolicy/RetriableException.php b/src/Recruiter/RetryPolicy/RetriableException.php index 59087f74..896f32cc 100644 --- a/src/Recruiter/RetryPolicy/RetriableException.php +++ b/src/Recruiter/RetryPolicy/RetriableException.php @@ -1,28 +1,23 @@ exceptionClass = $exceptionClass; - $this->retryPolicy = $retryPolicy; } public function exceptionClass(): string diff --git a/src/Recruiter/RetryPolicy/RetriableExceptionFilter.php b/src/Recruiter/RetryPolicy/RetriableExceptionFilter.php index b0225307..acfbfa81 100644 --- a/src/Recruiter/RetryPolicy/RetriableExceptionFilter.php +++ b/src/Recruiter/RetryPolicy/RetriableExceptionFilter.php @@ -2,19 +2,17 @@ namespace Recruiter\RetryPolicy; -use InvalidArgumentException; - use Recruiter\Job; -use Recruiter\RetryPolicy; use Recruiter\JobAfterFailure; +use Recruiter\RetryPolicy; class RetriableExceptionFilter implements RetryPolicy { - private $filteredRetryPolicy; private $retriableExceptions; /** - * @param string $exceptionClass fully qualified class or interface name + * @param string $exceptionClass fully qualified class or interface name + * * @return self */ public static function onlyFor($exceptionClass, RetryPolicy $retryPolicy) @@ -22,13 +20,12 @@ public static function onlyFor($exceptionClass, RetryPolicy $retryPolicy) return new self($retryPolicy, [$exceptionClass]); } - public function __construct(RetryPolicy $filteredRetryPolicy, array $retriableExceptions = ['Exception']) + public function __construct(private readonly RetryPolicy $filteredRetryPolicy, array $retriableExceptions = ['Exception']) { - $this->filteredRetryPolicy = $filteredRetryPolicy; $this->retriableExceptions = $this->ensureAreAllExceptions($retriableExceptions); } - public function schedule(JobAfterFailure $job) + public function schedule(JobAfterFailure $job): void { if ($this->isExceptionRetriable($job->causeOfFailure())) { $this->filteredRetryPolicy->schedule($job); @@ -42,9 +39,9 @@ public function export(): array return [ 'retriable_exceptions' => $this->retriableExceptions, 'filtered_retry_policy' => [ - 'class' => get_class($this->filteredRetryPolicy), - 'parameters' => $this->filteredRetryPolicy->export() - ] + 'class' => $this->filteredRetryPolicy::class, + 'parameters' => $this->filteredRetryPolicy->export(), + ], ]; } @@ -52,9 +49,10 @@ public static function import(array $parameters): RetryPolicy { $filteredRetryPolicy = $parameters['filtered_retry_policy']; $retriableExceptions = $parameters['retriable_exceptions']; + return new self( $filteredRetryPolicy['class']::import($filteredRetryPolicy['parameters']), - $retriableExceptions + $retriableExceptions, ); } @@ -67,24 +65,22 @@ private function ensureAreAllExceptions($exceptions) { foreach ($exceptions as $exception) { if (!is_a($exception, 'Throwable', true)) { - throw new InvalidArgumentException( - "Only subclasses of Exception can be retriable exceptions, '{$exception}' is not" - ); + throw new \InvalidArgumentException("Only subclasses of Exception can be retriable exceptions, '{$exception}' is not"); } } + return $exceptions; } private function isExceptionRetriable($exception) { - if (!is_null($exception) && is_object($exception)) { - return \Recruiter\array_some( + if (is_object($exception)) { + return array_any( $this->retriableExceptions, - function ($retriableExceptionType) use ($exception) { - return ($exception instanceof $retriableExceptionType); - } + fn ($retriableExceptionType) => $exception instanceof $retriableExceptionType, ); } + return false; } } diff --git a/src/Recruiter/RetryPolicy/RetryForever.php b/src/Recruiter/RetryPolicy/RetryForever.php index 0eeb491e..4ec6c57d 100644 --- a/src/Recruiter/RetryPolicy/RetryForever.php +++ b/src/Recruiter/RetryPolicy/RetryForever.php @@ -1,21 +1,20 @@ timeToWaitBeforeRetry = $timeToWaitBeforeRetry; } - public static function afterSeconds($timeToWaitBeforeRetry = 60) + public static function afterSeconds(int|Interval $timeToWaitBeforeRetry = 60): self { - return new static($timeToWaitBeforeRetry); + return new self($timeToWaitBeforeRetry); } - public function schedule(JobAfterFailure $job) + public function schedule(JobAfterFailure $job): void { $job->scheduleIn($this->timeToWaitBeforeRetry); } @@ -36,14 +35,14 @@ public function schedule(JobAfterFailure $job) public function export(): array { return [ - 'seconds_to_wait_before_retry' => $this->timeToWaitBeforeRetry->seconds() + 'seconds_to_wait_before_retry' => $this->timeToWaitBeforeRetry->seconds(), ]; } public static function import(array $parameters): RetryPolicy { return new self( - T\seconds($parameters['seconds_to_wait_before_retry']) + T\seconds($parameters['seconds_to_wait_before_retry']), ); } diff --git a/src/Recruiter/RetryPolicy/RetryManyTimes.php b/src/Recruiter/RetryPolicy/RetryManyTimes.php index 31e99ba2..d5fa7d02 100644 --- a/src/Recruiter/RetryPolicy/RetryManyTimes.php +++ b/src/Recruiter/RetryPolicy/RetryManyTimes.php @@ -1,36 +1,34 @@ retryHowManyTimes = $retryHowManyTimes; $this->timeToWaitBeforeRetry = $timeToWaitBeforeRetry; } - public static function forTimes($retryHowManyTimes, $timeToWaitBeforeRetry = 60) + public static function forTimes($retryHowManyTimes, int|Interval $timeToWaitBeforeRetry = 60): static { return new static($retryHowManyTimes, $timeToWaitBeforeRetry); } - public function schedule(JobAfterFailure $job) + public function schedule(JobAfterFailure $job): void { if ($job->numberOfAttempts() <= $this->retryHowManyTimes) { $job->scheduleIn($this->timeToWaitBeforeRetry); @@ -43,7 +41,7 @@ public function export(): array { return [ 'retry_how_many_times' => $this->retryHowManyTimes, - 'seconds_to_wait_before_retry' => $this->timeToWaitBeforeRetry->seconds() + 'seconds_to_wait_before_retry' => $this->timeToWaitBeforeRetry->seconds(), ]; } @@ -51,7 +49,7 @@ public static function import(array $parameters): RetryPolicy { return new self( $parameters['retry_how_many_times'], - T\seconds($parameters['seconds_to_wait_before_retry']) + T\seconds($parameters['seconds_to_wait_before_retry']), ); } diff --git a/src/Recruiter/RetryPolicy/SelectByException.php b/src/Recruiter/RetryPolicy/SelectByException.php index c38835cb..e00bafe6 100644 --- a/src/Recruiter/RetryPolicy/SelectByException.php +++ b/src/Recruiter/RetryPolicy/SelectByException.php @@ -3,15 +3,12 @@ namespace Recruiter\RetryPolicy; use Exception; -use InvalidArgumentException; use Recruiter\Job; use Recruiter\JobAfterFailure; use Recruiter\RetryPolicy; -use Throwable; -use function Recruiter\array_all; /** - * Select retry policies based on the raised exception + * Select retry policies based on the raised exception. * * If a job fails with an exception it's possible to select a retry * policy instance based on the class of the exception. The exception @@ -27,25 +24,23 @@ */ class SelectByException implements RetryPolicy { - /** - * @var array - */ - private $exceptions; - public static function create(): SelectByExceptionBuilder { return new SelectByExceptionBuilder(); } - public function __construct(array $exceptions) - { - $this->exceptions = $exceptions; + public function __construct( + /** + * @var array + */ + private readonly array $exceptions, + ) { } /** - * {@inheritDoc} + * @throws \Exception */ - public function schedule(JobAfterFailure $job) + public function schedule(JobAfterFailure $job): void { $exception = $job->causeOfFailure(); if ($this->isRetriable($exception)) { @@ -55,56 +50,47 @@ public function schedule(JobAfterFailure $job) } } - /** - * {@inheritDoc} - */ public function export(): array { return array_map( - function(RetriableException $retriableException) { + function (RetriableException $retriableException) { $retryPolicy = $retriableException->retryPolicy(); + return [ 'when' => $retriableException->exceptionClass(), 'then' => [ - 'class' => get_class($retryPolicy), - 'parameters' => $retryPolicy->export() - ] + 'class' => $retryPolicy::class, + 'parameters' => $retryPolicy->export(), + ], ]; }, - $this->exceptions + $this->exceptions, ); } - /** - * {@inheritDoc} - */ public static function import(array $parameters): RetryPolicy { return new self( array_reduce( $parameters, - function($exceptions, $parameters) { + function ($exceptions, $parameters) { $exceptionClass = $parameters['when']; $retryPolicyClass = $parameters['then']['class']; $retryPolicyParameters = $parameters['then']['parameters']; $exceptions[] = new RetriableException($exceptionClass, $retryPolicyClass::import($retryPolicyParameters)); + return $exceptions; - } - ) + }, + ), ); } - /** - * {@inheritDoc} - */ public function isLastRetry(Job $job): bool { // I cannot answer to that so... true only if everybody says true return array_all( $this->exceptions, - function(RetriableException $retriableException) use ($job) { - return $retriableException->retryPolicy()->isLastRetry($job); - } + fn (RetriableException $retriableException) => $retriableException->retryPolicy()->isLastRetry($job), ); } @@ -112,12 +98,16 @@ private function isRetriable($exception): bool { try { $this->retryPolicyFor($exception); + return true; - } catch (Exception $e) { + } catch (\Exception) { return false; } } + /** + * @throws \Exception + */ private function retryPolicyFor(?object $exception): RetryPolicy { if (!is_null($exception) && is_object($exception)) { @@ -128,14 +118,10 @@ private function retryPolicyFor(?object $exception): RetryPolicy return $retriableException->retryPolicy(); } } - if ($exception instanceof Throwable) { - throw new Exception( - 'Unable to find a RetryPolicy associated to exception: ' . get_class($exception), 0, $exception - ); + if ($exception instanceof \Throwable) { + throw new \Exception('Unable to find a RetryPolicy associated to exception: ' . $exception::class, 0, $exception); } } - throw new Exception( - 'Unable to find a RetryPolicy associated to: ' . var_export($exception, true) - ); + throw new \Exception('Unable to find a RetryPolicy associated to: ' . var_export($exception, true)); } } diff --git a/src/Recruiter/RetryPolicy/SelectByExceptionBuilder.php b/src/Recruiter/RetryPolicy/SelectByExceptionBuilder.php index b02f6602..6fa68c5c 100644 --- a/src/Recruiter/RetryPolicy/SelectByExceptionBuilder.php +++ b/src/Recruiter/RetryPolicy/SelectByExceptionBuilder.php @@ -1,12 +1,9 @@ currentException = $exceptionClass; + return $this; } @@ -39,6 +37,7 @@ public function then(RetryPolicy $retryPolicy): self } $this->exceptions[] = new RetriableException($this->currentException, $retryPolicy); $this->currentException = null; + return $this; } @@ -50,6 +49,7 @@ public function build(): SelectByException if (empty($this->exceptions)) { throw new LogicException('No retry policies has been specified. Use `$builder->when($e)->then($r)`'); } + return new SelectByException($this->exceptions); } } diff --git a/src/Recruiter/RetryPolicy/TimeTable.php b/src/Recruiter/RetryPolicy/TimeTable.php index 0231b57c..eec7a965 100644 --- a/src/Recruiter/RetryPolicy/TimeTable.php +++ b/src/Recruiter/RetryPolicy/TimeTable.php @@ -3,23 +3,21 @@ namespace Recruiter\RetryPolicy; use Recruiter\Job; +use Recruiter\JobAfterFailure; use Recruiter\RetryPolicy; use Recruiter\RetryPolicyBehaviour; -use Recruiter\JobAfterFailure; - use Timeless as T; -use Exception; - class TimeTable implements RetryPolicy { - /** @var array */ - private $timeTable; - - private $howManyRetries; - use RetryPolicyBehaviour; + private ?array $timeTable; + + private int $howManyRetries; + /** + * @throws \Exception + */ public function __construct(?array $timeTable) { if (is_null($timeTable)) { @@ -33,7 +31,7 @@ public function __construct(?array $timeTable) $this->howManyRetries = self::estimateHowManyRetriesIn($timeTable); } - public function schedule(JobAfterFailure $job) + public function schedule(JobAfterFailure $job): void { foreach ($this->timeTable as $timeSpent => $rescheduleIn) { if ($this->hasBeenCreatedLessThan($job, $timeSpent)) { @@ -47,6 +45,7 @@ public function isLastRetry(Job $job): bool { $timeSpents = array_keys($this->timeTable); $timeSpent = end($timeSpents); + return !$this->hasBeenCreatedLessThan($job, $timeSpent); } @@ -63,42 +62,37 @@ public static function import(array $parameters): RetryPolicy private function hasBeenCreatedLessThan($job, $relativeTime) { return $job->createdAt()->isAfter( - T\Moment::fromTimestamp(strtotime($relativeTime, T\now()->seconds())) + T\Moment::fromTimestamp(strtotime((string) $relativeTime, T\now()->seconds())), ); } - private function rescheduleIn($job, $relativeTime) + private function rescheduleIn($job, $relativeTime): void { $job->scheduleAt( - T\Moment::fromTimestamp(strtotime($relativeTime, T\now()->seconds())) + T\Moment::fromTimestamp(strtotime((string) $relativeTime, T\now()->seconds())), ); } - private static function estimateHowManyRetriesIn($timeTable) + private static function estimateHowManyRetriesIn(array $timeTable): int { $now = T\now()->seconds(); $howManyRetries = 0; $timeWindowInSeconds = 0; foreach ($timeTable as $timeWindow => $rescheduleTime) { - $timeWindowInSeconds = ($now - strtotime($timeWindow, $now)) - $timeWindowInSeconds; + $timeWindowInSeconds = ($now - strtotime((string) $timeWindow, $now)) - $timeWindowInSeconds; if ($timeWindowInSeconds <= 0) { - throw new Exception( - "Time window `$timeWindow` is invalid, must be in the past" - ); + throw new \Exception("Time window `$timeWindow` is invalid, must be in the past"); } - $rescheduleTimeInSeconds = (strtotime($rescheduleTime, $now) - $now); + $rescheduleTimeInSeconds = (strtotime((string) $rescheduleTime, $now) - $now); if ($rescheduleTimeInSeconds <= 0) { - throw new Exception( - "Reschedule time `$rescheduleTime` is invalid, must be in the future" - ); + throw new \Exception("Reschedule time `$rescheduleTime` is invalid, must be in the future"); } if ($rescheduleTimeInSeconds > $timeWindowInSeconds) { - throw new Exception( - "Reschedule time `$rescheduleTime` is invalid, must be greater than the time window" - ); + throw new \Exception("Reschedule time `$rescheduleTime` is invalid, must be greater than the time window"); } $howManyRetries += floor($timeWindowInSeconds / $rescheduleTimeInSeconds); } + return $howManyRetries; } } diff --git a/src/Recruiter/RetryPolicyBehaviour.php b/src/Recruiter/RetryPolicyBehaviour.php index 9944ce04..caea789a 100644 --- a/src/Recruiter/RetryPolicyBehaviour.php +++ b/src/Recruiter/RetryPolicyBehaviour.php @@ -2,9 +2,7 @@ namespace Recruiter; -use Exception; use Recruiter\RetryPolicy\RetriableExceptionFilter; -use Recruiter\JobAfterFailure; trait RetryPolicyBehaviour { @@ -25,9 +23,9 @@ public function retryOnlyWhenExceptionsAre($retriableExceptionTypes) return new RetriableExceptionFilter($this, $retriableExceptionTypes); } - public function schedule(JobAfterFailure $job) + public function schedule(JobAfterFailure $job): void { - throw new Exception('RetryPolicy::schedule(JobAfterFailure) need to be implemented'); + throw new \Exception('RetryPolicy::schedule(JobAfterFailure) need to be implemented'); } public function export(): array diff --git a/src/Recruiter/RetryPolicyInJob.php b/src/Recruiter/RetryPolicyInJob.php index 5f34814f..dd5b5205 100644 --- a/src/Recruiter/RetryPolicyInJob.php +++ b/src/Recruiter/RetryPolicyInJob.php @@ -2,26 +2,24 @@ namespace Recruiter; -use Exception; -use Recruiter\RetryPolicy; - class RetryPolicyInJob { public static function import($document) { if (!array_key_exists('retry_policy', $document)) { - throw new Exception('Unable to import Job without data about RetryPolicy object'); + throw new \Exception('Unable to import Job without data about RetryPolicy object'); } $dataAboutRetryPolicyObject = $document['retry_policy']; if (!array_key_exists('class', $dataAboutRetryPolicyObject)) { - throw new Exception('Unable to import Job without a class'); + throw new \Exception('Unable to import Job without a class'); } if (!class_exists($dataAboutRetryPolicyObject['class'])) { - throw new Exception('Unable to import Job with unknown RetryPolicy class'); + throw new \Exception('Unable to import Job with unknown RetryPolicy class'); } if (!method_exists($dataAboutRetryPolicyObject['class'], 'import')) { - throw new Exception('Unable to import RetryPolicy without method import'); + throw new \Exception('Unable to import RetryPolicy without method import'); } + return $dataAboutRetryPolicyObject['class']::import($dataAboutRetryPolicyObject['parameters']); } @@ -29,9 +27,9 @@ public static function export($retryPolicy) { return [ 'retry_policy' => [ - 'class' => get_class($retryPolicy), + 'class' => $retryPolicy::class, 'parameters' => $retryPolicy->export(), - ] + ], ]; } diff --git a/src/Recruiter/SchedulePolicy.php b/src/Recruiter/SchedulePolicy.php index 21aeadd3..f10c701a 100644 --- a/src/Recruiter/SchedulePolicy.php +++ b/src/Recruiter/SchedulePolicy.php @@ -7,25 +7,19 @@ interface SchedulePolicy { /** - * Returns the next time the job is to be executed - * - * @return Moment + * Returns the next time the job is to be executed. */ public function next(): Moment; /** - * Export schedule policy parameters - * - * @return array + * Export schedule policy parameters. */ public function export(): array; /** - * Import schedule policy parameters + * Import schedule policy parameters. * * @param array $parameters Previously exported parameters - * - * @return SchedulePolicy */ public static function import(array $parameters): SchedulePolicy; } diff --git a/src/Recruiter/SchedulePolicy/Cron.php b/src/Recruiter/SchedulePolicy/Cron.php index 24afd0cc..73e42f86 100644 --- a/src/Recruiter/SchedulePolicy/Cron.php +++ b/src/Recruiter/SchedulePolicy/Cron.php @@ -3,27 +3,19 @@ namespace Recruiter\SchedulePolicy; use Cron\CronExpression; -use DateInterval; -use DateTime; use Recruiter\SchedulePolicy; - use Timeless\Moment; class Cron implements SchedulePolicy { - private $cronExpression; - private $now; - - public function __construct(string $cronExpression, ?DateTime $now = null) + public function __construct(private readonly string $cronExpression, private readonly ?\DateTime $now = null) { - $this->cronExpression = $cronExpression; - $this->now = $now; } public function next(): Moment { return Moment::fromDateTime( - CronExpression::factory($this->cronExpression)->getNextRunDate($this->now ?? 'now') + CronExpression::factory($this->cronExpression)->getNextRunDate($this->now ?? 'now'), ); } @@ -39,8 +31,8 @@ public static function import(array $parameters): SchedulePolicy { $now = null; if (isset($parameters['now'])) { - $now = DateTime::createFromFormat('U', $parameters['now']); - $now = $now === false ? null : $now; + $now = \DateTime::createFromFormat('U', $parameters['now']); + $now = false === $now ? null : $now; } return new self($parameters['cron_expression'], $now); diff --git a/src/Recruiter/SchedulePolicy/EveryMinutes.php b/src/Recruiter/SchedulePolicy/EveryMinutes.php index 7245687e..908e5d55 100644 --- a/src/Recruiter/SchedulePolicy/EveryMinutes.php +++ b/src/Recruiter/SchedulePolicy/EveryMinutes.php @@ -2,9 +2,7 @@ namespace Recruiter\SchedulePolicy; -use DateInterval; use Recruiter\SchedulePolicy; - use Timeless\Moment; class EveryMinutes implements SchedulePolicy diff --git a/src/Recruiter/SchedulePolicyInJob.php b/src/Recruiter/SchedulePolicyInJob.php index d1671ce6..184b9242 100644 --- a/src/Recruiter/SchedulePolicyInJob.php +++ b/src/Recruiter/SchedulePolicyInJob.php @@ -2,26 +2,24 @@ namespace Recruiter; -use Exception; -use Recruiter\SchedulePolicy; - class SchedulePolicyInJob { public static function import($document): SchedulePolicy { if (!array_key_exists('schedule_policy', $document)) { - throw new Exception('Unable to import Job without data about SchedulePolicy object'); + throw new \Exception('Unable to import Job without data about SchedulePolicy object'); } $dataAboutSchedulePolicyObject = $document['schedule_policy']; if (!array_key_exists('class', $dataAboutSchedulePolicyObject)) { - throw new Exception('Unable to import Job without a SchedulePolicy class'); + throw new \Exception('Unable to import Job without a SchedulePolicy class'); } if (!class_exists($dataAboutSchedulePolicyObject['class'])) { - throw new Exception('Unable to import Job with unknown SchedulePolicy class'); + throw new \Exception('Unable to import Job with unknown SchedulePolicy class'); } if (!method_exists($dataAboutSchedulePolicyObject['class'], 'import')) { - throw new Exception('Unable to import SchedulePolicy without method import'); + throw new \Exception('Unable to import SchedulePolicy without method import'); } + return $dataAboutSchedulePolicyObject['class']::import($dataAboutSchedulePolicyObject['parameters']); } @@ -29,9 +27,9 @@ public static function export($schedulePolicy) { return [ 'schedule_policy' => [ - 'class' => get_class($schedulePolicy), + 'class' => $schedulePolicy::class, 'parameters' => $schedulePolicy->export(), - ] + ], ]; } diff --git a/src/Recruiter/Scheduler.php b/src/Recruiter/Scheduler.php index 01b272db..409c17ff 100644 --- a/src/Recruiter/Scheduler.php +++ b/src/Recruiter/Scheduler.php @@ -2,44 +2,26 @@ namespace Recruiter; -use MongoDB\BSON\ObjectId; -use Recruiter\Scheduler\Repository; use Recruiter\Job\Repository as JobsRepository; -use Recruiter\RetryPolicy; -use RuntimeException; -use Symfony\Component\EventDispatcher\EventDispatcher; -use Throwable; +use Recruiter\Scheduler\Repository; use Timeless as T; -use Timeless\Interval; -use Timeless\Moment; class Scheduler { private $job; - private $schedulers; - - private $status; - - private $schedulePolicy; - - private $repeatable; - - private $retryPolicy; - public static function around(Repeatable $repeatable, Repository $repository, Recruiter $recruiter) { $retryPolicy = ($repeatable instanceof Retriable) ? $repeatable->retryWithPolicy() : - new RetryPolicy\DoNotDoItAgain() - ; + new RetryPolicy\DoNotDoItAgain(); return new self( self::initialize(), $repeatable, null, $retryPolicy, - $repository + $repository, ); } @@ -50,22 +32,12 @@ public static function import($document, Repository $repository) RepeatableInJob::import($document['job']), SchedulePolicyInJob::import($document), RetryPolicyInJob::import($document['job']), - $repository + $repository, ); } - public function __construct( - array $status, - Repeatable $repeatable, - ?SchedulePolicy $schedulePolicy, - ?RetryPolicy $retryPolicy, - Repository $schedulers - ) { - $this->status = $status; - $this->repeatable = $repeatable; - $this->schedulePolicy = $schedulePolicy; - $this->retryPolicy = $retryPolicy; - $this->schedulers = $schedulers; + public function __construct(private array $status, private readonly Repeatable $repeatable, private ?SchedulePolicy $schedulePolicy, private ?RetryPolicy $retryPolicy, private readonly Repository $schedulers) + { } public function create() @@ -103,9 +75,9 @@ public function export() [ 'job' => array_merge( WorkableInJob::export($this->repeatable, 'execute'), - RetryPolicyInJob::export($this->retryPolicy) + RetryPolicyInJob::export($this->retryPolicy), ), - ] + ], ); } @@ -128,8 +100,9 @@ private function aJobIsStillRunning(JobsRepository $jobs) try { $alreadyScheduledJob = $jobs->scheduled($this->status['last_scheduling']['job_id']); + return true; - } catch (Throwable $e) { + } catch (\Throwable) { return false; } } @@ -137,7 +110,7 @@ private function aJobIsStillRunning(JobsRepository $jobs) public function schedule(JobsRepository $jobs) { if (!$this->schedulePolicy) { - throw new RuntimeException('You need to assign a `SchedulePolicy` (use `repeatWithPolicy` to inject it) in order to schedule a job'); + throw new \RuntimeException('You need to assign a `SchedulePolicy` (use `repeatWithPolicy` to inject it) in order to schedule a job'); } $nextScheduling = $this->schedulePolicy->next(); @@ -151,10 +124,10 @@ public function schedule(JobsRepository $jobs) $this->status['last_scheduling']['scheduled_at'] = T\MongoDate::from($nextScheduling); $this->status['last_scheduling']['job_id'] = null; - $this->status['attempts'] = $this->status['attempts'] + 1; + ++$this->status['attempts']; $this->schedulers->save($this); - $jobToSchedule = (new JobToSchedule(Job::around($this->repeatable, $jobs))) + $jobToSchedule = new JobToSchedule(Job::around($this->repeatable, $jobs)) ->scheduleAt($nextScheduling) ->retryWithPolicy($this->retryPolicy) ->scheduledBy('scheduler', $this->status['urn'], $this->status['attempts']) @@ -169,7 +142,7 @@ public function retryWithPolicy(RetryPolicy $retryPolicy, $retriableExceptionTyp { $this->retryPolicy = $this->filterForRetriableExceptions( $retryPolicy, - $retriableExceptionTypes + $retriableExceptionTypes, ); return $this; diff --git a/src/Recruiter/Scheduler/Repository.php b/src/Recruiter/Scheduler/Repository.php index df95d856..d22ccb96 100644 --- a/src/Recruiter/Scheduler/Repository.php +++ b/src/Recruiter/Scheduler/Repository.php @@ -1,14 +1,9 @@ map( $this->schedulers->find([], [ 'sort' => ['scheduled_at' => -1], - ]) + ]), ); } @@ -34,7 +29,7 @@ public function save(Scheduler $scheduler) $this->schedulers->replaceOne( ['_id' => $document['_id']], $document, - ['upsert' => true] + ['upsert' => true], ); } @@ -45,17 +40,15 @@ public function create(Scheduler $scheduler) if (0 === $this->schedulers->count(['urn' => $document['urn']])) { $this->schedulers->insertOne($document); } else { - $document = array_filter($document, function ($key) { - return in_array($key, [ - 'job', - 'schedule_policy', - 'unique', - ]); - }, ARRAY_FILTER_USE_KEY); + $document = array_filter($document, fn ($key) => in_array($key, [ + 'job', + 'schedule_policy', + 'unique', + ]), ARRAY_FILTER_USE_KEY); $this->schedulers->updateOne( ['urn' => $scheduler->urn()], - ['$set' => $document] + ['$set' => $document], ); } } diff --git a/src/Recruiter/SynchronousExecutionReport.php b/src/Recruiter/SynchronousExecutionReport.php index ebd1ac46..f2f80cda 100644 --- a/src/Recruiter/SynchronousExecutionReport.php +++ b/src/Recruiter/SynchronousExecutionReport.php @@ -1,40 +1,32 @@ data = $data; } - /** - *. @params array $data : key value array where key are the id of the job and value is the JobExecution + *. @params array $data : key value array where key are the id of the job and value is the JobExecution. */ public static function fromArray(array $data): SynchronousExecutionReport { return new self($data); } - public function isThereAFailure() + public function isThereAFailure(): bool { - return array_some($this->data, function ($jobExecution, $jobId) { - return $jobExecution->isFailed(); - }); + return array_any($this->data, fn ($jobExecution, $jobId) => $jobExecution->isFailed()); } public function toArray() diff --git a/src/Recruiter/Taggable.php b/src/Recruiter/Taggable.php index 6ab0d0e0..0d2e37db 100644 --- a/src/Recruiter/Taggable.php +++ b/src/Recruiter/Taggable.php @@ -5,9 +5,9 @@ interface Taggable { /** - * A Job can decide its own tags. Tags are useful to correlate jobs + * A Job can decide its own tags. Tags are useful to correlate jobs. * - * @return array Strings to be used to tag the job + * @return array Strings to be used to tag the job */ - public function taggedAs(); + public function taggedAs(): array; } diff --git a/src/Recruiter/WaitStrategy.php b/src/Recruiter/WaitStrategy.php index 4e41d029..8278023c 100644 --- a/src/Recruiter/WaitStrategy.php +++ b/src/Recruiter/WaitStrategy.php @@ -9,19 +9,18 @@ class WaitStrategy private $timeToWaitAtLeast; private $timeToWaitAtMost; private $timeToWait; - private $howToWait; - public function __construct(Interval $timeToWaitAtLeast, Interval $timeToWaitAtMost, $howToWait = 'usleep') + public function __construct(Interval $timeToWaitAtLeast, Interval $timeToWaitAtMost, private $howToWait = 'usleep') { $this->timeToWaitAtLeast = $timeToWaitAtLeast->milliseconds(); $this->timeToWaitAtMost = $timeToWaitAtMost->milliseconds(); $this->timeToWait = $timeToWaitAtLeast->milliseconds(); - $this->howToWait = $howToWait; } public function reset() { $this->timeToWait = $this->timeToWaitAtLeast; + return $this; } @@ -29,8 +28,9 @@ public function goForward() { $this->timeToWait = max( $this->timeToWait / 2, - $this->timeToWaitAtLeast + $this->timeToWaitAtLeast, ); + return $this; } @@ -38,14 +38,16 @@ public function backOff() { $this->timeToWait = min( $this->timeToWait * 2, - $this->timeToWaitAtMost + $this->timeToWaitAtMost, ); + return $this; } public function wait() { call_user_func($this->howToWait, $this->timeToWait * 1000); + return $this; } diff --git a/src/Recruiter/Workable.php b/src/Recruiter/Workable.php index aefa2471..937317a1 100644 --- a/src/Recruiter/Workable.php +++ b/src/Recruiter/Workable.php @@ -5,27 +5,19 @@ interface Workable { /** - * Turn this `Recruiter\Workable` instance into a `Recruiter\Job` instance - * - * @param Recruiter $recruiter - * - * @return JobToSchedule + * Turn this `Recruiter\Workable` instance into a `Recruiter\Job` instance. */ - public function asJobOf(Recruiter $recruiter); + public function asJobOf(Recruiter $recruiter): JobToSchedule; /** - * Export parameters that need to be persisted - * - * @return array + * Export parameters that need to be persisted. */ - public function export(); + public function export(): array; /** - * Import an array of parameters as a Workable instance + * Import an array of parameters as a Workable instance. * * @param array $parameters Previously exported parameters - * - * @return Workable */ - public static function import($parameters); + public static function import(array $parameters): static; } diff --git a/src/Recruiter/Workable/AlwaysFail.php b/src/Recruiter/Workable/AlwaysFail.php index 485e967e..f0439e11 100644 --- a/src/Recruiter/Workable/AlwaysFail.php +++ b/src/Recruiter/Workable/AlwaysFail.php @@ -1,7 +1,7 @@ parameters['howManyItems']); } } - diff --git a/src/Recruiter/Workable/ThrowsFatalError.php b/src/Recruiter/Workable/ExitsAbruptly.php similarity index 55% rename from src/Recruiter/Workable/ThrowsFatalError.php rename to src/Recruiter/Workable/ExitsAbruptly.php index f8a16eaa..dd3e5876 100644 --- a/src/Recruiter/Workable/ThrowsFatalError.php +++ b/src/Recruiter/Workable/ExitsAbruptly.php @@ -1,15 +1,16 @@ steps = $steps; } - public function asJobOf(Recruiter $recruiter) + public function asJobOf(Recruiter $recruiter): JobToSchedule { return $recruiter->jobOf($this); } @@ -58,9 +57,9 @@ public function execute($retryOptions = null) $callable = [$result, $step['method']]; } if (!is_callable($callable)) { - $message = "The following step does not result in a callable: " . var_export($step, true) . "."; + $message = 'The following step does not result in a callable: ' . var_export($step, true) . '.'; if (is_object($result)) { - $message .= ' Reached object: ' . get_class($result); + $message .= ' Reached object: ' . $result::class; } else { $message .= ' Reached value: ' . var_export($result, true); } @@ -72,15 +71,16 @@ public function execute($retryOptions = null) } $result = call_user_func_array( $callable, - $arguments + $arguments, ); } + return $result; } private function arguments($step) { - $arguments = isset($step['arguments']) ? $step['arguments'] : []; + $arguments = $step['arguments'] ?? []; return $arguments; } @@ -94,18 +94,19 @@ public function __call($method, $arguments) $step['arguments'] = $arguments; } $this->steps[] = $step; + return $this; } - public function export() + public function export(): array { return [ 'steps' => $this->steps, ]; } - public static function import($document) + public static function import(array $parameters): static { - return new self($document['steps']); + return new self($parameters['steps']); } } diff --git a/src/Recruiter/Workable/FailsInConstructor.php b/src/Recruiter/Workable/FailsInConstructor.php index 0c1e25df..2d70eb0e 100644 --- a/src/Recruiter/Workable/FailsInConstructor.php +++ b/src/Recruiter/Workable/FailsInConstructor.php @@ -1,7 +1,7 @@ usToSleep = $usToSleep; - $this->usOfDelta = $usOfDelta; } - public function execute() + public function execute(): void { - usleep($this->usToSleep + (rand(intval(-$this->usOfDelta), $this->usOfDelta))); + usleep($this->usToSleep + random_int(intval(-$this->usOfDelta), $this->usOfDelta)); } - public function export() + public function export(): array { return [ 'us_to_sleep' => $this->usToSleep, @@ -41,11 +36,11 @@ public function export() ]; } - public static function import($parameters) + public static function import(array $parameters): static { return new self( $parameters['us_to_sleep'], - $parameters['us_of_delta'] + $parameters['us_of_delta'], ); } } diff --git a/src/Recruiter/Workable/RecoverRepeatableFromException.php b/src/Recruiter/Workable/RecoverRepeatableFromException.php index 4fcce94c..bb33d04e 100644 --- a/src/Recruiter/Workable/RecoverRepeatableFromException.php +++ b/src/Recruiter/Workable/RecoverRepeatableFromException.php @@ -1,7 +1,7 @@ parameters = $parameters; - $this->recoverForClass = $recoverForClass; - $this->recoverForException = $recoverForException; } - public function execute() + public function execute(): never { - throw new \Exception( - 'This job failed while instantiating a workable of class: ' . $this->recoverForClass . PHP_EOL . - 'Original exception: ' . get_class($this->recoverForException) . PHP_EOL . - $this->recoverForException->getMessage() . PHP_EOL . - $this->recoverForException->getTraceAsString() . PHP_EOL - ); + throw new \Exception('This job failed while instantiating a workable of class: ' . $this->recoverForClass . PHP_EOL . 'Original exception: ' . $this->recoverForException::class . PHP_EOL . $this->recoverForException->getMessage() . PHP_EOL . $this->recoverForException->getTraceAsString() . PHP_EOL); } public function getClass() @@ -38,6 +28,7 @@ public function urn(): string { $recoverForInstance = new $this->recoverForClass($this->parameters); assert($recoverForInstance instanceof Repeatable); + return $recoverForInstance->urn(); } @@ -45,6 +36,7 @@ public function unique(): bool { $recoverForInstance = new $this->recoverForClass($this->parameters); assert($recoverForInstance instanceof Repeatable); + return $recoverForInstance->unique(); } } diff --git a/src/Recruiter/Workable/RecoverWorkableFromException.php b/src/Recruiter/Workable/RecoverWorkableFromException.php index 53e2fc67..ebeacc86 100644 --- a/src/Recruiter/Workable/RecoverWorkableFromException.php +++ b/src/Recruiter/Workable/RecoverWorkableFromException.php @@ -1,7 +1,7 @@ parameters = $parameters; - $this->recoverForClass = $recoverForClass; - $this->recoverForException = $recoverForException; } - public function execute() + public function execute(): never { - throw new Exception( - 'This job failed while instantiating a workable of class: ' . $this->recoverForClass . PHP_EOL . - 'Original exception: ' . get_class($this->recoverForException) . PHP_EOL . - $this->recoverForException->getMessage() . PHP_EOL . - $this->recoverForException->getTraceAsString() . PHP_EOL - ); + throw new \Exception('This job failed while instantiating a workable of class: ' . $this->recoverForClass . PHP_EOL . 'Original exception: ' . $this->recoverForException::class . PHP_EOL . $this->recoverForException->getMessage() . PHP_EOL . $this->recoverForException->getTraceAsString() . PHP_EOL); } public function getClass() diff --git a/src/Recruiter/Workable/SampleRepeatableCommand.php b/src/Recruiter/Workable/SampleRepeatableCommand.php index 92a57b5c..07bb97be 100644 --- a/src/Recruiter/Workable/SampleRepeatableCommand.php +++ b/src/Recruiter/Workable/SampleRepeatableCommand.php @@ -3,19 +3,18 @@ namespace Recruiter\Workable; use Recruiter\Repeatable; -use Recruiter\SchedulePolicy\EveryMinutes; -use Recruiter\SchedulePolicy; +use Recruiter\RepeatableBehaviour; use Recruiter\Workable; use Recruiter\WorkableBehaviour; -use Recruiter\RepeatableBehaviour; class SampleRepeatableCommand implements Workable, Repeatable { - use WorkableBehaviour, RepeatableBehaviour; + use WorkableBehaviour; + use RepeatableBehaviour; public function execute() { - var_export((new \DateTime())->format('c')); + var_export(new \DateTime()->format('c')); } public function urn(): string diff --git a/src/Recruiter/Workable/ShellCommand.php b/src/Recruiter/Workable/ShellCommand.php index 62aea315..c92778cc 100644 --- a/src/Recruiter/Workable/ShellCommand.php +++ b/src/Recruiter/Workable/ShellCommand.php @@ -4,40 +4,37 @@ use Recruiter\Workable; use Recruiter\WorkableBehaviour; -use RuntimeException; class ShellCommand implements Workable { use WorkableBehaviour; - private $commandLine; - public static function fromCommandLine($commandLine) { return new self($commandLine); } - private function __construct($commandLine) + private function __construct(private $commandLine) { - $this->commandLine = $commandLine; } public function execute() { exec($this->commandLine, $output, $returnCode); $output = implode(PHP_EOL, $output); - if ($returnCode != 0) { - throw new RuntimeException("Command execution failed (return code $returnCode). Output: " . $output); + if (0 != $returnCode) { + throw new \RuntimeException("Command execution failed (return code $returnCode). Output: " . $output); } + return $output; } - public function export() + public function export(): array { return ['command' => $this->commandLine]; } - public static function import($parameters) + public static function import(array $parameters): static { return new self($parameters['command']); } diff --git a/src/Recruiter/WorkableBehaviour.php b/src/Recruiter/WorkableBehaviour.php index 8cfbc88b..dd6f40e3 100644 --- a/src/Recruiter/WorkableBehaviour.php +++ b/src/Recruiter/WorkableBehaviour.php @@ -2,33 +2,28 @@ namespace Recruiter; -use Exception; - trait WorkableBehaviour { - protected $parameters; - - public function __construct($parameters = []) + final public function __construct(protected array $parameters = []) { - $this->parameters = $parameters; } - public function asJobOf(Recruiter $recruiter) + public function asJobOf(Recruiter $recruiter): JobToSchedule { return $recruiter->jobOf($this); } - public function execute() + public function execute(): never { - throw new Exception('Workable::execute() need to be implemented'); + throw new \Exception('Workable::execute() need to be implemented'); } - public function export() + public function export(): array { return $this->parameters; } - public static function import($parameters) + public static function import(array $parameters): static { return new static($parameters); } diff --git a/src/Recruiter/WorkableInJob.php b/src/Recruiter/WorkableInJob.php index 4ec2f421..f09b7f87 100644 --- a/src/Recruiter/WorkableInJob.php +++ b/src/Recruiter/WorkableInJob.php @@ -1,9 +1,8 @@ self::classNameOf($workable), 'parameters' => $workable->export(), 'method' => $methodToCall, - ] + ], ]; } @@ -56,10 +55,11 @@ public static function initialize() private static function classNameOf($workable) { - $workableClassName = get_class($workable); + $workableClassName = $workable::class; if (method_exists($workable, 'getClass')) { $workableClassName = $workable->getClass(); } + return $workableClassName; } } diff --git a/src/Recruiter/Worker.php b/src/Recruiter/Worker.php index 8ea0c97c..072d5e3c 100644 --- a/src/Recruiter/Worker.php +++ b/src/Recruiter/Worker.php @@ -2,8 +2,6 @@ namespace Recruiter; -use DateInterval; -use DateTimeImmutable; use MongoDB\BSON\ObjectId; use MongoDB\Collection as MongoCollection; use Recruiter\Infrastructure\Memory\MemoryLimit; @@ -11,35 +9,22 @@ use Recruiter\Worker\Repository; use Timeless as T; use Timeless\Interval; -use Timeless\Moment; class Worker { - private $status; - private $recruiter; - private $repository; - private $memoryLimit; - public static function workFor( Recruiter $recruiter, Repository $repository, - MemoryLimit $memoryLimit + MemoryLimit $memoryLimit, ) { $worker = new self(self::initialize(), $recruiter, $repository, $memoryLimit); $worker->save(); + return $worker; } - public function __construct( - $status, - Recruiter $recruiter, - Repository $repository, - MemoryLimit $memoryLimit - ) { - $this->status = $status; - $this->recruiter = $recruiter; - $this->repository = $repository; - $this->memoryLimit = $memoryLimit; + public function __construct(private $status, private readonly Recruiter $recruiter, private readonly Repository $repository, private readonly MemoryLimit $memoryLimit) + { } public function id() @@ -58,12 +43,14 @@ public function work() if ($this->hasBeenAssignedToDoSomething()) { $this->workOn( $job = $this->recruiter->scheduledJob( - $this->status['assigned_to'][(string)$this->status['_id']] - ) + $this->status['assigned_to'][(string) $this->status['_id']], + ), ); + return (string) $job->id(); } else { $this->stillHere(); + return false; } } @@ -131,8 +118,8 @@ private function afterExecutionOf($job) date('c'), $this->id(), $job->id(), - get_class($e), - $e->getMessage() + $e::class, + $e->getMessage(), ); $this->retireAfterMemoryLimitIsExceeded(); @@ -154,7 +141,6 @@ private function retireAfterMemoryLimitIsExceeded() $this->repository->retireWorkerWithId($this->id()); } - private function hasBeenAssignedToDoSomething() { if (is_null($this->status)) { @@ -164,6 +150,7 @@ private function hasBeenAssignedToDoSomething() // thing to do seems like terminate the process exit(1); } + return array_key_exists('assigned_to', $this->status); } @@ -192,13 +179,13 @@ private static function initialize() 'last_seen_at' => T\MongoDate::now(), 'created_at' => T\MongoDate::now(), 'working' => false, - 'pid' => getmypid() + 'pid' => getmypid(), ]; } public static function canWorkOnAnyJobs($worksOn) { - return $worksOn === '*'; + return '*' === $worksOn; } public static function pickAvailableWorkers(MongoCollection $collection, $workersPerUnit) @@ -208,9 +195,7 @@ public static function pickAvailableWorkers(MongoCollection $collection, $worker if (count($workers) > 0) { $unitsOfWorkers = array_group_by( $workers, - function ($worker) { - return $worker['work_on']; - } + fn ($worker) => $worker['work_on'], ); foreach ($unitsOfWorkers as $workOn => $workersInUnit) { $workersInUnit = array_column($workersInUnit, '_id'); @@ -218,16 +203,15 @@ function ($worker) { $result[] = [$workOn, $workersInUnit]; } } + return $result; } public static function tryToAssignJobsToWorkers(MongoCollection $collection, $jobs, $workers) { $assignment = array_combine( - array_map(function ($id) { - return (string)$id; - }, $workers), - $jobs + array_map(fn ($id) => (string) $id, $workers), + $jobs, ); $result = $collection->updateMany( @@ -235,8 +219,8 @@ public static function tryToAssignJobsToWorkers(MongoCollection $collection, $jo $update = ['$set' => [ 'available' => false, 'assigned_to' => $assignment, - 'assigned_since' => T\MongoDate::now() - ]] + 'assigned_since' => T\MongoDate::now(), + ]], ); return [$assignment, $result->getModifiedCount()]; @@ -258,7 +242,7 @@ public static function assignedJobs(MongoCollection $collection) return array_values(array_unique($jobs)); } - public static function retireDeadWorkers(Repository $roster, DateTimeImmutable $now, Interval $consideredDeadAfter) + public static function retireDeadWorkers(Repository $roster, \DateTimeImmutable $now, Interval $consideredDeadAfter) { $consideredDeadAt = $now->sub($consideredDeadAfter->toDateInterval()); $deadWorkers = $roster->deadWorkers($consideredDeadAt); @@ -266,8 +250,8 @@ public static function retireDeadWorkers(Repository $roster, DateTimeImmutable $ foreach ($deadWorkers as $deadWorker) { $roster->retireWorkerWithId($deadWorker['_id']); if (array_key_exists('assigned_to', $deadWorker)) { - if (array_key_exists((string)$deadWorker['_id'], $deadWorker['assigned_to'])) { - $jobsToReassign[] = $deadWorker['assigned_to'][(string)$deadWorker['_id']]; + if (array_key_exists((string) $deadWorker['_id'], $deadWorker['assigned_to'])) { + $jobsToReassign[] = $deadWorker['assigned_to'][(string) $deadWorker['_id']]; } } } diff --git a/src/Recruiter/Worker/Process.php b/src/Recruiter/Worker/Process.php index 064f52ee..815be80f 100644 --- a/src/Recruiter/Worker/Process.php +++ b/src/Recruiter/Worker/Process.php @@ -3,38 +3,35 @@ namespace Recruiter\Worker; use Sink\BlackHole; -use Recruiter\Worker\Repository; class Process { - private $pid; - - public static function withPid($pid) + public static function withPid(int $pid) { return new self($pid); } - public function __construct($pid) + public function __construct(private readonly int $pid) { - $this->pid = $pid; } - public function cleanUp(Repository $repository) + public function cleanUp(Repository $repository): void { if (!$this->isAlive()) { $repository->retireWorkerWithPid($this->pid); } } - public function ifDead() + public function ifDead(): BlackHole|static { if ($this->isAlive()) { return new BlackHole(); } + return $this; } - protected function isAlive() + protected function isAlive(): bool { return posix_kill($this->pid, 0); } diff --git a/src/Recruiter/Worker/Repository.php b/src/Recruiter/Worker/Repository.php index b54ad992..74fd14a9 100644 --- a/src/Recruiter/Worker/Repository.php +++ b/src/Recruiter/Worker/Repository.php @@ -5,17 +5,14 @@ use MongoDB; use MongoDB\BSON\UTCDateTime as MongoUTCDateTime; use Recruiter\Recruiter; -use Recruiter\Worker; class Repository { private $roster; - private $recruiter; - public function __construct(MongoDB\Database $db, Recruiter $recruiter) + public function __construct(MongoDB\Database $db, private readonly Recruiter $recruiter) { $this->roster = $db->selectCollection('roster'); - $this->recruiter = $recruiter; } public function save($worker) @@ -24,7 +21,7 @@ public function save($worker) $result = $this->roster->replaceOne( ['_id' => $document['_id']], $document, - ['upsert' => true] + ['upsert' => true], ); } @@ -32,14 +29,14 @@ public function atomicUpdate($worker, array $changeSet) { $this->roster->updateOne( ['_id' => $worker->id()], - ['$set' => $changeSet] + ['$set' => $changeSet], ); } public function refresh($worker) { $worker->updateWith( - $this->roster->findOne(['_id' => $worker->id()]) + $this->roster->findOne(['_id' => $worker->id()]), ); } @@ -47,9 +44,9 @@ public function deadWorkers($consideredDeadAt) { return $this->roster->find( ['last_seen_at' => [ - '$lt' => new MongoUTCDateTime($consideredDeadAt->format('U') * 1000)] + '$lt' => new MongoUTCDateTime($consideredDeadAt->format('U') * 1000)], ], - ['projection' => ['_id' => true, 'assigned_to' => true]] + ['projection' => ['_id' => true, 'assigned_to' => true]], ); } diff --git a/src/Recruiter/WorkerDiedInTheLineOfDutyException.php b/src/Recruiter/WorkerDiedInTheLineOfDutyException.php index 7a3d83e0..6543f13b 100644 --- a/src/Recruiter/WorkerDiedInTheLineOfDutyException.php +++ b/src/Recruiter/WorkerDiedInTheLineOfDutyException.php @@ -1,8 +1,7 @@ $value) { - if (!call_user_func($predicate, $value, $key, $array)) { - return false; - } - } - return true; -} + $f = $f ?: (fn ($value) => $value); -function array_some($array, callable $predicate) -{ - foreach ($array as $key => $value) { - if (call_user_func($predicate, $value, $key, $array)) { - return true; - } - } - return false; -} - -function array_group_by($array, callable $f = null) -{ - $f = $f ?: function ($value) { - return $value; - }; return array_reduce( $array, function ($buckets, $x) use ($f) { @@ -34,8 +14,9 @@ function ($buckets, $x) use ($f) { $buckets[$key] = []; } $buckets[$key][] = $x; + return $buckets; }, - [] + [], ); } diff --git a/src/Sink/BlackHole.php b/src/Sink/BlackHole.php index 154fe15a..e4571ee6 100644 --- a/src/Sink/BlackHole.php +++ b/src/Sink/BlackHole.php @@ -2,10 +2,10 @@ namespace Sink; -use Iterator; use ArrayAccess; +use Iterator; -class BlackHole implements Iterator, ArrayAccess +class BlackHole implements \Iterator, \ArrayAccess, \Stringable { public function __construct() { @@ -15,90 +15,90 @@ public function __destruct() { } - public function __set($name, $value) + public function __set(string $name, mixed $value) { } - public function __get($name) + public function __get(string $name): self { return $this; } - public function __isset($name) + public function __isset(string $name): bool { return false; } - public function __unset($name) + public function __unset(string $name): void { } - public function __call($name, $arguments) + public function __call(string $name, array $arguments) { return $this; } - public function __toString() + public function __toString(): string { return ''; } - public function __invoke() + public function __invoke(): self { return $this; } - public function __clone() + public function __clone(): void { } - public static function __callStatic($name, $args) + public static function __callStatic(string $name, array $args): self { return new self(); } // Iterator Interface - public function current() + public function current(): self { return $this; } - public function key() + public function key(): self { return $this; } - public function next() + public function next(): void { } - public function rewind() + public function rewind(): void { } - public function valid() + public function valid(): bool { return false; } // ArrayAccess Interface - public function offsetExists($offset) + public function offsetExists(mixed $offset): bool { return false; } - public function offsetGet($offset) + public function offsetGet(mixed $offset): self { return $this; } - public function offsetSet($offset, $value) + public function offsetSet(mixed $offset, mixed $value): void { } - public function offsetUnset($offset) + public function offsetUnset(mixed $offset): void { } } diff --git a/src/Timeless/Clock.php b/src/Timeless/Clock.php index eb8cc7c0..0c90fa27 100644 --- a/src/Timeless/Clock.php +++ b/src/Timeless/Clock.php @@ -2,15 +2,24 @@ namespace Timeless; -class Clock +class Clock implements ClockInterface { - public function stop() + public function start(): self { - return clock(new StoppedClock($this->now())); + clock($this); + + return $this; + } + + public function stop(): StoppedClock + { + clock($clock = new StoppedClock($this->now())); + + return $clock; } - public function now() + public function now(): Moment { - return new Moment(round(microtime(true) * 1000)); + return new Moment((int) round(microtime(true) * 1000)); } } diff --git a/src/Timeless/ClockInterface.php b/src/Timeless/ClockInterface.php new file mode 100644 index 00000000..65faa620 --- /dev/null +++ b/src/Timeless/ClockInterface.php @@ -0,0 +1,12 @@ +ms = intval($ms); } public function us(): int @@ -101,7 +96,7 @@ public function from(Moment $reference): Moment return $reference->after($this); } - public function multiplyBy($multiplier): self + public function multiplyBy(int $multiplier): self { return new self($this->ms * $multiplier); } @@ -111,48 +106,46 @@ public function add(Interval $interval): self return new self($this->ms + $interval->ms); } - public function format($format) - { - if (is_string($format)) { - $availableFormatsTable = [ - 'ms' => ['milliseconds', 'ms', 'ms'], - 's' => ['seconds', 's', 's'], - 'm' => ['minutes', 'm', 'm'], - 'h' => ['hours', 'h', 'h'], - 'd' => ['days', 'd', 'd'], - 'w' => ['weeks', 'w', 'w'], - 'mo' => ['months', 'mo', 'mo'], - 'y' => ['years', 'y', 'y'], - 'milliseconds' => ['milliseconds', ' milliseconds', ' millisecond'], - 'seconds' => ['seconds', ' seconds', ' second'], - 'minutes' => ['minutes', ' minutes', ' minute'], - 'hours' => ['hours', ' hours', ' hour'], - 'days' => ['days', ' days', ' day'], - 'weeks' => ['weeks', ' weeks', ' week'], - 'months' => ['months', ' months', ' month'], - 'years' => ['years', ' years', ' year'], - ]; - $format = trim($format); - if (array_key_exists($format, $availableFormatsTable)) { - $callable = [$this, $availableFormatsTable[$format][0]]; - if (!is_callable($callable)) { - throw new \RuntimeException("function `{$availableFormatsTable[$format][0]}` does not exists"); - } - - $amountOfTime = call_user_func($callable); - $unitOfTime = $amountOfTime === 1 ? - $availableFormatsTable[$format][2] : - $availableFormatsTable[$format][1]; - return sprintf('%d%s', $amountOfTime, $unitOfTime); + public function format(string $format): string + { + $availableFormatsTable = [ + 'ms' => ['milliseconds', 'ms', 'ms'], + 's' => ['seconds', 's', 's'], + 'm' => ['minutes', 'm', 'm'], + 'h' => ['hours', 'h', 'h'], + 'd' => ['days', 'd', 'd'], + 'w' => ['weeks', 'w', 'w'], + 'mo' => ['months', 'mo', 'mo'], + 'y' => ['years', 'y', 'y'], + 'milliseconds' => ['milliseconds', ' milliseconds', ' millisecond'], + 'seconds' => ['seconds', ' seconds', ' second'], + 'minutes' => ['minutes', ' minutes', ' minute'], + 'hours' => ['hours', ' hours', ' hour'], + 'days' => ['days', ' days', ' day'], + 'weeks' => ['weeks', ' weeks', ' week'], + 'months' => ['months', ' months', ' month'], + 'years' => ['years', ' years', ' year'], + ]; + $format = trim($format); + if (array_key_exists($format, $availableFormatsTable)) { + $callable = [$this, $availableFormatsTable[$format][0]]; + if (!is_callable($callable)) { + throw new \RuntimeException("function `{$availableFormatsTable[$format][0]}` does not exists"); } - throw new InvalidIntervalFormat("'{$format}' is not a valid Interval format"); + + $amountOfTime = call_user_func($callable); + $unitOfTime = 1 === $amountOfTime ? + $availableFormatsTable[$format][2] : + $availableFormatsTable[$format][1]; + + return sprintf('%d%s', $amountOfTime, $unitOfTime); } - throw new InvalidIntervalFormat('You need to use strings'); + throw new InvalidIntervalFormat("'{$format}' is not a valid Interval format"); } public function toDateInterval() { - return new DateInterval("PT{$this->seconds()}S"); + return new \DateInterval("PT{$this->seconds()}S"); } public static function parse($string) @@ -169,7 +162,7 @@ public static function parse($string) 'years' => 'years', 'year' => 'years', 'y' => 'years', ]; $units = implode('|', array_keys($tokenToFunction)); - if (preg_match("/^[^\d]*(?P\d+)\s*(?P{$units})(?:\W.*|$)/", $string, $matches)) { + if (preg_match("/^[^\\d]*(?P\\d+)\\s*(?P{$units})(?:\\W.*|$)/", $string, $matches)) { $callable = 'Timeless\\' . $tokenToFunction[$matches['unit']]; if (is_callable($callable)) { return call_user_func($callable, $matches['quantity']); @@ -188,10 +181,11 @@ public static function parse($string) throw new InvalidIntervalFormat('You need to use strings'); } - public static function fromDateInterval(DateInterval $interval) + public static function fromDateInterval(\DateInterval $interval): self { - $startTime = new DateTimeImmutable(); + $startTime = new \DateTimeImmutable(); $endTime = $startTime->add($interval); + return new self(($endTime->getTimestamp() - $startTime->getTimestamp()) * 1000); } } diff --git a/src/Timeless/InvalidIntervalFormat.php b/src/Timeless/InvalidIntervalFormat.php index 55ca83b4..01175de2 100644 --- a/src/Timeless/InvalidIntervalFormat.php +++ b/src/Timeless/InvalidIntervalFormat.php @@ -2,8 +2,6 @@ namespace Timeless; -use Exception; - -class InvalidIntervalFormat extends Exception +class InvalidIntervalFormat extends \Exception { } diff --git a/src/Timeless/Moment.php b/src/Timeless/Moment.php index 94eeeabe..4e394126 100644 --- a/src/Timeless/Moment.php +++ b/src/Timeless/Moment.php @@ -2,80 +2,74 @@ namespace Timeless; -use DateTime; -use DateTimeZone; - -class Moment +readonly class Moment { - private $ms; - - public static function fromTimestamp($ts) + public static function fromTimestamp(int $ts): self { return new self($ts * 1000); } - public static function fromDateTime(DateTime $dateTime) + public static function fromDateTime(\DateTime $dateTime): self { - return static::fromTimestamp($dateTime->getTimestamp()); + return self::fromTimestamp($dateTime->getTimestamp()); } - public function __construct($ms) + public function __construct(private int $ms) { - $this->ms = $ms; } - public function milliseconds() + public function milliseconds(): int { return $this->ms; } - public function ms() + public function ms(): int { return $this->ms; } - public function seconds() + public function seconds(): int { return $this->s(); } - public function s() + public function s(): int { - return round($this->ms / 1000.0); + return (int) round($this->ms / 1000.0); } - public function after(Interval $d) + public function after(Interval $d): self { return new self($this->ms + $d->ms()); } - public function before(Interval $d) + public function before(Interval $d): self { return new self($this->ms - $d->ms()); } - public function isAfter(Moment $m) + public function isAfter(Moment $m): bool { return $this->ms >= $m->ms(); } - public function isBefore(Moment $m) + public function isBefore(Moment $m): bool { return $this->ms <= $m->ms(); } - public function toSecondPrecision() + public function toSecondPrecision(): Moment { return new self($this->s() * 1000); } - public function format() + public function format(): string { - return (new DateTime('@' . $this->s(), new DateTimeZone('UTC')))->format(DateTime::RFC3339); + return new \DateTime('@' . $this->s(), new \DateTimeZone('UTC'))->format(\DateTime::RFC3339); } - public function toDateTime() + public function toDateTime(): \DateTime { - return new DateTime('@' . $this->s(), new DateTimeZone('UTC')); + return new \DateTime('@' . $this->s(), new \DateTimeZone('UTC')); } } diff --git a/src/Timeless/MongoDate.php b/src/Timeless/MongoDate.php index 255ccdc2..d61714de 100644 --- a/src/Timeless/MongoDate.php +++ b/src/Timeless/MongoDate.php @@ -1,4 +1,5 @@ now = $now; } - public function now() + public function now(): Moment { return $this->now; } - public function driftForwardBySeconds($seconds) + public function driftForwardBySeconds(int $seconds): void { $this->now = $this->now->after(seconds($seconds)); } - public function start() + public function start(): Clock + { + clock($clock = new Clock()); + + return $clock; + } + + public function stop(): self { - return clock(new Clock()); + clock($this); + + return $this; } } diff --git a/src/Timeless/functions.php b/src/Timeless/functions.php index 26b0a0ab..74e2ec13 100644 --- a/src/Timeless/functions.php +++ b/src/Timeless/functions.php @@ -2,102 +2,104 @@ namespace Timeless; -function clock($clock = null) +function clock(?ClockInterface $clock = null): ClockInterface { global $__2852bec4cda046fca0e5e21dc007935c; + /** @var ClockInterface $__2852bec4cda046fca0e5e21dc007935c */ $__2852bec4cda046fca0e5e21dc007935c = $clock ?: ( $__2852bec4cda046fca0e5e21dc007935c ?: new Clock() ); + return $__2852bec4cda046fca0e5e21dc007935c; } -function now() +function now(): Moment { return clock()->now(); } -function millisecond($numberOf) +function millisecond(int $numberOf): Interval { return milliseconds($numberOf); } -function milliseconds($numberOf) +function milliseconds(int $numberOf): Interval { return new Interval($numberOf); } -function second($numberOf) +function second(int $numberOf): Interval { return seconds($numberOf); } -function seconds($numberOf) +function seconds(int $numberOf): Interval { return new Interval($numberOf * Interval::MILLISECONDS_IN_SECONDS); } -function minute($numberOf) +function minute(int $numberOf): Interval { return minutes($numberOf); } -function minutes($numberOf) +function minutes(int $numberOf): Interval { return new Interval($numberOf * Interval::MILLISECONDS_IN_MINUTES); } -function hour($numberOf) +function hour(int $numberOf): Interval { return hours($numberOf); } -function hours($numberOf) +function hours(int $numberOf): Interval { return new Interval($numberOf * Interval::MILLISECONDS_IN_HOURS); } -function day($numberOf) +function day(int $numberOf): Interval { return days($numberOf); } -function days($numberOf) +function days(int $numberOf): Interval { return new Interval($numberOf * Interval::MILLISECONDS_IN_DAYS); } -function week($numberOf) +function week(int $numberOf): Interval { return weeks($numberOf); } -function weeks($numberOf) +function weeks(int $numberOf): Interval { return new Interval($numberOf * Interval::MILLISECONDS_IN_WEEKS); } -function month($numberOf) +function month(int $numberOf): Interval { return months($numberOf); } -function months($numberOf) +function months(int $numberOf): Interval { return new Interval($numberOf * Interval::MILLISECONDS_IN_MONTHS); } -function year($numberOf) +function year(int $numberOf): Interval { return years($numberOf); } -function years($numberOf) +function years(int $numberOf): Interval { return new Interval($numberOf * Interval::MILLISECONDS_IN_YEARS); } -function fromDateInterval(\DateInterval $interval) +function fromDateInterval(\DateInterval $interval): Interval { $seconds = (string) $interval->s; if ($interval->i) {