From f27f69e75da42748e831178873460732ab78fc7a Mon Sep 17 00:00:00 2001 From: Alex Horkun Date: Mon, 24 Jun 2019 12:45:27 -0500 Subject: [PATCH 1/4] Create a page describing working with async code Page described how to work with DeferredInterface introduced in https://github.com/magento/architecture/pull/71 --- .../extension-dev-guide/async-operations.md | 268 ++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 guides/v2.3/extension-dev-guide/async-operations.md diff --git a/guides/v2.3/extension-dev-guide/async-operations.md b/guides/v2.3/extension-dev-guide/async-operations.md new file mode 100644 index 00000000000..1ef88b1f5d4 --- /dev/null +++ b/guides/v2.3/extension-dev-guide/async-operations.md @@ -0,0 +1,268 @@ +# Asynchronous and deferred operations +Asynchronous operations are not something native to PHP but it still is possible to execute some heavy +operations simultaneously or delay them until they absolutely have to be finished. + +To make writing asynchronous code easier Magento provides DeferredInterface to use +as the result of asynchronous operations to allow client code to work with such +operations as it would with usual values. + +## DeferredInterface +_Magento\Framework\Async\DeferredInterface_ is rather simple: +```php +interface DeferredInterface +{ + /** + * @return mixed Value. + * @throws \Throwable + */ + public function get(); + + public function isDone(): bool; +} +``` +When client code needs the result _get()_ method will be called to retrieve the result +and if it's possible to wait _isDone()_ can be used to see whether the wait is needed. + +There are 2 types of asynchronous operations where _DeferredInterface_ can be used to describe the result: +* Asynchronous operations in progress, calling _get()_ would wait for them to finish and return their result +* Deferred operations, _get()_ would actually start an operation, wait for it to finish and return the result + +Sometimes developers require more control over long asynchronous operations that's why +there is an extended deferred variant - Magento\Framework\Async\CancelableDeferredInterface: +```php +interface CancelableDeferredInterface extends DeferredInterface +{ + /** + * @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; + + /** + * @return bool + */ + public function isCancelled(): bool; +} +``` +This interface is for operations that may take too long and can be canceled. + +#### Client code +Now let's see how client code of an asynchronous operations would look like. +Assume _serviceA, serviceB and serviceC_ all execute asynchronous operations (like sending HTTP requests) +```php +public function aMethod() { + //Started executing 1st operation + $operationA = $serviceA->executeOp(); + //Executing 2nd operations at the same time + $operationB = $serviceB->executeOp2(); + + //We need to wait for 1st operation to start operation #3 + $serviceC->executeOp3($operationA->get()); + + //We don't have to wait for operation #2, let client code wait for it if it needs the result + //Operation number #3 is being executed simultaneously with operation #2 + return $operationB; +} +``` +And no callback in sight! + +With deferred client code can start multiple operations at the same time, wait for operations required to finish +and pass promise of a result to another method. + +## Deferred proxy +When writing a module or an extension you may not want to burden other developers +with knowing that your method is performing an asynchronous operation and there is a way to hide it - +_Magento\Framework\Async\ProxyDeferredFactory_, With it's help you can return values that seem like regular objects +but are in fact deferred results. + +Example: +```php +public function doARemoteCall(string $uniqueValue): CallResult +{ + //Async HTTP request, get() will return a CallResult instance. + //Call is in progress. + $deferredResult = $this->client->call($uniqueValue); + + //Returns CallResult instance that will call $deferredResult->get() when any of the object's methods is used. + return $this->proxyDeferredFactory->createFor(CallResult::class, $deferredResult); +} + +public function doCallsAndProcess(): Result +{ + //Both calls running simultaneously + $call1 = $this->doARemoteCall('call1'); + $call2 = $this->doARemoteCall('call2'); + + //Only when CallResult::getStuff() is called the $deferredResult->get() is called. + return new Result([ + 'call1' => $call1->getStuff(), + 'call2' => $call2->getStuff() + ]); +} +``` + +## Using DeferredInterface for background operations +As mentioned above the 1st type of asynchronous operations are operations executing in a background. +DeferredInterface can be used here to give client code a promise of a not yet received +result and wait for it by calling the _get()_ method. + +Take a look at the example - creating shipments for multiple products: +```php +class DeferredShipment implements DeferredInterface +{ + private $request; + + private $done = false; + + private $trackingNumber; + + public function __construct(AsyncRequest $request) + { + $this->request = $request; + } + + public function isDone() : bool + { + return $this->done; + } + + public function get() + { + if (!$this->trackingNumber) { + $this->request->wait(); + $this->trackingNumber = json_decode($this->request->getBody(), true)['tracking']; + + $this->done = true; + } + + return $this->trackingNumber; + } +} + +class Shipping +{ + .... + + public function ship(array $products): array + { + $shipments = []; + //Shipping simultaneously + foreach ($products as $product) { + $shipments[] = new DeferredShipment( + $this->client->sendAsync(['id' => $product->getId()]) + ); + } + + return $shipments; + } +} + +class ShipController +{ + .... + + public function execute(Request $request): Response + { + $shipments = $this->shipping->ship($this->producs->find($request->getParam('ids'))); + $trackingsNumbers = []; + foreach ($shipments as $shipment) { + $trackingsNumbers[] = $shipment->get(); + } + + return new Response(['trackings' => $trackingNumbers]); + } +} +``` + +Here multiple shipment requests are being sent at the same time with their results +gathered later. If you don't want to write your own _DeferredInterface_ implementations processing +results of asynchronous calls you can use _CallbackDeferred_ to provide callbacks that will be used +when _get()_ is called. + +## Using DeferredInterface for deferred operations +2nd type of asynchronous operations are operations that are being postponed and executed only when a result is +absolutely needed. + +Let's see the next example: + +Assume you are creating a repository for an entity, you have a method that returns a singular entity +by ID, you want to make a performance optimization for cases when multiple entities are requested +during the same request-response process so you wouldn't load each one separately. + +```php +class EntityRepository +{ + private $requestedEntityIds = []; + + private $identityMap = []; + + ... + + /** + * @return Entity[] + */ + public function findMultiple(array $ids): array + { + ..... + + //Adding found entities to the identity map be able to find them by ID. + foreach ($found as $entity) { + $this->identityMap[$entity->getId()] = $entity; + } + + .... + } + + public function find(string $id): Entity + { + //Adding this ID to the list of previously requested IDs. + $this->requestedEntityIds[] = $id; + + //Returning deferred that will find all requested entities + //and return the one with $id + return $this->proxyDefferedFactory->createFor( + Entity::class, + new CallbackDeferred( + function () use ($id) { + if (empty($this->identityMap[$id])) { + $this->findMultiple($this->requestedEntityIds); + $this->requestedEntityIds = []; + } + + return $this->identityMap[$id]; + } + ) + ); + } + + .... +} + +class EntitiesController +{ + .... + + public function execute(): Response + { + //No actual DB query issued + $criteria1Id = $this->entityService->getEntityIdWithCriteria1(); + $criteria2Id = $this->entityService->getEntityIdWithCriteria2(); + $criteria1Entity = $this->entityRepo->find($criteria1Id); + $criteria2Entity = $this->entityRepo->find($criteria2Id); + + //Querying the DB for both entities only when getStringValue() is called the 1st time. + return new Response( + [ + 'criteria1' => $criteria1Entity->getStringValue(), + 'criteria2' => $criteria2Entity->getStringValue() + ] + ); + } +} +``` + +## Examples in Magento +Please see our asynchronous HTTP client _Magento\Framework\HTTP\AsyncClientInterface_ +and _Magento\Shipping\Model\Shipping_ with various _Magento\Shipping\Model\Carrier\AbstractCarrierOnline_ +implementations to see how DeferredInterface can be used to work with asynchronous code. From 3b807314731b003305780ba8fd36832fccfb5fbf Mon Sep 17 00:00:00 2001 From: Donald Booth Date: Tue, 23 Jul 2019 13:57:42 -0500 Subject: [PATCH 2/4] Grammar and formatting fixes. --- .../extension-dev-guide/async-operations.md | 88 ++++++++++--------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/guides/v2.3/extension-dev-guide/async-operations.md b/guides/v2.3/extension-dev-guide/async-operations.md index 1ef88b1f5d4..125f0be8dba 100644 --- a/guides/v2.3/extension-dev-guide/async-operations.md +++ b/guides/v2.3/extension-dev-guide/async-operations.md @@ -1,13 +1,15 @@ # Asynchronous and deferred operations -Asynchronous operations are not something native to PHP but it still is possible to execute some heavy -operations simultaneously or delay them until they absolutely have to be finished. + +Asynchronous operations are not native to PHP but it is still possible to execute heavy +operations simultaneously, or delay them until they absolutely have to be finished. -To make writing asynchronous code easier Magento provides DeferredInterface to use -as the result of asynchronous operations to allow client code to work with such -operations as it would with usual values. +To make writing asynchronous code easier, Magento provides the `DeferredInterface` to use with asynchronous operations. +This allows client code to work with asynchronous operations just as it would with standard operations. ## DeferredInterface -_Magento\Framework\Async\DeferredInterface_ is rather simple: + +`_Magento\Framework\Async\DeferredInterface_` is quite simple: + ```php interface DeferredInterface { @@ -20,15 +22,18 @@ interface DeferredInterface public function isDone(): bool; } ``` -When client code needs the result _get()_ method will be called to retrieve the result -and if it's possible to wait _isDone()_ can be used to see whether the wait is needed. + +When the client code needs the result, the `_get()_` method will be called to retrieve the result. +`_isDone()_` can be used to see whether the code has completed. -There are 2 types of asynchronous operations where _DeferredInterface_ can be used to describe the result: -* Asynchronous operations in progress, calling _get()_ would wait for them to finish and return their result -* Deferred operations, _get()_ would actually start an operation, wait for it to finish and return the result +There are 2 types of asynchronous operations where `_DeferredInterface_` can be used to describe the result: + +* With asynchronous operations in progress, calling `_get()_` would wait for them to finish and return their result. +* With deferred operations, `_get()_` would actually start the operation, wait for it to finish, and then return the result. -Sometimes developers require more control over long asynchronous operations that's why -there is an extended deferred variant - Magento\Framework\Async\CancelableDeferredInterface: +Sometimes developers require more control over long asynchronous operations. +That is why there is an extended deferred variant - `Magento\Framework\Async\CancelableDeferredInterface`: + ```php interface CancelableDeferredInterface extends DeferredInterface { @@ -45,15 +50,18 @@ interface CancelableDeferredInterface extends DeferredInterface public function isCancelled(): bool; } ``` + This interface is for operations that may take too long and can be canceled. -#### Client code -Now let's see how client code of an asynchronous operations would look like. -Assume _serviceA, serviceB and serviceC_ all execute asynchronous operations (like sending HTTP requests) +### Client code + +Assumeing that `_serviceA`, `serviceB` and `serviceC_` all execute asynchronous operations, such as HTTP requests, the client code would look like: + ```php public function aMethod() { //Started executing 1st operation $operationA = $serviceA->executeOp(); + //Executing 2nd operations at the same time $operationB = $serviceB->executeOp2(); @@ -65,18 +73,19 @@ public function aMethod() { return $operationB; } ``` -And no callback in sight! + +And not a callback in sight! -With deferred client code can start multiple operations at the same time, wait for operations required to finish -and pass promise of a result to another method. +With the deferred client, the code can start multiple operations at the same time, wait for operations required to finish and pass the promise of a result to another method. -## Deferred proxy -When writing a module or an extension you may not want to burden other developers -with knowing that your method is performing an asynchronous operation and there is a way to hide it - -_Magento\Framework\Async\ProxyDeferredFactory_, With it's help you can return values that seem like regular objects +## ProxyDeferredFactory + +When writing a module or an extension, you may not want to burden other developers with having to know that your method is performing an asynchronous operation. +There is a way to hide it: `_Magento\Framework\Async\ProxyDeferredFactory_`. With its help, you can return values that seem like regular objects but are in fact deferred results. -Example: +For example: + ```php public function doARemoteCall(string $uniqueValue): CallResult { @@ -103,11 +112,12 @@ public function doCallsAndProcess(): Result ``` ## Using DeferredInterface for background operations -As mentioned above the 1st type of asynchronous operations are operations executing in a background. -DeferredInterface can be used here to give client code a promise of a not yet received -result and wait for it by calling the _get()_ method. + +As mentioned above, the first type of asynchronous operations are operations executing in a background. +`DeferredInterface` can be used to give client code a promise of a not-yet-received result and wait for it by calling the `_get()_` method. -Take a look at the example - creating shipments for multiple products: +Take a look at an example: creating shipments for multiple products: + ```php class DeferredShipment implements DeferredInterface { @@ -175,20 +185,17 @@ class ShipController } ``` -Here multiple shipment requests are being sent at the same time with their results -gathered later. If you don't want to write your own _DeferredInterface_ implementations processing -results of asynchronous calls you can use _CallbackDeferred_ to provide callbacks that will be used -when _get()_ is called. +Here, multiple shipment requests are being sent at the same time with their results gathered later. +If you do not want to write your own `_DeferredInterface_` implementation, you can use `_CallbackDeferred_` to provide callbacks that will be used when `_get()_` is called. ## Using DeferredInterface for deferred operations -2nd type of asynchronous operations are operations that are being postponed and executed only when a result is -absolutely needed. + +The second type of asynchronous operations are operations that are being postponed and executed only when a result is absolutely needed. -Let's see the next example: +An example: -Assume you are creating a repository for an entity, you have a method that returns a singular entity -by ID, you want to make a performance optimization for cases when multiple entities are requested -during the same request-response process so you wouldn't load each one separately. +Assume you are creating a repository for an entity and you have a method that returns a singular entity by ID. +You want to make a performance optimization for cases when multiple entities are requested during the same request-response process, so you would not load them separately. ```php class EntityRepository @@ -263,6 +270,5 @@ class EntitiesController ``` ## Examples in Magento -Please see our asynchronous HTTP client _Magento\Framework\HTTP\AsyncClientInterface_ -and _Magento\Shipping\Model\Shipping_ with various _Magento\Shipping\Model\Carrier\AbstractCarrierOnline_ -implementations to see how DeferredInterface can be used to work with asynchronous code. + +Please see our asynchronous HTTP client` _Magento\Framework\HTTP\AsyncClientInterface_` and `_Magento\Shipping\Model\Shipping_` with various `_Magento\Shipping\Model\Carrier\AbstractCarrierOnline_` implementations to see how `DeferredInterface` can be used to work with asynchronous code. From 771cfd864cc95406c29e55455ae0b217d22a2daf Mon Sep 17 00:00:00 2001 From: Donald Booth Date: Tue, 23 Jul 2019 14:26:05 -0500 Subject: [PATCH 3/4] Fixed markdown and add TOC. --- _data/toc/php-developer-guide.yml | 3 + .../extension-dev-guide/async-operations.md | 91 ++++++++++--------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/_data/toc/php-developer-guide.yml b/_data/toc/php-developer-guide.yml index 3794ce08d80..27ebe47852e 100644 --- a/_data/toc/php-developer-guide.yml +++ b/_data/toc/php-developer-guide.yml @@ -123,6 +123,9 @@ pages: url: /extension-dev-guide/module-development.html children: + - label: Asynchronous/deferred operations + url: /extension-dev-guide/async-operations.html + - label: Service contracts url: /extension-dev-guide/service-contracts/service-contracts.html diff --git a/guides/v2.3/extension-dev-guide/async-operations.md b/guides/v2.3/extension-dev-guide/async-operations.md index 125f0be8dba..ec624ca7bef 100644 --- a/guides/v2.3/extension-dev-guide/async-operations.md +++ b/guides/v2.3/extension-dev-guide/async-operations.md @@ -1,11 +1,14 @@ -# Asynchronous and deferred operations +--- +group: php-developer-guide +title: Asynchronous and deferred operations +--- Asynchronous operations are not native to PHP but it is still possible to execute heavy operations simultaneously, or delay them until they absolutely have to be finished. - + To make writing asynchronous code easier, Magento provides the `DeferredInterface` to use with asynchronous operations. This allows client code to work with asynchronous operations just as it would with standard operations. - + ## DeferredInterface `_Magento\Framework\Async\DeferredInterface_` is quite simple: @@ -25,12 +28,12 @@ interface DeferredInterface When the client code needs the result, the `_get()_` method will be called to retrieve the result. `_isDone()_` can be used to see whether the code has completed. - + There are 2 types of asynchronous operations where `_DeferredInterface_` can be used to describe the result: * With asynchronous operations in progress, calling `_get()_` would wait for them to finish and return their result. * With deferred operations, `_get()_` would actually start the operation, wait for it to finish, and then return the result. - + Sometimes developers require more control over long asynchronous operations. That is why there is an extended deferred variant - `Magento\Framework\Async\CancelableDeferredInterface`: @@ -52,22 +55,22 @@ interface CancelableDeferredInterface extends DeferredInterface ``` This interface is for operations that may take too long and can be canceled. - + ### Client code -Assumeing that `_serviceA`, `serviceB` and `serviceC_` all execute asynchronous operations, such as HTTP requests, the client code would look like: +Assuming that `_serviceA`, `serviceB` and `serviceC_` all execute asynchronous operations, such as HTTP requests, the client code would look like: ```php public function aMethod() { //Started executing 1st operation $operationA = $serviceA->executeOp(); - + //Executing 2nd operations at the same time $operationB = $serviceB->executeOp2(); //We need to wait for 1st operation to start operation #3 $serviceC->executeOp3($operationA->get()); - + //We don't have to wait for operation #2, let client code wait for it if it needs the result //Operation number #3 is being executed simultaneously with operation #2 return $operationB; @@ -75,15 +78,15 @@ public function aMethod() { ``` And not a callback in sight! - + With the deferred client, the code can start multiple operations at the same time, wait for operations required to finish and pass the promise of a result to another method. - + ## ProxyDeferredFactory When writing a module or an extension, you may not want to burden other developers with having to know that your method is performing an asynchronous operation. There is a way to hide it: `_Magento\Framework\Async\ProxyDeferredFactory_`. With its help, you can return values that seem like regular objects but are in fact deferred results. - + For example: ```php @@ -92,7 +95,7 @@ public function doARemoteCall(string $uniqueValue): CallResult //Async HTTP request, get() will return a CallResult instance. //Call is in progress. $deferredResult = $this->client->call($uniqueValue); - + //Returns CallResult instance that will call $deferredResult->get() when any of the object's methods is used. return $this->proxyDeferredFactory->createFor(CallResult::class, $deferredResult); } @@ -102,7 +105,7 @@ public function doCallsAndProcess(): Result //Both calls running simultaneously $call1 = $this->doARemoteCall('call1'); $call2 = $this->doARemoteCall('call2'); - + //Only when CallResult::getStuff() is called the $deferredResult->get() is called. return new Result([ 'call1' => $call1->getStuff(), @@ -110,42 +113,42 @@ public function doCallsAndProcess(): Result ]); } ``` - + ## Using DeferredInterface for background operations As mentioned above, the first type of asynchronous operations are operations executing in a background. `DeferredInterface` can be used to give client code a promise of a not-yet-received result and wait for it by calling the `_get()_` method. - + Take a look at an example: creating shipments for multiple products: ```php class DeferredShipment implements DeferredInterface { private $request; - + private $done = false; - + private $trackingNumber; - + public function __construct(AsyncRequest $request) { $this->request = $request; } - + public function isDone() : bool { return $this->done; } - + public function get() { if (!$this->trackingNumber) { $this->request->wait(); $this->trackingNumber = json_decode($this->request->getBody(), true)['tracking']; - + $this->done = true; } - + return $this->trackingNumber; } } @@ -153,7 +156,7 @@ class DeferredShipment implements DeferredInterface class Shipping { .... - + public function ship(array $products): array { $shipments = []; @@ -163,7 +166,7 @@ class Shipping $this->client->sendAsync(['id' => $product->getId()]) ); } - + return $shipments; } } @@ -171,48 +174,48 @@ class Shipping class ShipController { .... - + public function execute(Request $request): Response { - $shipments = $this->shipping->ship($this->producs->find($request->getParam('ids'))); + $shipments = $this->shipping->ship($this->products->find($request->getParam('ids'))); $trackingsNumbers = []; foreach ($shipments as $shipment) { $trackingsNumbers[] = $shipment->get(); } - + return new Response(['trackings' => $trackingNumbers]); } } ``` - + Here, multiple shipment requests are being sent at the same time with their results gathered later. If you do not want to write your own `_DeferredInterface_` implementation, you can use `_CallbackDeferred_` to provide callbacks that will be used when `_get()_` is called. - + ## Using DeferredInterface for deferred operations The second type of asynchronous operations are operations that are being postponed and executed only when a result is absolutely needed. - + An example: - + Assume you are creating a repository for an entity and you have a method that returns a singular entity by ID. You want to make a performance optimization for cases when multiple entities are requested during the same request-response process, so you would not load them separately. - + ```php class EntityRepository { private $requestedEntityIds = []; - + private $identityMap = []; ... - + /** * @return Entity[] */ public function findMultiple(array $ids): array { ..... - + //Adding found entities to the identity map be able to find them by ID. foreach ($found as $entity) { $this->identityMap[$entity->getId()] = $entity; @@ -220,12 +223,12 @@ class EntityRepository .... } - + public function find(string $id): Entity { //Adding this ID to the list of previously requested IDs. $this->requestedEntityIds[] = $id; - + //Returning deferred that will find all requested entities //and return the one with $id return $this->proxyDefferedFactory->createFor( @@ -236,20 +239,20 @@ class EntityRepository $this->findMultiple($this->requestedEntityIds); $this->requestedEntityIds = []; } - + return $this->identityMap[$id]; } ) ); } - + .... } class EntitiesController { .... - + public function execute(): Response { //No actual DB query issued @@ -257,7 +260,7 @@ class EntitiesController $criteria2Id = $this->entityService->getEntityIdWithCriteria2(); $criteria1Entity = $this->entityRepo->find($criteria1Id); $criteria2Entity = $this->entityRepo->find($criteria2Id); - + //Querying the DB for both entities only when getStringValue() is called the 1st time. return new Response( [ @@ -268,7 +271,7 @@ class EntitiesController } } ``` - + ## Examples in Magento -Please see our asynchronous HTTP client` _Magento\Framework\HTTP\AsyncClientInterface_` and `_Magento\Shipping\Model\Shipping_` with various `_Magento\Shipping\Model\Carrier\AbstractCarrierOnline_` implementations to see how `DeferredInterface` can be used to work with asynchronous code. +Please see our asynchronous HTTP client `_Magento\Framework\HTTP\AsyncClientInterface_` and `_Magento\Shipping\Model\Shipping_` with various `_Magento\Shipping\Model\Carrier\AbstractCarrierOnline_` implementations to see how `DeferredInterface` can be used to work with asynchronous code. From 3cc2053d25bc5dfb2299d8d0560bd66abf82721a Mon Sep 17 00:00:00 2001 From: Donald Booth Date: Tue, 23 Jul 2019 14:46:23 -0500 Subject: [PATCH 4/4] Added version num --- _data/toc/php-developer-guide.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_data/toc/php-developer-guide.yml b/_data/toc/php-developer-guide.yml index 27ebe47852e..2b89bc370c3 100644 --- a/_data/toc/php-developer-guide.yml +++ b/_data/toc/php-developer-guide.yml @@ -124,8 +124,9 @@ pages: children: - label: Asynchronous/deferred operations + include_versions: ["2.3"] url: /extension-dev-guide/async-operations.html - + - label: Service contracts url: /extension-dev-guide/service-contracts/service-contracts.html