From c278dd3b43b5f7896f72672934716c1a7c060f55 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Thu, 24 Jan 2019 13:45:49 -0600 Subject: [PATCH 1/9] Standard Promises --- design-documents/promises.md | 138 +++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 design-documents/promises.md diff --git a/design-documents/promises.md b/design-documents/promises.md new file mode 100644 index 000000000..64693719c --- /dev/null +++ b/design-documents/promises.md @@ -0,0 +1,138 @@ +### Why? +Service contracts in the future will often be executed in an asynchronous manner +and it's time to introduce a standard Promise to Magento for asynchronous operations to employ +### Requirements +* Promises CANNOT be forwarded with _then_ and _otherwise_ ([see explanation](#forwarding)) +* _then_ accepts a function that will be executed when the promise is resolved, the callback will +receive a single argument - result of the execution +* _otherwise_ accepts a function that will be executed if an error occurs during the asynchronous +operation, it will receive a single argument - a _Throwable_ +* Promises can be used in a synchronous way to prevent methods that use methods returning promises +having to return a promise as well ([see explanation](#callback-hell)); This will be done by promises having _wait_ method +* _wait_ method does not throw an exception if the promise is rejected +nor does it return the result of the resolved promise ([see explanation](#wait-not-unwrapping-promises)) +* If an exception occurs during an asynchronous operation and no _otherwise_ callback is +provided then it will just be rethrown +### API +##### Promise to be used by client code +When client code receives a promise from calling another object's method +it shouldn't have access to _resolve_ and _reject_ methods, it should only be able to +provide callbacks to process promised results and to wait for promised operation's execution. + +This interface will be used as the return type of methods returning promises. +```php +interface PromiseInterface +{ + /** + * @throws PromiseProcessedException When callback was alredy provided. + */ + public function then(callable $callback): void; + + /** + * @throws PromiseProcessedException When callback was alredy provided. + */ + public function otherwise(callable $callback): void; + + public function wait(): void; +} +``` +##### Promise to be created +This promise will be created by asynchronous code +```php +interface ResultPromiseInterface extends PromiseInterface +{ + public function resolve($value): void; + + public function reject(\Throwable $exception): void; +} +``` + +### Implementation +A wrapper around [Guzzle Promises](https://github.com/guzzle/promises) will be created to implement the APIs above. Guzzle Promises fit +most important criteria - they allow synchronous execution as well as asynchronous. It's a mature +and well-known library and, while we would have to add guzzle/promises to our composer.json, +the library is already required in Magento indirectly - we won't be actually adding a new dependency. + +There are other libraries like [Reactphp Promises](https://github.com/reactphp/promise) but they either do not provide synchronous way +to interact with promises or are not as refined. + +### Explanations +##### Forwarding +Consider this code +```php +$promise = $this->anotherObject->doStuff(); +$promise->then($doStuffCallback) + ->otherwise($processErrorCallback) + ->otherwise($processAnotherError) + ->then($doOtherStuff) + ->otherwise($processErrorCallback); +``` +Does 1st _then_ return a forwarded promise? Or is it the same object? +Then what promise is the second _otherwise_ callback for? +Code looking like this is confusing and it will be much cleaner if we don't use forwarding. +```php +$doStuffOperation = $this->someObject->doStuff(); +$result = null; +$doStuffOperation->then(function ($response) use (&$result) { $result = $response; }); +$simultaneousOperation = $this->otherObject->processStuff(); +$doStuffOperation->wait(); +$updated = null; +$saveOperation = $this->repo->save($result); +$saveOperation->then(function ($result) use (&$updated) { $updated = $result; }); + +//Waiting for all +$saveOperation->wait(); +$simultaneousOperation->wait(); +return $updated; +``` +Here we clearly state that for _save operation_ we need to _do stuff operation_ to finish +and _simultaneous operation_ may run up until then end of our algorithm execution. + + +##### Callback hell +Consider this code responsible for placing orders +```php +class ServiceA +{ +.... + + public function process(DTOInterface $dto): ProcessedInterface + { + .... + + $this->serviceB->processSmth($val)->then(function ($val) use ($processed) { + $processed->setBValue($val); + }); + + return $processed; + } + +.... +} +``` +We cannot be sure _serviceB_ has finished doing it's stuff when we return _$processed_. +So, if we cannot wait for the promise _serviceB_ returned, the only thing we can do +is to return a promise ourselves instead of _ProcessedInterface_. But then methods using +_ServiceA::process()_ would have to do the same - and PHP code is not supposed to be this way. + +##### Wait not unwrapping promises +For _wait_ method to also unwrap promises can result in confusion. It's better to have a single way of retreiving promised results and a single way of retreiving errors. + +Consider next situation: +```php +class ServiceA +{ + public function doSmth(): void + { + $promise = $this->otherService->doSmthElse(); + $promise->otherwise(function ($exception) { $this->logger->critical($exception); }); + try { + //wait would throw the exception processed in the otherwise callback once again + $promise->wait(); + } catch (\Throwable $exception) { + //we've already processed this + } + } +} +``` +That is a simple situation but it illustrates how having multiple ways of receiving promised results may lead to duplicating code \ No newline at end of file From df32ddebbabb49d4d93fab8e4d17a75a8b6fbc52 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Tue, 26 Mar 2019 10:37:58 -0500 Subject: [PATCH 2/9] updates --- design-documents/promises.md | 96 +++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/design-documents/promises.md b/design-documents/promises.md index 64693719c..55083c08a 100644 --- a/design-documents/promises.md +++ b/design-documents/promises.md @@ -135,4 +135,98 @@ class ServiceA } } ``` -That is a simple situation but it illustrates how having multiple ways of receiving promised results may lead to duplicating code \ No newline at end of file +That is a simple situation but it illustrates how having multiple ways of receiving promised results may lead to duplicating code + +### Using promises for service contracts +#### Why use promises for service contracts? +Another way that was proposed to execute service contracts in an asynchronous manner was to use async web API, but there +are number of problems with that approach: +* Async web API allows execution of the same operation with different sets of arguments, but not different operations +* Async web API was meant for execution big number of operations at the same time (thouthands) which is not the case + for most functionality and mostly fits only import +* Since only 1 type of operation can be executed at the same time it will be impossible to execute service contracts + from different domains at the same time +* Async web API uses status requests to check whether operations are completed which is alright for large numbers + of operations but for a small number (like 2, 3) it will just generate more requests than just sending 1 request + for each operation + +So to allow execution of multiple service contracts from different domains it's best to send 1 request per operation +and to let client code to chain, pass and properly receive promises of results of operations. + +#### How will it look? +There are to ways we can go about using promises for asynchronous execution of service contracts: +* Service interfaces themselves returning promises for client code to use + + _service contract_: + ```php + interface SomeRepositoryInterface + { + public function save(DTOInterface $data): PromiseInterface; + } + ``` + + _client code_: + ```php + class AnotherService implements AnotherServiceInterface + { + /** + * @var SomeRepositoryInterface + */ + private $someRepo; + + public function doSmth(): void + { + .... + + //Both operations running asynchronously + $promise = $this->someRepo->save($dto); + $anotherPromise = $this->someService->doStuff(); + //Waiting for both results + $promise->wait(); + $anotherPromise->wait(); + } + } + ``` +* Using a runner that will accept interface name, method name and arguments that will return a promise + + _async runner_: + ```php + interface AsynchronousRunnerInterface + { + public function run(string $serviceName, string $serviceMethod, array $arguments): PromiseInterface; + } + ``` + _regular service_: + ```php + interface SomeRepositoryInterface + { + public function save(DTOInterface $dto): void; + } + ``` + _client code_: + ```php + class AnotherService implements AnotherServiceInterface + { + /** + * @var SomeRepositoryInterface + */ + private $someRepo; + + /** + * @var AsynchronousRunnerInterface + */ + private $runner; + + public function doSmth(): void + { + .... + + //Both operations running asynchronously + $promise = $this->runner->run(SomeRepositoryInterface::class, 'save', [$dto]); + $anotherPromise = $this->runner->run(SomeServiceInterface::class, 'doStuff', []); + //Waiting for both results + $promise->wait(); + $anotherPromise->wait(); + } + } + ``` From 0d5d5d187f84a51e047c2ae35ae53bd6f4aa30f9 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Tue, 26 Mar 2019 11:00:46 -0500 Subject: [PATCH 3/9] updates --- design-documents/promises.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design-documents/promises.md b/design-documents/promises.md index 55083c08a..ea1db31cc 100644 --- a/design-documents/promises.md +++ b/design-documents/promises.md @@ -230,3 +230,7 @@ There are to ways we can go about using promises for asynchronous execution of s } } ``` + +### Using promises for existing code +We have a standard HTTP client - Magento\Framework\HTTP\ClientInterface, it can benefit from allowing async requests +functionality for developers to use by employing promises. From eb6cb98951880f5703cc3689e4a859b978f92abf Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Tue, 26 Mar 2019 16:49:35 -0500 Subject: [PATCH 4/9] updates --- design-documents/promises.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design-documents/promises.md b/design-documents/promises.md index ea1db31cc..ddae92f6c 100644 --- a/design-documents/promises.md +++ b/design-documents/promises.md @@ -234,3 +234,7 @@ There are to ways we can go about using promises for asynchronous execution of s ### Using promises for existing code We have a standard HTTP client - Magento\Framework\HTTP\ClientInterface, it can benefit from allowing async requests functionality for developers to use by employing promises. + +This client is being used in Magento\Shipping\Model\Carrier\AbstractCarrierOnline to create package shipments/ +shipment returns in 3rd party systems, the process can be optimized by sending requests asynchronously to create +multiple shipments at once. From fc15a42c2078e460219184467d9dea36de71facd Mon Sep 17 00:00:00 2001 From: Alex Horkun Date: Fri, 29 Mar 2019 11:50:42 -0500 Subject: [PATCH 5/9] updates --- design-documents/promises.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/promises.md b/design-documents/promises.md index ddae92f6c..6304c5c16 100644 --- a/design-documents/promises.md +++ b/design-documents/promises.md @@ -154,7 +154,7 @@ So to allow execution of multiple service contracts from different domains it's and to let client code to chain, pass and properly receive promises of results of operations. #### How will it look? -There are to ways we can go about using promises for asynchronous execution of service contracts: +There are two ways we can go about using promises for asynchronous execution of service contracts: * Service interfaces themselves returning promises for client code to use _service contract_: From 7fa6e001b9119c95b725c85a6d5f5db69e92644c Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Fri, 12 Apr 2019 15:13:52 -0500 Subject: [PATCH 6/9] deferred --- design-documents/promises.md | 209 ++++++++++++----------------------- 1 file changed, 70 insertions(+), 139 deletions(-) diff --git a/design-documents/promises.md b/design-documents/promises.md index ddae92f6c..a08cfa351 100644 --- a/design-documents/promises.md +++ b/design-documents/promises.md @@ -1,144 +1,62 @@ ### Why? Service contracts in the future will often be executed in an asynchronous manner -and it's time to introduce a standard Promise to Magento for asynchronous operations to employ +and it's time to introduce a standard Promise to Magento for asynchronous operations to employ. +Also operations like sending HTTP requests can be easily performed asynchronously since cUrl multi can be utilized +to send requests asynchronously. ### Requirements -* Promises CANNOT be forwarded with _then_ and _otherwise_ ([see explanation](#forwarding)) -* _then_ accepts a function that will be executed when the promise is resolved, the callback will -receive a single argument - result of the execution -* _otherwise_ accepts a function that will be executed if an error occurs during the asynchronous -operation, it will receive a single argument - a _Throwable_ -* Promises can be used in a synchronous way to prevent methods that use methods returning promises -having to return a promise as well ([see explanation](#callback-hell)); This will be done by promises having _wait_ method -* _wait_ method does not throw an exception if the promise is rejected -nor does it return the result of the resolved promise ([see explanation](#wait-not-unwrapping-promises)) -* If an exception occurs during an asynchronous operation and no _otherwise_ callback is -provided then it will just be rethrown +* Avoid callbacks that cause noodle code and generally alien to PHP +* Introduce a way to work with asynchronous operations in a familiar way +* Employ solution within core code to serve as an example for our and 3rd party developers ### API -##### Promise to be used by client code -When client code receives a promise from calling another object's method -it shouldn't have access to _resolve_ and _reject_ methods, it should only be able to -provide callbacks to process promised results and to wait for promised operation's execution. +##### Deferred +A future that describes a values that will be available later. +If a library returns a promise or it's own implementation of a future it can be easily wrapped to support our interface. This interface will be used as the return type of methods returning promises. ```php -interface PromiseInterface +interface DeferredInterface { /** - * @throws PromiseProcessedException When callback was alredy provided. + * Wait for and return the value. + * + * @return mixed Value. + * @throws \Throwable When it was impossible to get the value. */ - public function then(callable $callback): void; - + public function get(); + /** - * @throws PromiseProcessedException When callback was alredy provided. + * Is the process of getting the value is done? + * + * @return bool */ - public function otherwise(callable $callback): void; - - public function wait(): void; -} -``` -##### Promise to be created -This promise will be created by asynchronous code -```php -interface ResultPromiseInterface extends PromiseInterface -{ - public function resolve($value): void; - - public function reject(\Throwable $exception): void; + public function isDone(): bool; } ``` ### Implementation -A wrapper around [Guzzle Promises](https://github.com/guzzle/promises) will be created to implement the APIs above. Guzzle Promises fit -most important criteria - they allow synchronous execution as well as asynchronous. It's a mature -and well-known library and, while we would have to add guzzle/promises to our composer.json, -the library is already required in Magento indirectly - we won't be actually adding a new dependency. - -There are other libraries like [Reactphp Promises](https://github.com/reactphp/promise) but they either do not provide synchronous way -to interact with promises or are not as refined. +This interface will be used as a wrapper for libraries that return promises and deferred values. ### Explanations -##### Forwarding -Consider this code -```php -$promise = $this->anotherObject->doStuff(); -$promise->then($doStuffCallback) - ->otherwise($processErrorCallback) - ->otherwise($processAnotherError) - ->then($doOtherStuff) - ->otherwise($processErrorCallback); -``` -Does 1st _then_ return a forwarded promise? Or is it the same object? -Then what promise is the second _otherwise_ callback for? -Code looking like this is confusing and it will be much cleaner if we don't use forwarding. -```php -$doStuffOperation = $this->someObject->doStuff(); -$result = null; -$doStuffOperation->then(function ($response) use (&$result) { $result = $response; }); -$simultaneousOperation = $this->otherObject->processStuff(); -$doStuffOperation->wait(); -$updated = null; -$saveOperation = $this->repo->save($result); -$saveOperation->then(function ($result) use (&$updated) { $updated = $result; }); - -//Waiting for all -$saveOperation->wait(); -$simultaneousOperation->wait(); -return $updated; -``` -Here we clearly state that for _save operation_ we need to _do stuff operation_ to finish -and _simultaneous operation_ may run up until then end of our algorithm execution. +##### Why not promises? +Promises mean callbacks. One callback is fair enough but multiple callbacks within the same method, callbacks for forwarded +promises create noodle-like hard to support code. Closures are a part of PHP but still are a foreign concept complicating +developer experience. Also it is an extra effort to ensure strict typing of return values and arguments with anonymous +functions. - -##### Callback hell -Consider this code responsible for placing orders -```php -class ServiceA -{ -.... - - public function process(DTOInterface $dto): ProcessedInterface - { - .... - - $this->serviceB->processSmth($val)->then(function ($val) use ($processed) { - $processed->setBValue($val); - }); - - return $processed; - } +Other thing is that promises are meant to be forwarded which complicates things. It can be hard to understand what are +you writing a callback for - promised result? Another callback for promised result introduced earlier? OnFulfilled callback +for resolved value in a OnRejected callback to the initial promise? -.... -} -``` -We cannot be sure _serviceB_ has finished doing it's stuff when we return _$processed_. -So, if we cannot wait for the promise _serviceB_ returned, the only thing we can do -is to return a promise ourselves instead of _ProcessedInterface_. But then methods using -_ServiceA::process()_ would have to do the same - and PHP code is not supposed to be this way. +##### Typing +Methods returning Deferred can still provide types for their actual returned values - they can extend the original interface +and add return type hint to the _get()_ method. -##### Wait not unwrapping promises -For _wait_ method to also unwrap promises can result in confusion. It's better to have a single way of retreiving promised results and a single way of retreiving errors. - -Consider next situation: -```php -class ServiceA -{ - public function doSmth(): void - { - $promise = $this->otherService->doSmthElse(); - $promise->otherwise(function ($exception) { $this->logger->critical($exception); }); - try { - //wait would throw the exception processed in the otherwise callback once again - $promise->wait(); - } catch (\Throwable $exception) { - //we've already processed this - } - } -} -``` -That is a simple situation but it illustrates how having multiple ways of receiving promised results may lead to duplicating code +##### Advantage +Since deferred does not require any confusing callbacks and forwarding it's pretty easy to just treat it as a values +and only calling _get()_ when you actually need it. Client code will look mostly like it's just a regular synchronous code. -### Using promises for service contracts -#### Why use promises for service contracts? +### Using Deferred for service contracts +#### Why use futures for service contracts? Another way that was proposed to execute service contracts in an asynchronous manner was to use async web API, but there are number of problems with that approach: * Async web API allows execution of the same operation with different sets of arguments, but not different operations @@ -151,17 +69,29 @@ are number of problems with that approach: for each operation So to allow execution of multiple service contracts from different domains it's best to send 1 request per operation -and to let client code to chain, pass and properly receive promises of results of operations. +and to let client code to work with asynchronously received values almost as they would've with synchronous ones. #### How will it look? -There are to ways we can go about using promises for asynchronous execution of service contracts: -* Service interfaces themselves returning promises for client code to use - +There are to ways we can go about using Deferred for asynchronous execution of service contracts: +* Service interfaces themselves returning deferred values for client code to use + + _contract's deferred_: + ```php + interface DTODeferredInterface extends DeferredInterface + { + /** + * @inheritDoc + * @return DTOInterface + */ + public function get(): DTOInterface; + } + ``` + _service contract_: ```php interface SomeRepositoryInterface { - public function save(DTOInterface $data): PromiseInterface; + public function save(DTOInterface $data): DTODeferredInterface; } ``` @@ -179,28 +109,28 @@ There are to ways we can go about using promises for asynchronous execution of s .... //Both operations running asynchronously - $promise = $this->someRepo->save($dto); - $anotherPromise = $this->someService->doStuff(); - //Waiting for both results - $promise->wait(); - $anotherPromise->wait(); + $deferredDTO = $this->someRepo->save($dto); + $deferredStuff = $this->someService->doStuff(); + //Started both processes at the same time, waiting for both to finish + $dto = $deferredDTO->get(); + $stuff = $deferredStuff->get(); } } ``` -* Using a runner that will accept interface name, method name and arguments that will return a promise +* Using a runner that will accept interface name, method name and arguments that will return a deferred _async runner_: ```php interface AsynchronousRunnerInterface { - public function run(string $serviceName, string $serviceMethod, array $arguments): PromiseInterface; + public function run(string $serviceName, string $serviceMethod, array $arguments): DeferredInterface; } ``` _regular service_: ```php interface SomeRepositoryInterface { - public function save(DTOInterface $dto): void; + public function save(DTOInterface $dto): DTOInterface; } ``` _client code_: @@ -222,18 +152,19 @@ There are to ways we can go about using promises for asynchronous execution of s .... //Both operations running asynchronously - $promise = $this->runner->run(SomeRepositoryInterface::class, 'save', [$dto]); - $anotherPromise = $this->runner->run(SomeServiceInterface::class, 'doStuff', []); - //Waiting for both results - $promise->wait(); - $anotherPromise->wait(); + $deferredDTO = $this->runner->run(SomeRepositoryInterface::class, 'save', [$dto]); + $deferredStuff = $this->runner->run(SomeServiceInterface::class, 'doStuff', []); + //Started both processes at the same time, waiting for both to finish + $dto = $deferredDTO->get(); + $stuff = $deferredStuff->get() } } ``` -### Using promises for existing code +### Using deferred for existing code We have a standard HTTP client - Magento\Framework\HTTP\ClientInterface, it can benefit from allowing async requests -functionality for developers to use by employing promises. +functionality for developers to use by employing promises. Since it's an API, and a messy one at that, we should create +a new asynchronous client. This client is being used in Magento\Shipping\Model\Carrier\AbstractCarrierOnline to create package shipments/ shipment returns in 3rd party systems, the process can be optimized by sending requests asynchronously to create From a882c1ccc4c96ebd687384bdf31fff39855b77db Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Fri, 19 Apr 2019 18:36:51 -0500 Subject: [PATCH 7/9] updates --- design-documents/promises.md | 46 ++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/design-documents/promises.md b/design-documents/promises.md index 683c013ff..b67d0ede2 100644 --- a/design-documents/promises.md +++ b/design-documents/promises.md @@ -1,6 +1,6 @@ ### Why? Service contracts in the future will often be executed in an asynchronous manner -and it's time to introduce a standard Promise to Magento for asynchronous operations to employ. +and it's time to introduce a standard Future to Magento for asynchronous operations to employ. Also operations like sending HTTP requests can be easily performed asynchronously since cUrl multi can be utilized to send requests asynchronously. ### Requirements @@ -10,9 +10,10 @@ to send requests asynchronously. ### API ##### Deferred A future that describes a values that will be available later. +Only contains basic methods so that any asynchronous operations can be wrapping using this interface. If a library returns a promise or it's own implementation of a future it can be easily wrapped to support our interface. -This interface will be used as the return type of methods returning promises. +This interface will be used as the return type of methods returning futures. ```php interface DeferredInterface { @@ -33,8 +34,32 @@ interface DeferredInterface } ``` +Advanced interface that allows canceling asynchronous operations that have been started. +```php +interface CancelableDeferredInterface extends DeferredInterface +{ + /** + * Cancels the opration. + * + * Will not cancel the operation when it has already started and given $force is not true. + * + * @param bool $force Cancel operation even if it's already started. + * @return void + * @throws CancelingDeferredException When failed to cancel. + */ + public function cancel(bool $force = false): void; + + /** + * Whether the operation has been cancelled already. + * + * @return bool + */ + public function isCancelled(): bool; +} +``` + ### Implementation -This interface will be used as a wrapper for libraries that return promises and deferred values. +This interface will be used as a wrapper for libraries that return promises and futures. ### Explanations ##### Why not promises? @@ -163,9 +188,22 @@ There are two ways we can go about using Deferred for asynchronous execution of ### Using deferred for existing code We have a standard HTTP client - Magento\Framework\HTTP\ClientInterface, it can benefit from allowing async requests -functionality for developers to use by employing promises. Since it's an API, and a messy one at that, we should create +functionality for developers to use by employing futures. Since it's an API, and a messy one at that, we should create a new asynchronous client. This client is being used in Magento\Shipping\Model\Carrier\AbstractCarrierOnline to create package shipments/ shipment returns in 3rd party systems, the process can be optimized by sending requests asynchronously to create multiple shipments at once. + +### Prototype +To demonstrate using DeferredInterface for asynchronous operations I've created prototype where requests sent to a +shipment provider were updated to be sent asynchronously using new asynchronous HTTP client. + +To start check out an integration test that shows the difference between sending requests one by one or at the same time +available [here](https://github.com/AlexMaxHorkun/magento2/blob/futures-prototype/dev/tests/integration/testsuite/Magento/Ups/Model/ShipmentCreatorTest.php). +To make it work you would have to set up couple of environment variable before running it, list of those variables and +what they are meant for can be found inside the fixture used in the test. + +### Sources +* DeferredInterface is based on Java's [Future interface](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html) +* For real asynchronous operations [Guzzle HTTP client](https://github.com/guzzle/guzzle) was used From 1fb8265ca95c121dc4ff2246e81fc5dbddb0bca4 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Tue, 23 Apr 2019 12:42:32 -0500 Subject: [PATCH 8/9] async client API --- design-documents/promises.md | 114 +++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/design-documents/promises.md b/design-documents/promises.md index b67d0ede2..5ca162802 100644 --- a/design-documents/promises.md +++ b/design-documents/promises.md @@ -195,6 +195,120 @@ This client is being used in Magento\Shipping\Model\Carrier\AbstractCarrierOnlin shipment returns in 3rd party systems, the process can be optimized by sending requests asynchronously to create multiple shipments at once. +#### Asynchronous HTTP client API +Client: +```php +interface AsyncClientInterface +{ + /** + * Perform an HTTP request. + * + * @param Request $request + * @return HttpResponseDeferredInterface + */ + public function request(Request $request): HttpResponseDeferredInterface; +} +``` + +Request: +```php +class Request +{ + const METHOD_GET = 'GET'; + + const METHOD_POST = 'POST'; + + const METHOD_HEAD = 'HEAD'; + + const METHOD_PUT = 'PUT'; + + const METHOD_DELETE = 'DELETE'; + + const METHOD_CONNECT = 'CONNECT'; + + const METHOD_PATCH = 'PATCH'; + + const METHOD_OPTIONS = 'OPTIONS'; + + const METHOD_PROPFIND = 'PROPFIND'; + + const METHOD_TRACE = 'TRACE'; + + /** + * URL to send request to. + * + * @return string + */ + public function getUrl(): string; + + /** + * HTTP method to use. + * + * @return string + */ + public function getMethod(): string; + + /** + * Headers to send. + * + * Keys - header names, values - array of header values. + * + * @return string[][] + */ + public function getHeaders(): array; + + /** + * Body to send + * + * @return string|null + */ + public function getBody(): ?string; +} +``` + +Response: +```php +class Response +{ + /** + * Status code returned. + * + * @return int + */ + public function getStatusCode(): int; + + /** + * With header names as keys (case preserved) and values as header values. + * + * If a header's value had multiple values they will be shown like "val1, val2, val3". + * + * @return string[] + */ + public function getHeaders(): array; + + /** + * Response body. + * + * @return string + */ + public function getBody(): string; +} +``` + +Future containing response: +```php +interface HttpResponseDeferredInterface extends DeferredInterface +{ + /** + * @inheritdoc + * @return Response HTTP response. + * @throws HttpException When failed to send the request, + * if response has 400+ status code it will not be treated as an exception. + */ + public function get(): Response; +} +``` + ### Prototype To demonstrate using DeferredInterface for asynchronous operations I've created prototype where requests sent to a shipment provider were updated to be sent asynchronously using new asynchronous HTTP client. From 3e4d3aeae406998ad9fd05107943321af7f2b24a Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Wed, 24 Apr 2019 16:54:07 -0500 Subject: [PATCH 9/9] updates --- design-documents/promises.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design-documents/promises.md b/design-documents/promises.md index 5ca162802..673eed40a 100644 --- a/design-documents/promises.md +++ b/design-documents/promises.md @@ -58,6 +58,10 @@ interface CancelableDeferredInterface extends DeferredInterface } ``` +This interface can be used for operations that take to long and can be cancelled +(like stopping waiting for a server's response) or for delayed operations that could be +canceled even before they start (like cancelling an aggregated SQL query to DB after all the required criteria has been collected). + ### Implementation This interface will be used as a wrapper for libraries that return promises and futures.