-
Notifications
You must be signed in to change notification settings - Fork 22
Add async/promise support to Twirp PHP clients #225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
winmutt
wants to merge
1
commit into
twirphp:master
Choose a base branch
from
winmutt:async
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,290 @@ | ||
| Async Processing | ||
| ================ | ||
|
|
||
| This document describes how to use asynchronous processing with Twirp PHP clients using Guzzle's promise framework. | ||
|
|
||
| Overview | ||
| -------- | ||
|
|
||
| All generated Twirp PHP clients now support asynchronous request processing using promises that conform to the PSR-7 standard and leverage Guzzle's promise implementation. This allows you to make non-blocking HTTP requests and handle multiple requests concurrently. | ||
|
|
||
| Requirements | ||
| ------------ | ||
|
|
||
| * **Guzzle HTTP Client** (^7.0): For async support, you must use Guzzle as your HTTP client | ||
| * **guzzlehttp/promises** (^1.5 or ^2.0): Promise implementation (usually installed as a Guzzle dependency) | ||
|
|
||
| Installation | ||
| ------------ | ||
|
|
||
| If not already installed, add Guzzle to your project: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| composer require guzzlehttp/guzzle | ||
|
|
||
| Basic Usage | ||
| ----------- | ||
|
|
||
| Synchronous Request (Existing Behavior) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| use GuzzleHttp\Client as GuzzleClient; | ||
| use YourNamespace\GreeterClient; | ||
| use YourNamespace\HelloRequest; | ||
|
|
||
| // Create a Guzzle HTTP client | ||
| $httpClient = new GuzzleClient(); | ||
|
|
||
| // Create the Twirp client | ||
| $client = new GreeterClient('http://localhost:8080', $httpClient); | ||
|
|
||
| // Make a synchronous request | ||
| $request = new HelloRequest(); | ||
| $request->setName('World'); | ||
|
|
||
| try { | ||
| $response = $client->SayHello([], $request); | ||
| echo $response->getMessage(); | ||
| } catch (\Twirp\Error $e) { | ||
| echo "Error: " . $e->getMessage(); | ||
| } | ||
|
|
||
| Asynchronous Request (New Feature) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| use GuzzleHttp\Client as GuzzleClient; | ||
| use YourNamespace\GreeterClient; | ||
| use YourNamespace\HelloRequest; | ||
|
|
||
| // Create a Guzzle HTTP client | ||
| $httpClient = new GuzzleClient(); | ||
|
|
||
| // Create the Twirp client | ||
| $client = new GreeterClient('http://localhost:8080', $httpClient); | ||
|
|
||
| // Make an asynchronous request | ||
| $request = new HelloRequest(); | ||
| $request->setName('World'); | ||
|
|
||
| // Returns a promise immediately | ||
| $promise = $client->SayHelloAsync([], $request); | ||
|
|
||
| // Handle the response when it's ready | ||
| $promise->then( | ||
| function ($response) { | ||
| echo "Success: " . $response->getMessage(); | ||
| }, | ||
| function ($exception) { | ||
| echo "Error: " . $exception->getMessage(); | ||
| } | ||
| ); | ||
|
|
||
| // Wait for the promise to resolve (optional) | ||
| $promise->wait(); | ||
|
|
||
| Advanced Usage | ||
| -------------- | ||
|
|
||
| Multiple Concurrent Requests | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
|
||
| You can make multiple asynchronous requests and wait for all of them to complete: | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| use GuzzleHttp\Client as GuzzleClient; | ||
| use GuzzleHttp\Promise; | ||
| use YourNamespace\GreeterClient; | ||
| use YourNamespace\HelloRequest; | ||
|
|
||
| $httpClient = new GuzzleClient(); | ||
| $client = new GreeterClient('http://localhost:8080', $httpClient); | ||
|
|
||
| // Create multiple requests | ||
| $promises = []; | ||
|
|
||
| foreach (['Alice', 'Bob', 'Charlie'] as $name) { | ||
| $request = new HelloRequest(); | ||
| $request->setName($name); | ||
| $promises[$name] = $client->SayHelloAsync([], $request); | ||
| } | ||
|
|
||
| // Wait for all promises to complete | ||
| $results = Promise\Utils::settle($promises)->wait(); | ||
|
|
||
| // Process results | ||
| foreach ($results as $name => $result) { | ||
| if ($result['state'] === 'fulfilled') { | ||
| echo "$name: " . $result['value']->getMessage() . "\n"; | ||
| } else { | ||
| echo "$name failed: " . $result['reason']->getMessage() . "\n"; | ||
| } | ||
| } | ||
|
|
||
| Using Promise::all() | ||
| ^^^^^^^^^^^^^^^^^^^^ | ||
|
|
||
| If you want all requests to succeed or fail together: | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| use GuzzleHttp\Promise; | ||
|
|
||
| $promises = [ | ||
| 'alice' => $client->SayHelloAsync([], $request1), | ||
| 'bob' => $client->SayHelloAsync([], $request2), | ||
| 'charlie' => $client->SayHelloAsync([], $request3), | ||
| ]; | ||
|
|
||
| // Wait for all promises - throws if any fails | ||
| try { | ||
| $results = Promise\Utils::all($promises)->wait(); | ||
| foreach ($results as $name => $response) { | ||
| echo "$name: " . $response->getMessage() . "\n"; | ||
| } | ||
| } catch (\Exception $e) { | ||
| echo "One or more requests failed: " . $e->getMessage(); | ||
| } | ||
|
|
||
| Chaining Promises | ||
| ^^^^^^^^^^^^^^^^^ | ||
|
|
||
| You can chain multiple operations: | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| $client->SayHelloAsync([], $request) | ||
| ->then(function ($response) use ($client) { | ||
| // Use the response to make another request | ||
| $followUpRequest = new AnotherRequest(); | ||
| $followUpRequest->setData($response->getMessage()); | ||
| return $client->AnotherMethodAsync([], $followUpRequest); | ||
| }) | ||
| ->then(function ($finalResponse) { | ||
| echo "Final result: " . $finalResponse->getResult(); | ||
| }) | ||
| ->otherwise(function ($exception) { | ||
| echo "Error in chain: " . $exception->getMessage(); | ||
| }) | ||
| ->wait(); | ||
|
|
||
| Non-blocking Execution | ||
| ^^^^^^^^^^^^^^^^^^^^^^ | ||
|
|
||
| For truly non-blocking execution, don't call ``wait()``: | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| // Fire off async requests without waiting | ||
| $promise1 = $client->Method1Async([], $request1); | ||
| $promise2 = $client->Method2Async([], $request2); | ||
|
|
||
| // Do other work here | ||
| doOtherWork(); | ||
|
|
||
| // Later, check if promises are resolved | ||
| if ($promise1->getState() === 'fulfilled') { | ||
| $result = $promise1->wait(); // Returns immediately since already resolved | ||
| } | ||
|
|
||
| Fallback Behavior | ||
| ----------------- | ||
|
|
||
| If you don't use Guzzle as your HTTP client (e.g., using a generic PSR-18 client), the async methods will automatically fall back to synchronous execution and return a resolved promise. This ensures backward compatibility. | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| use Symfony\Component\HttpClient\Psr18Client; | ||
|
|
||
| // Non-Guzzle client | ||
| $httpClient = new Psr18Client(); | ||
| $client = new GreeterClient('http://localhost:8080', $httpClient); | ||
|
|
||
| // This will execute synchronously but still return a promise | ||
| $promise = $client->SayHelloAsync([], $request); | ||
| $response = $promise->wait(); | ||
|
|
||
| Error Handling | ||
| -------------- | ||
|
|
||
| Async methods throw the same ``\Twirp\Error`` exceptions as synchronous methods, but they're caught in the promise rejection handler: | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| $client->SayHelloAsync([], $request) | ||
| ->then( | ||
| function ($response) { | ||
| // Success handler | ||
| return $response; | ||
| }, | ||
| function ($error) { | ||
| // Error handler | ||
| if ($error instanceof \Twirp\Error) { | ||
| echo "Twirp Error Code: " . $error->getErrorCode() . "\n"; | ||
| echo "Message: " . $error->getMessage() . "\n"; | ||
|
|
||
| // Check metadata | ||
| $metadata = $error->getMeta(); | ||
| if (isset($metadata['http_error_from_intermediary'])) { | ||
| echo "HTTP intermediary error\n"; | ||
| } | ||
| } | ||
| throw $error; // Re-throw if you want to propagate | ||
| } | ||
| ); | ||
|
|
||
| Best Practices | ||
| -------------- | ||
|
|
||
| 1. **Use Guzzle for async operations**: While other PSR-18 clients work, only Guzzle supports true async processing. | ||
|
|
||
| 2. **Handle errors appropriately**: Always provide rejection handlers for your promises to catch errors. | ||
|
|
||
| 3. **Don't call wait() in loops**: If making many requests, collect all promises first, then use ``Promise\Utils::settle()`` or ``Promise\Utils::all()``. | ||
|
|
||
| 4. **Consider connection pooling**: Guzzle reuses connections by default, which is more efficient for multiple requests. | ||
|
|
||
| 5. **Set appropriate timeouts**: Configure Guzzle with appropriate timeouts for async operations: | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| $httpClient = new GuzzleClient([ | ||
| 'timeout' => 10.0, | ||
| 'connect_timeout' => 5.0, | ||
| ]); | ||
|
|
||
| JSON Client Support | ||
| ------------------- | ||
|
|
||
| Both the Protobuf client and JSON client support async operations: | ||
|
|
||
| .. code-block:: php | ||
|
|
||
| use YourNamespace\GreeterJsonClient; | ||
|
|
||
| $jsonClient = new GreeterJsonClient('http://localhost:8080', $httpClient); | ||
| $promise = $jsonClient->SayHelloAsync([], $request); | ||
|
|
||
| Performance Considerations | ||
| -------------------------- | ||
|
|
||
| Async requests provide the most benefit when: | ||
|
|
||
| * Making multiple independent requests that can run concurrently | ||
| * Dealing with high-latency services | ||
| * Building services that need to remain responsive while waiting for I/O | ||
|
|
||
| For single requests with no other work to do, synchronous requests may be simpler and sufficient. | ||
|
|
||
| More Information | ||
| ---------------- | ||
|
|
||
| * `Guzzle Promises Documentation <https://github.com/guzzle/promises>`_ | ||
| * `Guzzle Async Requests <https://docs.guzzlephp.org/en/stable/quickstart.html#async-requests>`_ | ||
| * `PSR-7: HTTP Message Interface <https://www.php-fig.org/psr/psr-7/>`_ | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really appreciate writing an awesome documentation.
However, the docs have been migrated here: https://github.com/twirphp/twirphp.github.io
Could you send a PR there? It's in markdown format though.
I'm happy to merge this PR without the docs PR though, so it's not a blocker.