diff --git a/.editorconfig b/.editorconfig index 0dd272d..7fbab54 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true -[*.{yml, yaml, sh, conf, neon*}] +[*.{yml,yaml,sh,conf,neon*}] indent_size = 2 [Makefile] diff --git a/.gitattributes b/.gitattributes index b35bb1f..6cf3d8c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,10 @@ * text=auto -/.github export-ignore +/.* export-ignore /tests export-ignore /[Dd]ocker* export-ignore -/.* export-ignore -/phpunit.xml* export-ignore +/*.xml export-ignore +/*.xml.dist export-ignore /phpstan.* export-ignore /Makefile export-ignore +/rector.php export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 99aea44..0a1c52b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,6 @@ name: Bug report about: Create a report to help us improve labels: type:bug -assignees: tarampampam --- ## Describe the bug diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 1f76be3..b534773 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,6 @@ name: Feature request about: Suggest an idea for this package labels: type:enhancement -assignees: tarampampam --- ### Is your feature request related to a problem? diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 14ff0c6..da4617f 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -2,7 +2,6 @@ name: Question about: Ask anything labels: type:question -assignees: tarampampam --- > If you have any questions feel free to ask diff --git a/.github/workflows/cs-fix.yml b/.github/workflows/cs-fix.yml new file mode 100644 index 0000000..0395b27 --- /dev/null +++ b/.github/workflows/cs-fix.yml @@ -0,0 +1,12 @@ +on: + push: + branches: + - '*' + +name: Fix Code Style + +jobs: + cs-fix: + permissions: + contents: write + uses: spiral/gh-actions/.github/workflows/cs-fix.yml@master diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 9fd31dc..4d3b491 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -10,4 +10,5 @@ ->include(__DIR__ . '/config') ->include(__DIR__ . '/tests') ->include(__DIR__ . '/rector.php') + ->allowRisky(false) ->build(); diff --git a/CHANGELOG.md b/CHANGELOG.md index 59007c3..167c72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver]. +### Added + +- gRPC client support +- Added support to set queue options globally or per job [#158] + +## Unreleased + +### Fixed + +- Check pipeline stats on message push [#147] +- Edit the $ttl calculation for the RoadRunnerStore, the time calculation takes place inside the spiral/roadrunner-kv package +- Tasks were stuck in case of an error, the "release" method did not return them to the queue. +- The "calculateBackoff" method incorrectly took the index "$job->attempts()" +- The "withHeader" method of the "\Spiral\RoadRunner\Jobs\Task\WritableHeadersInterface" interface expects the type "string|iterable", "int" is passed + +[#147]:https://github.com/roadrunner-php/laravel-bridge/issues/147 + ## v5.12.0 ### Added +- Method `setPrefix` on the `\Spiral\RoadRunnerLaravel\Cache\RoadRunnerStore` class - Support for `v3.x` of `spiral/roadrunner-http` package [#125] [#125]:https://github.com/roadrunner-php/laravel-bridge/issues/125 diff --git a/Makefile b/Makefile index d10e286..d38f564 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ test: ## Execute php tests and linters docker-compose run $(RUN_APP_ARGS) app composer test test-cover: ## Execute php tests with coverage - docker-compose run --rm --user "0:0" -e 'XDEBUG_MODE=coverage' app sh -c 'docker-php-ext-enable xdebug && su $(shell whoami) -s /bin/sh -c "composer phpunit-cover"' + docker-compose run --rm --user "0:0" -e 'XDEBUG_MODE=coverage' app sh -c 'docker-php-ext-enable xdebug && su $(shell whoami) -s /bin/sh -c "composer phpunit:cover"' shell: ## Start shell into container with php docker-compose run $(RUN_APP_ARGS) app sh diff --git a/README.md b/README.md index 6a00836..abbb585 100644 --- a/README.md +++ b/README.md @@ -12,49 +12,57 @@ Easy way for connecting [RoadRunner][roadrunner] and [Laravel][laravel] applicat ## Why Use This Package? -Laravel provides the [Octane](https://laravel.com/docs/12.x/octane) package which partially supports RoadRunner as an -application server, but RoadRunner offers much more than just HTTP capabilities. It also includes Jobs, Temporal, gRPC, -and other plugins. +This package provides complete Laravel integration with RoadRunner, offering: + +- Support for HTTP and other RoadRunner plugins like gRPC, Queue, KeyValue, and more. +- [Temporal](https://temporal.io/) integration +- Full RoadRunner configuration control ![RoadRunner](https://github.com/user-attachments/assets/609d2e29-b6af-478b-b350-1d27b77ed6fb) -> **Note:** There is an article that explains all the RoadRunner -> plugins: https://butschster.medium.com/roadrunner-an-underrated-powerhouse-for-php-applications-46410b0abc +> [!TIP] +> [There is an article][rr-plugins-article] that explains all the RoadRunner plugins. -The main limitation of Octane is that it has a built-in worker only for the HTTP plugin and doesn't provide the ability -to create additional workers for other RoadRunner plugins, restricting its use to just the HTTP plugin. +## Table of Contents -Our **Laravel Bridge** solves this problem by taking a different approach: +- [Get Started](#get-started) + - [Installation](#installation) + - [Configuration](#configuration) + - [Starting the Server](#starting-the-server) +- [How It Works](#how-it-works) +- [Supported Plugins](#supported-plugins) + - [HTTP Plugin](#http-plugin) + - [Jobs (Queue) Plugin](#jobs-queue-plugin) + - [gRPC Plugin](#grpc-plugin) + - [gRPC Client](#grpc-client) + - [Temporal](#temporal) +- [Custom Workers](#custom-workers) +- [Support](#support) +- [License](#license) -1. We include `laravel/octane` in our package and reuse its **SDK** for clearing the state of Laravel applications -2. We add support for running and configuring multiple workers for different RoadRunner plugins -3. By reusing Octane's functionality for state clearing, we automatically support all third-party packages that are - compatible with Octane +## Get Started -**This way, you get the best of both worlds:** Octane's state management and RoadRunner's full plugin ecosystem. +### Installation -## Installation +First, install the Laravel Bridge package via Composer: -```shell script +```shell composer require roadrunner-php/laravel-bridge ``` -After that you can "publish" package configuration file (`./config/roadrunner.php`) using next command: +Publish the configuration file: -```shell script +```shell php artisan vendor:publish --provider='Spiral\RoadRunnerLaravel\ServiceProvider' --tag=config ``` -## Usage - -After package installation, you can download and install [RoadRunner][roadrunner] binary -using Composer script: +Download and install RoadRunner binary using DLoad: -```bash -composer get:rr +```shell +./vendor/bin/dload get rr ``` -### Basic Configuration (.rr.yaml) +### Configuration Create a `.rr.yaml` configuration file in your project root: @@ -65,7 +73,6 @@ rpc: server: command: 'php vendor/bin/rr-worker start' - relay: pipes http: address: 0.0.0.0:8080 @@ -82,49 +89,30 @@ http: forbid: [ ".php" ] ``` -## RoadRunner Worker Configuration - -You can configure workers in `config/roadrunner.php` file in the `workers` section: - -```php -use Spiral\RoadRunner\Environment\Mode; -use Spiral\RoadRunnerLaravel\Grpc\GrpcWorker; -use Spiral\RoadRunnerLaravel\Http\HttpWorker; -use Spiral\RoadRunnerLaravel\Queue\QueueWorker; -use Spiral\RoadRunnerLaravel\Temporal\TemporalWorker; +### Starting the Server -return [ - // ... other configuration options ... +Start the RoadRunner server with: - 'workers' => [ - Mode::MODE_HTTP => HttpWorker::class, - Mode::MODE_JOBS => QueueWorker::class, - Mode::MODE_GRPC => GrpcWorker::class, - Mode::MODE_TEMPORAL => TemporalWorker::class, - ], -]; +```shell +./rr serve ``` -As you can see, there are several predefined workers for HTTP, Jobs, gRPC, and Temporal. Feel free to replace any of -them with your implementation if needed. Or create your own worker, for example, -for [Centrifugo](https://docs.roadrunner.dev/docs/plugins/centrifuge), [TCP](https://docs.roadrunner.dev/docs/plugins/tcp) -or any other plugin. - ## How It Works -In the server section of the RoadRunner config, we specify the command to start our worker: +RoadRunner creates a worker pool by executing the command specified in the server configuration: ```yaml server: command: 'php vendor/bin/rr-worker start' - relay: pipes ``` -When RoadRunner server creates a worker pool for a specific plugin, it exposes an environment variable `RR_MODE` that -indicates which plugin is being used. Our worker checks this variable to determine which Worker class should handle the -request based on the configuration in `roadrunner.php`. +When RoadRunner creates a worker pool for a specific plugin, +it sets the `RR_MODE` environment variable to indicate which plugin is being used. +The Laravel Bridge checks this variable to determine +which Worker class should handle the request based on your configuration. -The selected worker starts listening for requests from the RoadRunner server and handles them using the Octane worker, +The selected worker then listens for requests from the RoadRunner server +and handles them using the [Octane][octane] worker, which clears the application state after each task (request, command, etc.). ## Supported Plugins @@ -148,16 +136,17 @@ http: forbid: [ ".php" ] ``` -> **Note:** Read more about the HTTP plugin in -> the [RoadRunner documentation][https://docs.roadrunner.dev/docs/http/http]. +> [!TIP] +> Read more about the HTTP plugin in the [RoadRunner documentation][roadrunner-docs-http]. ### Jobs (Queue) Plugin -The Queue plugin allows you to use RoadRunner as a queue driver for Laravel. +The Queue plugin allows you to use RoadRunner as a queue driver for Laravel +without additional services like Redis or a database. #### Configuration -First, add the Queue Service Provider in your `config/app.php`: +First, add the Queue Service Provider in `config/app.php`: ```php 'providers' => [ @@ -166,7 +155,7 @@ First, add the Queue Service Provider in your `config/app.php`: ], ``` -Then, configure a new connection in your `config/queue.php`: +Then, configure a new connection in `config/queue.php`: ```php 'connections' => [ @@ -192,10 +181,7 @@ jobs: config: { } ``` -> **Note:** Read more about the Jobs plugin in -> the [RoadRunner documentation][https://docs.roadrunner.dev/docs/queues-and-jobs/overview-queues]. - -Don't forget to set the `QUEUE_CONNECTION` environment variable in your `.env` file: +Set the `QUEUE_CONNECTION` environment variable in your `.env` file: ```dotenv QUEUE_CONNECTION=roadrunner @@ -203,6 +189,9 @@ QUEUE_CONNECTION=roadrunner That's it! You can now dispatch jobs to the RoadRunner queue without any additional services like Redis or Database. +> [!TIP] +> Read more about the Jobs plugin in the [RoadRunner documentation][roadrunner-docs-jobs]. + ### gRPC Plugin The gRPC plugin enables serving gRPC services with your Laravel application. @@ -231,6 +220,66 @@ return [ ]; ``` +#### gRPC Client Usage + +The package also allows your Laravel application to act as a gRPC client, making requests to external gRPC services. + +##### Client Configuration + +Add your gRPC client configuration to `config/roadrunner.php`: + +```php +return [ + // ... other configuration + 'grpc' => [ + // ... server config + 'clients' => [ + 'services' => [ + [ + 'connection' => '127.0.0.1:9001', // gRPC server address + 'interfaces' => [ + \App\Grpc\EchoServiceInterface::class, + ], + // 'tls' => [ ... ] // Optional TLS configuration + ], + ], + // 'interceptors' => [ ... ] // Optional interceptors + ], + ], +]; +``` + +##### Using the gRPC Client in Laravel + +You can inject `Spiral\Grpc\Client\ServiceClientProvider` into your services or controllers to obtain a gRPC client instance: + +```php +use Spiral\Grpc\Client\ServiceClientProvider; +use App\Grpc\EchoServiceInterface; +use App\Grpc\EchoRequest; + +class GrpcController extends Controller +{ + public function callService(ServiceClientProvider $provider) + { + /** @var EchoServiceInterface $client */ + $client = $provider->get(EchoServiceInterface::class); + + $request = new EchoRequest(); + $request->setMessage('Hello from client!'); + + $response = $client->Echo($request); + + return $response->getMessage(); + } +} +``` + +> **Note:** +> - Make sure you have generated the PHP classes from your `.proto` files (using `protoc`). +> - The `connection` and `interfaces` must match the service you want to call. +> - You can configure multiple gRPC client services as needed. + ### Temporal Temporal is a workflow engine that enables orchestration of microservices and provides sophisticated workflow @@ -263,36 +312,54 @@ return [ ]; ``` -Download Temporal binary for development purposes using the following command: +Download Temporal binary for development: ```bash -composer get:temporal +./vendor/bin/dload get temporal ``` -To start the Temporal server, you can use the following command: +Start the Temporal dev server: ```bash ./temporal server start-dev --log-level error --color always ``` -#### Useful links +#### Useful Links - [PHP SDK on GitHub](https://github.com/temporalio/sdk-php) - [PHP SDK docs](https://docs.temporal.io/develop/php/) - [Code samples](https://github.com/temporalio/samples-php) - [Taxi service sample](https://github.com/butschster/podlodka-taxi-service) -## Starting RoadRunner Server +## Custom Workers -To start the RoadRunner server: +The RoadRunner Laravel Bridge comes with several predefined workers for common plugins, +but you can easily create your own custom workers for any RoadRunner plugin. +This section explains how to create and register custom workers in your application. -```shell script -./rr serve +### Understanding Workers + +Workers are responsible for handling requests from the RoadRunner server +and processing them in your Laravel application. +The predefined workers are configured in the `config/roadrunner.php` file: + +```php +return [ + // ... other configuration options ... + + 'workers' => [ + Mode::MODE_HTTP => HttpWorker::class, + Mode::MODE_JOBS => QueueWorker::class, + Mode::MODE_GRPC => GrpcWorker::class, + Mode::MODE_TEMPORAL => TemporalWorker::class, + ], +]; ``` -## Custom Workers +### Creating Custom Workers -You can create your own custom workers by implementing the `Spiral\RoadRunnerLaravel\WorkerInterface`: +To create a custom worker, you need to implement the `Spiral\RoadRunnerLaravel\WorkerInterface`. +This interface has a single method, `start()`, which is called when the worker is started by the RoadRunner server: ```php namespace App\Workers; @@ -304,30 +371,46 @@ class CustomWorker implements WorkerInterface { public function start(WorkerOptionsInterface $options): void { - // Your custom worker implementation + // Your worker implementation goes here + // This method should handle requests from the RoadRunner server } } ``` -Then register it in the `config/roadrunner.php`: +### Registering Custom Workers + +After creating your custom worker, you need to register it in the `config/roadrunner.php` file: ```php return [ + // ... other configuration options ... + 'workers' => [ - 'custom' => \App\Workers\CustomWorker::class, + // Existing workers + Mode::MODE_HTTP => HttpWorker::class, + Mode::MODE_JOBS => QueueWorker::class, + + // Your custom worker for a custom or built-in plugin + 'custom_plugin' => \App\Workers\CustomWorker::class, ], ]; ``` +The key in the `workers` array should match the value of the `RR_MODE` environment variable +set by the RoadRunner server for your plugin. + ## Support -If you find this package helpful, please consider giving it a star on GitHub. Your support helps make the project more visible to other developers who might benefit from it! +If you find this package helpful, please consider giving it a star on GitHub. +Your support helps make the project more visible to other developers who might benefit from it! [![Issues][badge_issues]][link_issues] [![Issues][badge_pulls]][link_pulls] If you find any package errors, please, [make an issue][link_create_issue] in a current repository. +You can also [sponsor this project][link_sponsor] to help ensure its continued development and maintenance. + ## License MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. @@ -374,10 +457,14 @@ MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. [link_pulls]:https://github.com/roadrunner-php/laravel-bridge/pulls +[link_sponsor]:https://github.com/sponsors/roadrunner-server + [link_license]:https://github.com/roadrunner-php/laravel-bridge/blob/master/LICENSE [getcomposer]:https://getcomposer.org/download/ +[dload]:https://github.com/php-internal/dload + [roadrunner]:https://github.com/roadrunner-server/roadrunner [roadrunner_config]:https://github.com/roadrunner-server/roadrunner/blob/master/.rr.yaml @@ -387,3 +474,11 @@ MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. [laravel_events]:https://laravel.com/docs/events [roadrunner-binary-releases]:https://github.com/roadrunner-server/roadrunner/releases + +[roadrunner-docs-jobs]:https://docs.roadrunner.dev/docs/queues-and-jobs/overview-queues + +[roadrunner-docs-http]:https://docs.roadrunner.dev/docs/http/http + +[octane]:https://laravel.com/docs/12.x/octane + +[rr-plugins-article]:https://butschster.medium.com/roadrunner-an-underrated-powerhouse-for-php-applications-46410b0abc diff --git a/composer.json b/composer.json index 61e412b..35a0f59 100644 --- a/composer.json +++ b/composer.json @@ -1,103 +1,104 @@ { - "name": "roadrunner-php/laravel-bridge", - "type": "library", - "description": "Laravel integration for RoadRunner with support for HTTP, Jobs, gRPC, and Temporal plugins - going beyond Octane's capabilities", - "keywords": [ - "laravel", - "bridge", - "roadrunner", - "temporal", - "grpc", - "queue", - "cache", - "http" - ], - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/roadrunner-server" - } - ], - "license": "MIT", - "authors": [ - { - "name": "butschster", - "homepage": "https://github.com/butschster" + "name": "roadrunner-php/laravel-bridge", + "type": "library", + "description": "Laravel integration for RoadRunner with support for HTTP, Jobs, gRPC, and Temporal plugins - going beyond Octane's capabilities", + "keywords": [ + "laravel", + "bridge", + "roadrunner", + "temporal", + "grpc", + "queue", + "cache", + "http" + ], + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/roadrunner-server" + } + ], + "license": "MIT", + "authors": [ + { + "name": "butschster", + "homepage": "https://github.com/butschster" + }, + { + "name": "roxblnfk", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "tarampampam", + "homepage": "https://github.com/tarampampam" + } + ], + "require": { + "php": "^8.2", + "spiral/roadrunner-kv": "^4.0", + "spiral/roadrunner-jobs": "^4.0", + "spiral/roadrunner-grpc": "^3.5", + "laravel/octane": "^2.9", + "spiral/roadrunner-http": "^3.0", + "spiral/roadrunner-worker": "^3.0", + "temporal/sdk": "^2.0", + "internal/dload": "^1.1", + "spiral/grpc-client": "^1.0.0-rc1" }, - { - "name": "roxblnfk", - "homepage": "https://github.com/roxblnfk" + "require-dev": { + "laravel/framework": "^12.0", + "spiral/code-style": "^2.2.2", + "rector/rector": "^2.0", + "guzzlehttp/guzzle": "^7.0", + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0" }, - { - "name": "tarampampam", - "homepage": "https://github.com/tarampampam" - } - ], - "require": { - "php": "^8.2", - "spiral/roadrunner-kv": "^4.0", - "spiral/roadrunner-jobs": "^4.0", - "spiral/roadrunner-grpc": "^3.5", - "laravel/octane": "^2.9", - "spiral/roadrunner-http": "^3.0", - "spiral/roadrunner-worker": "^3.0", - "temporal/sdk": "^2.0", - "internal/dload": "^1.1" - }, - "require-dev": { - "laravel/framework": "^12.0", - "spiral/code-style": "^2.2.2", - "rector/rector": "^2.0", - "guzzlehttp/guzzle": "^7.0", - "mockery/mockery": "^1.6", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0" - }, - "autoload": { - "psr-4": { - "Spiral\\RoadRunnerLaravel\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Spiral\\RoadRunnerLaravel\\Tests\\": "tests/" - } - }, - "bin": [ - "bin/rr-worker" - ], - "scripts": { - "get:rr": "dload get rr", - "get:temporal": "dload get temporal", - "get:protoc": "dload get protoc protoc-gen-php-grpc", - "cs-check": "vendor/bin/php-cs-fixer fix --dry-run", - "cs-fix": "vendor/bin/php-cs-fixer fix", - "refactor": "rector process --config=rector.php", - "refactor:ci": "rector process --config=rector.php --dry-run --ansi", - "phpunit": "@php ./vendor/bin/phpunit --no-coverage", - "phpunit-cover": "@php ./vendor/bin/phpunit", - "phpstan": "@php ./vendor/bin/phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi", - "test": [ - "@phpstan", - "@phpunit" + "autoload": { + "psr-4": { + "Spiral\\RoadRunnerLaravel\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Spiral\\RoadRunnerLaravel\\Tests\\": "tests/" + } + }, + "bin": [ + "bin/rr-worker" ], - "test-cover": [ - "@phpstan", - "@phpunit-cover" - ] - }, - "extra": { - "laravel": { - "providers": [ - "Spiral\\RoadRunnerLaravel\\ServiceProvider", - "Spiral\\RoadRunnerLaravel\\Queue\\QueueServiceProvider", - "Spiral\\RoadRunnerLaravel\\Cache\\CacheServiceProvider", - "Spiral\\RoadRunnerLaravel\\Temporal\\TemporalServiceProvider" - ] + "scripts": { + "get:rr": "dload get rr", + "get:temporal": "dload get temporal", + "get:protoc": "dload get protoc protoc-gen-php-grpc", + "cs:check": "php-cs-fixer fix --dry-run", + "cs:fix": "php-cs-fixer fix", + "refactor": "rector process --config=rector.php", + "refactor:ci": "rector process --config=rector.php --dry-run --ansi", + "phpunit": "phpunit --no-coverage", + "phpunit-cover": "phpunit", + "phpstan": "phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi", + "test": "phpunit --color=always --testdox", + "test:unit": "phpunit --color=always --testsuite=Unit", + "test:feat": "phpunit --color=always --testsuite=Feature", + "test-cover": [ + "phpstan", + "@putenv XDEBUG_MODE=coverage", + "phpunit --coverage-clover=runtime/phpunit/logs/clover.xml --color=always" + ] + }, + "extra": { + "laravel": { + "providers": [ + "Spiral\\RoadRunnerLaravel\\ServiceProvider", + "Spiral\\RoadRunnerLaravel\\Queue\\QueueServiceProvider", + "Spiral\\RoadRunnerLaravel\\Cache\\CacheServiceProvider", + "Spiral\\RoadRunnerLaravel\\Temporal\\TemporalServiceProvider" + ] + } + }, + "support": { + "issues": "https://github.com/roadrunner-php/laravel-bridge/issues", + "source": "https://github.com/roadrunner-php/laravel-bridge" } - }, - "support": { - "issues": "https://github.com/roadrunner-php/laravel-bridge/issues", - "source": "https://github.com/roadrunner-php/laravel-bridge" - } } diff --git a/config/roadrunner.php b/config/roadrunner.php index 14bb231..77837ae 100644 --- a/config/roadrunner.php +++ b/config/roadrunner.php @@ -18,6 +18,31 @@ 'services' => [ // GreeterInterface::class => new Greeter::class, ], + 'clients' => [ + 'interceptors' => [ + // LoggingInterceptor::class, + ], + 'services' => [ + // [ + // 'connection' => 'my-grpc-server:9002', + // 'interfaces' => [ + // GreeterInterface::class, + // ], + // ], + // [ + // 'connection' => 'my-secure-grpc-server:9002', + // 'interfaces' => [ + // GreeterInterface::class, + // ], + // 'tls' => [ + // 'rootCerts' => '/path/to/ca.pem', + // 'privateKey' => '/path/to/client.key', + // 'certChain' => '/path/to/client.crt', + // 'serverName' => 'my.grpc.server', + // ], + // ], + ], + ], ], 'temporal' => [ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1680c26..5cb43d4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,8 +2,12 @@ - + cacheResultFile="runtime/phpunit/result.cache" + colors="true" + stderr="true" + displayDetailsOnTestsThatTriggerWarnings="true" + displayDetailsOnTestsThatTriggerDeprecations="true" +> ./tests/Unit @@ -12,25 +16,22 @@ ./tests/Feature - - + - ./src + src - ./vendor - ./tests + tests + + - - - - + + + - - - - - + + + diff --git a/src/Cache/RoadRunnerStore.php b/src/Cache/RoadRunnerStore.php index 9239e52..90af3fd 100644 --- a/src/Cache/RoadRunnerStore.php +++ b/src/Cache/RoadRunnerStore.php @@ -6,14 +6,16 @@ use Illuminate\Cache\TaggableStore; use Illuminate\Contracts\Cache\LockProvider; -use Illuminate\Support\InteractsWithTime; use Spiral\RoadRunner\KeyValue\StorageInterface; final class RoadRunnerStore extends TaggableStore implements LockProvider { - use InteractsWithTime; + private string $prefix; - public function __construct(private StorageInterface $storage, private string $prefix = '') {} + public function __construct(private StorageInterface $storage, string $prefix = '') + { + $this->setPrefix($prefix); + } public function get($key) { @@ -39,7 +41,7 @@ public function many(array $keys) public function put($key, $value, $seconds) { - return $this->storage->set($this->prefix . $key, $value, $this->calculateExpiration($seconds)); + return $this->storage->set($this->prefix . $key, $value, $seconds); } public function putMany(array $values, $seconds) @@ -52,7 +54,7 @@ public function putMany(array $values, $seconds) return $this->storage->setMultiple( $prefixedValues, - $this->calculateExpiration($seconds), + $seconds, ); } @@ -86,32 +88,13 @@ public function flush() return $this->storage->clear(); } - public function getPrefix() + public function getPrefix(): string { return $this->prefix; } - /** - * Set the cache key prefix. - */ public function setPrefix(string $prefix): void { - $this->prefix = !empty($prefix) ? $prefix . ':' : ''; - } - - /** - * Get the expiration time of the key. - */ - protected function calculateExpiration(int $seconds): int - { - return $this->toTimestamp($seconds); - } - - /** - * Get the UNIX timestamp for the given number of seconds. - */ - protected function toTimestamp(int $seconds): int - { - return $seconds > 0 ? $this->availableAt($seconds) : 0; + $this->prefix = $prefix; } } diff --git a/src/Queue/Contract/HasQueueOptions.php b/src/Queue/Contract/HasQueueOptions.php new file mode 100644 index 0000000..85f7c64 --- /dev/null +++ b/src/Queue/Contract/HasQueueOptions.php @@ -0,0 +1,10 @@ +resolveName( - ) . ' has been attempted too many times or run too long. The job may have previously timed out.', + $job->resolveName() . ' has been attempted too many times or run too long. The job may have previously timed out.', ); } @@ -267,9 +266,9 @@ protected function calculateBackoff(RoadRunnerJob $job, WorkerOptions $options): ',', \method_exists($job, 'backoff') && !\is_null($job->backoff()) ? $job->backoff() - : $options->backoff, + : (string) $options->backoff, ); - return (int) ($backoff[$job->attempts() - 1] ?? last($backoff)); + return (int) ($backoff[$job->attempts()] ?? last($backoff)); } } diff --git a/src/Queue/RoadRunnerConnector.php b/src/Queue/RoadRunnerConnector.php index 86cffbd..ef12a7d 100644 --- a/src/Queue/RoadRunnerConnector.php +++ b/src/Queue/RoadRunnerConnector.php @@ -26,6 +26,7 @@ public function connect(array $config): Queue new Jobs($rpc), $rpc, $config['queue'], + $config['options'] ?? [], ); } } diff --git a/src/Queue/RoadRunnerJob.php b/src/Queue/RoadRunnerJob.php index 24364d5..8a4d8ba 100644 --- a/src/Queue/RoadRunnerJob.php +++ b/src/Queue/RoadRunnerJob.php @@ -48,12 +48,24 @@ public function fire(): void $this->task->complete(); } + public function release($delay = 0): void + { + $attempts = $this->attempts(); + + $this->task + ->withDelay($delay) + ->withHeader('attempts', (string) ++$attempts) + ->requeue('release'); + + parent::release($delay); + } + protected function failed($e): void { $attempts = $this->attempts(); $this->task - ->withHeader('attempts', $attempts + 1) + ->withHeader('attempts', (string) ++$attempts) ->fail($e->getMessage()); parent::failed($e); diff --git a/src/Queue/RoadRunnerQueue.php b/src/Queue/RoadRunnerQueue.php index 27424b7..6f5f388 100644 --- a/src/Queue/RoadRunnerQueue.php +++ b/src/Queue/RoadRunnerQueue.php @@ -12,7 +12,12 @@ use RoadRunner\Jobs\DTO\V1\Stats; use Spiral\Goridge\RPC\RPCInterface; use Spiral\RoadRunner\Jobs\Jobs; +use Spiral\RoadRunner\Jobs\KafkaOptions; +use Spiral\RoadRunner\Jobs\Options; +use Spiral\RoadRunner\Jobs\OptionsInterface; +use Spiral\RoadRunner\Jobs\Queue\Driver; use Spiral\RoadRunner\Jobs\QueueInterface; +use Spiral\RoadRunnerLaravel\Queue\Contract\HasQueueOptions; final class RoadRunnerQueue extends Queue implements QueueContract { @@ -20,6 +25,7 @@ public function __construct( private readonly Jobs $jobs, private readonly RPCInterface $rpc, private readonly string $default = 'default', + private readonly array $defaultOptions = [], ) {} public function push($job, $data = '', $queue = null): string @@ -29,13 +35,13 @@ public function push($job, $data = '', $queue = null): string $this->createPayload($job, $queue, $data), $queue, null, - fn($payload, $queue) => $this->pushRaw($payload, $queue), + fn($payload, $queue) => $this->pushRaw($payload, $queue, $this->getJobOverrideOptions($job)), ); } public function pushRaw($payload, $queue = null, array $options = []): string { - $queue = $this->getQueue($queue); + $queue = $this->getQueue($queue, $options); $task = $queue->dispatch( $queue @@ -52,7 +58,7 @@ public function later($delay, $job, $data = '', $queue = null): string $this->createPayload($job, $queue, $data), $queue, $delay, - fn($payload, $queue) => $this->laterRaw($delay, $payload, $queue), + fn($payload, $queue) => $this->laterRaw($delay, $payload, $queue, $this->getJobOverrideOptions($job)), ); } @@ -81,29 +87,9 @@ protected function availableAt($delay = 0): int : $delay; } - /** - * Push a raw job onto the queue after a delay. - */ - private function laterRaw( - \DateTimeInterface|\DateInterval|int $delay, - array $payload, - ?string $queue = null, - ): string { - $queue = $this->getQueue($queue); - - $task = $queue->dispatch( - $queue - ->create($payload['displayName'] ?? Uuid::uuid4()->toString()) - ->withValue($payload) - ->withDelay($this->availableAt($delay)), - ); - - return $task->getId(); - } - - private function getQueue(?string $queue = null): QueueInterface + private function getQueue(?string $queue = null, array $options = []): QueueInterface { - $queue = $this->jobs->connect($queue ?? $this->default); + $queue = $this->jobs->connect($queue ?? $this->default, $this->getQueueOptions($options)); if (!$this->getStats($queue->getName())->getReady()) { $queue->resume(); @@ -112,6 +98,22 @@ private function getQueue(?string $queue = null): QueueInterface return $queue; } + private function getQueueOptions(array $overrides = []): OptionsInterface + { + $config = array_merge($this->defaultOptions, $overrides); + $options = new Options( + $config['delay'] ?? OptionsInterface::DEFAULT_DELAY, + $config['priority'] ?? OptionsInterface::DEFAULT_PRIORITY, + $config['auto_ack'] ?? OptionsInterface::DEFAULT_AUTO_ACK, + ); + + return match ($config['driver'] ?? null) { + Driver::Kafka => KafkaOptions::from($options) + ->withTopic($config['topic'] ?? ($this->defaultOptions['topic'] ?? '')), + default => $options, + }; + } + private function getStats(?string $queue = null): Stat { $queue ??= $this->default; @@ -120,11 +122,48 @@ private function getStats(?string $queue = null): Stat /** @var Stat $stat */ foreach ($stats as $stat) { - if ($stat->getQueue() === $queue) { + if ($stat->getPipeline() === $queue) { return $stat; } } return new Stat(); } + + private function getJobOverrideOptions(string|object $job): array + { + if (is_string($job) && class_exists($job)) { + $job = app($job); + } + + if ($job instanceof HasQueueOptions) { + $options = $job->queueOptions(); + if ($options instanceof Options) { + return $options->toArray(); + } + } + + return []; + } + + /** + * Push a raw job onto the queue after a delay. + */ + private function laterRaw( + \DateTimeInterface|\DateInterval|int $delay, + array $payload, + ?string $queue = null, + array $options = [], + ): string { + $queue = $this->getQueue($queue, $options); + + $task = $queue->dispatch( + $queue + ->create($payload['displayName'] ?? Uuid::uuid4()->toString()) + ->withValue($payload) + ->withDelay($this->availableAt($delay)), + ); + + return $task->getId(); + } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 8bf06ec..54d21e7 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,8 +4,15 @@ namespace Spiral\RoadRunnerLaravel; +use Illuminate\Contracts\Container\Container; use Spiral\Attributes\AttributeReader; use Spiral\Attributes\ReaderInterface; +use Spiral\Core\FactoryInterface; +use Spiral\Grpc\Client\Config\ConnectionConfig; +use Spiral\Grpc\Client\Config\GrpcClientConfig; +use Spiral\Grpc\Client\Config\ServiceConfig; +use Spiral\Grpc\Client\Config\TlsConfig; +use Spiral\Grpc\Client\ServiceClientProvider; final class ServiceProvider extends \Illuminate\Support\ServiceProvider { @@ -23,6 +30,7 @@ public function register(): void { $this->app->singleton(ReaderInterface::class, AttributeReader::class); $this->initializeConfigs(); + $this->initializeGrpcClientServices(); } protected function initializeConfigs(): void @@ -33,4 +41,70 @@ protected function initializeConfigs(): void \realpath(self::getConfigPath()) => config_path(\basename(self::getConfigPath())), ], 'config'); } + + protected function initializeGrpcClientServices(): void + { + $this->app->singleton(FactoryInterface::class, fn() => new class($this->app) implements FactoryInterface { + public function __construct( + private readonly Container $container, + ) {} + + /** + * @param class-string $class + * @param array $parameters + */ + public function make(string $class, array $parameters = []): object + { + return $this->container->make($class, $parameters); + } + }); + $this->app->singleton(ServiceClientProvider::class, function () { + $toNonEmptyStringOrNull = static fn($value): ?string => (is_string($value) && $value !== '') ? $value : null; + /** + * @var array, + * tls?: array{ + * rootCerts?: non-empty-string|null, + * privateKey?: non-empty-string|null, + * certChain?: non-empty-string|null, + * serverName?: non-empty-string|null + * } + * }> + */ + $rawServices = config('roadrunner.grpc.clients.services', []); + $services = collect($rawServices); + $serviceConfigs = []; + foreach ($services as $service) { + $tls = null; + if (isset($service['tls'])) { + $tlsConfig = $service['tls']; + $tls = new TlsConfig( + $toNonEmptyStringOrNull($tlsConfig['rootCerts'] ?? null), + $toNonEmptyStringOrNull($tlsConfig['privateKey'] ?? null), + $toNonEmptyStringOrNull($tlsConfig['certChain'] ?? null), + $toNonEmptyStringOrNull($tlsConfig['serverName'] ?? null), + ); + } + /** @var non-empty-string $connection */ + $connection = $service['connection']; + /** @var list $interfaces */ + $interfaces = $service['interfaces']; + $serviceConfigs[] = new ServiceConfig( + connections: new ConnectionConfig($connection, $tls), + interfaces: $interfaces, + ); + } + + /** @var array|\Spiral\Core\Container\Autowire<\Spiral\Interceptors\InterceptorInterface>|\Spiral\Interceptors\InterceptorInterface> $interceptors */ + $interceptors = config('roadrunner.grpc.clients.interceptors', []); + $config = new GrpcClientConfig( + interceptors: $interceptors, + services: $serviceConfigs, + ); + + return new ServiceClientProvider($config, $this->app->make(FactoryInterface::class)); + }); + + } } diff --git a/src/Temporal/Attribute/AssignWorker.php b/src/Temporal/Attribute/AssignWorker.php index be1b594..5870f8e 100644 --- a/src/Temporal/Attribute/AssignWorker.php +++ b/src/Temporal/Attribute/AssignWorker.php @@ -4,7 +4,9 @@ namespace Spiral\RoadRunnerLaravel\Temporal\Attribute; -#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final readonly class AssignWorker { /**