Skip to content

Commit 7fa6e00

Browse files
author
Oleksandr Gorkun
committed
deferred
1 parent eb6cb98 commit 7fa6e00

File tree

1 file changed

+70
-139
lines changed

1 file changed

+70
-139
lines changed

design-documents/promises.md

Lines changed: 70 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,62 @@
11
### Why?
22
Service contracts in the future will often be executed in an asynchronous manner
3-
and it's time to introduce a standard Promise to Magento for asynchronous operations to employ
3+
and it's time to introduce a standard Promise to Magento for asynchronous operations to employ.
4+
Also operations like sending HTTP requests can be easily performed asynchronously since cUrl multi can be utilized
5+
to send requests asynchronously.
46
### Requirements
5-
* Promises CANNOT be forwarded with _then_ and _otherwise_ ([see explanation](#forwarding))
6-
* _then_ accepts a function that will be executed when the promise is resolved, the callback will
7-
receive a single argument - result of the execution
8-
* _otherwise_ accepts a function that will be executed if an error occurs during the asynchronous
9-
operation, it will receive a single argument - a _Throwable_
10-
* Promises can be used in a synchronous way to prevent methods that use methods returning promises
11-
having to return a promise as well ([see explanation](#callback-hell)); This will be done by promises having _wait_ method
12-
* _wait_ method does not throw an exception if the promise is rejected
13-
nor does it return the result of the resolved promise ([see explanation](#wait-not-unwrapping-promises))
14-
* If an exception occurs during an asynchronous operation and no _otherwise_ callback is
15-
provided then it will just be rethrown
7+
* Avoid callbacks that cause noodle code and generally alien to PHP
8+
* Introduce a way to work with asynchronous operations in a familiar way
9+
* Employ solution within core code to serve as an example for our and 3rd party developers
1610
### API
17-
##### Promise to be used by client code
18-
When client code receives a promise from calling another object's method
19-
it shouldn't have access to _resolve_ and _reject_ methods, it should only be able to
20-
provide callbacks to process promised results and to wait for promised operation's execution.
11+
##### Deferred
12+
A future that describes a values that will be available later.
13+
If a library returns a promise or it's own implementation of a future it can be easily wrapped to support our interface.
2114

2215
This interface will be used as the return type of methods returning promises.
2316
```php
24-
interface PromiseInterface
17+
interface DeferredInterface
2518
{
2619
/**
27-
* @throws PromiseProcessedException When callback was alredy provided.
20+
* Wait for and return the value.
21+
*
22+
* @return mixed Value.
23+
* @throws \Throwable When it was impossible to get the value.
2824
*/
29-
public function then(callable $callback): void;
30-
25+
public function get();
26+
3127
/**
32-
* @throws PromiseProcessedException When callback was alredy provided.
28+
* Is the process of getting the value is done?
29+
*
30+
* @return bool
3331
*/
34-
public function otherwise(callable $callback): void;
35-
36-
public function wait(): void;
37-
}
38-
```
39-
##### Promise to be created
40-
This promise will be created by asynchronous code
41-
```php
42-
interface ResultPromiseInterface extends PromiseInterface
43-
{
44-
public function resolve($value): void;
45-
46-
public function reject(\Throwable $exception): void;
32+
public function isDone(): bool;
4733
}
4834
```
4935

5036
### Implementation
51-
A wrapper around [Guzzle Promises](https://github.com/guzzle/promises) will be created to implement the APIs above. Guzzle Promises fit
52-
most important criteria - they allow synchronous execution as well as asynchronous. It's a mature
53-
and well-known library and, while we would have to add guzzle/promises to our composer.json,
54-
the library is already required in Magento indirectly - we won't be actually adding a new dependency.
55-
56-
There are other libraries like [Reactphp Promises](https://github.com/reactphp/promise) but they either do not provide synchronous way
57-
to interact with promises or are not as refined.
37+
This interface will be used as a wrapper for libraries that return promises and deferred values.
5838

5939
### Explanations
60-
##### Forwarding
61-
Consider this code
62-
```php
63-
$promise = $this->anotherObject->doStuff();
64-
$promise->then($doStuffCallback)
65-
->otherwise($processErrorCallback)
66-
->otherwise($processAnotherError)
67-
->then($doOtherStuff)
68-
->otherwise($processErrorCallback);
69-
```
70-
Does 1st _then_ return a forwarded promise? Or is it the same object?
71-
Then what promise is the second _otherwise_ callback for?
72-
Code looking like this is confusing and it will be much cleaner if we don't use forwarding.
73-
```php
74-
$doStuffOperation = $this->someObject->doStuff();
75-
$result = null;
76-
$doStuffOperation->then(function ($response) use (&$result) { $result = $response; });
77-
$simultaneousOperation = $this->otherObject->processStuff();
78-
$doStuffOperation->wait();
79-
$updated = null;
80-
$saveOperation = $this->repo->save($result);
81-
$saveOperation->then(function ($result) use (&$updated) { $updated = $result; });
82-
83-
//Waiting for all
84-
$saveOperation->wait();
85-
$simultaneousOperation->wait();
86-
return $updated;
87-
```
88-
Here we clearly state that for _save operation_ we need to _do stuff operation_ to finish
89-
and _simultaneous operation_ may run up until then end of our algorithm execution.
40+
##### Why not promises?
41+
Promises mean callbacks. One callback is fair enough but multiple callbacks within the same method, callbacks for forwarded
42+
promises create noodle-like hard to support code. Closures are a part of PHP but still are a foreign concept complicating
43+
developer experience. Also it is an extra effort to ensure strict typing of return values and arguments with anonymous
44+
functions.
9045

91-
92-
##### Callback hell
93-
Consider this code responsible for placing orders
94-
```php
95-
class ServiceA
96-
{
97-
....
98-
99-
public function process(DTOInterface $dto): ProcessedInterface
100-
{
101-
....
102-
103-
$this->serviceB->processSmth($val)->then(function ($val) use ($processed) {
104-
$processed->setBValue($val);
105-
});
106-
107-
return $processed;
108-
}
46+
Other thing is that promises are meant to be forwarded which complicates things. It can be hard to understand what are
47+
you writing a callback for - promised result? Another callback for promised result introduced earlier? OnFulfilled callback
48+
for resolved value in a OnRejected callback to the initial promise?
10949

110-
....
111-
}
112-
```
113-
We cannot be sure _serviceB_ has finished doing it's stuff when we return _$processed_.
114-
So, if we cannot wait for the promise _serviceB_ returned, the only thing we can do
115-
is to return a promise ourselves instead of _ProcessedInterface_. But then methods using
116-
_ServiceA::process()_ would have to do the same - and PHP code is not supposed to be this way.
50+
##### Typing
51+
Methods returning Deferred can still provide types for their actual returned values - they can extend the original interface
52+
and add return type hint to the _get()_ method.
11753

118-
##### Wait not unwrapping promises
119-
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.
120-
121-
Consider next situation:
122-
```php
123-
class ServiceA
124-
{
125-
public function doSmth(): void
126-
{
127-
$promise = $this->otherService->doSmthElse();
128-
$promise->otherwise(function ($exception) { $this->logger->critical($exception); });
129-
try {
130-
//wait would throw the exception processed in the otherwise callback once again
131-
$promise->wait();
132-
} catch (\Throwable $exception) {
133-
//we've already processed this
134-
}
135-
}
136-
}
137-
```
138-
That is a simple situation but it illustrates how having multiple ways of receiving promised results may lead to duplicating code
54+
##### Advantage
55+
Since deferred does not require any confusing callbacks and forwarding it's pretty easy to just treat it as a values
56+
and only calling _get()_ when you actually need it. Client code will look mostly like it's just a regular synchronous code.
13957

140-
### Using promises for service contracts
141-
#### Why use promises for service contracts?
58+
### Using Deferred for service contracts
59+
#### Why use futures for service contracts?
14260
Another way that was proposed to execute service contracts in an asynchronous manner was to use async web API, but there
14361
are number of problems with that approach:
14462
* 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:
15169
for each operation
15270

15371
So to allow execution of multiple service contracts from different domains it's best to send 1 request per operation
154-
and to let client code to chain, pass and properly receive promises of results of operations.
72+
and to let client code to work with asynchronously received values almost as they would've with synchronous ones.
15573

15674
#### How will it look?
157-
There are to ways we can go about using promises for asynchronous execution of service contracts:
158-
* Service interfaces themselves returning promises for client code to use
159-
75+
There are to ways we can go about using Deferred for asynchronous execution of service contracts:
76+
* Service interfaces themselves returning deferred values for client code to use
77+
78+
_contract's deferred_:
79+
```php
80+
interface DTODeferredInterface extends DeferredInterface
81+
{
82+
/**
83+
* @inheritDoc
84+
* @return DTOInterface
85+
*/
86+
public function get(): DTOInterface;
87+
}
88+
```
89+
16090
_service contract_:
16191
```php
16292
interface SomeRepositoryInterface
16393
{
164-
public function save(DTOInterface $data): PromiseInterface;
94+
public function save(DTOInterface $data): DTODeferredInterface;
16595
}
16696
```
16797

@@ -179,28 +109,28 @@ There are to ways we can go about using promises for asynchronous execution of s
179109
....
180110

181111
//Both operations running asynchronously
182-
$promise = $this->someRepo->save($dto);
183-
$anotherPromise = $this->someService->doStuff();
184-
//Waiting for both results
185-
$promise->wait();
186-
$anotherPromise->wait();
112+
$deferredDTO = $this->someRepo->save($dto);
113+
$deferredStuff = $this->someService->doStuff();
114+
//Started both processes at the same time, waiting for both to finish
115+
$dto = $deferredDTO->get();
116+
$stuff = $deferredStuff->get();
187117
}
188118
}
189119
```
190-
* Using a runner that will accept interface name, method name and arguments that will return a promise
120+
* Using a runner that will accept interface name, method name and arguments that will return a deferred
191121

192122
_async runner_:
193123
```php
194124
interface AsynchronousRunnerInterface
195125
{
196-
public function run(string $serviceName, string $serviceMethod, array $arguments): PromiseInterface;
126+
public function run(string $serviceName, string $serviceMethod, array $arguments): DeferredInterface;
197127
}
198128
```
199129
_regular service_:
200130
```php
201131
interface SomeRepositoryInterface
202132
{
203-
public function save(DTOInterface $dto): void;
133+
public function save(DTOInterface $dto): DTOInterface;
204134
}
205135
```
206136
_client code_:
@@ -222,18 +152,19 @@ There are to ways we can go about using promises for asynchronous execution of s
222152
....
223153

224154
//Both operations running asynchronously
225-
$promise = $this->runner->run(SomeRepositoryInterface::class, 'save', [$dto]);
226-
$anotherPromise = $this->runner->run(SomeServiceInterface::class, 'doStuff', []);
227-
//Waiting for both results
228-
$promise->wait();
229-
$anotherPromise->wait();
155+
$deferredDTO = $this->runner->run(SomeRepositoryInterface::class, 'save', [$dto]);
156+
$deferredStuff = $this->runner->run(SomeServiceInterface::class, 'doStuff', []);
157+
//Started both processes at the same time, waiting for both to finish
158+
$dto = $deferredDTO->get();
159+
$stuff = $deferredStuff->get()
230160
}
231161
}
232162
```
233163

234-
### Using promises for existing code
164+
### Using deferred for existing code
235165
We have a standard HTTP client - Magento\Framework\HTTP\ClientInterface, it can benefit from allowing async requests
236-
functionality for developers to use by employing promises.
166+
functionality for developers to use by employing promises. Since it's an API, and a messy one at that, we should create
167+
a new asynchronous client.
237168

238169
This client is being used in Magento\Shipping\Model\Carrier\AbstractCarrierOnline to create package shipments/
239170
shipment returns in 3rd party systems, the process can be optimized by sending requests asynchronously to create

0 commit comments

Comments
 (0)