0

I'm doing POC implementation of async http requests in PHP. The symfony http client works well when we retrieve response data following way:

$response1 = $httpClient->request('GET', 'https://127.0.0.1:8000/service/a');
$response2 = $httpClient->request('GET', 'https://127.0.0.1:8000/service/b');

$array1 = $response1->toArray();
$array2 = $response2->toArray();

Considering both services A and B take 5 seconds to execute, our client code will have to wait only 5 seconds totally.

Now, the problem is that usually we write different repositories for different clients (RepositoryA, RepositoryB). Hence, in client code we won't operate on simple HttpResponseInterface object, but on some DTO instead.

I'd like to take advantage of async http requests, while retaining the repositories separation.

The simplified approach looks like this:

private function getResponse1(): Generator
{
    $response1 = $this->httpClient->request('GET', 'https://127.0.0.1:8000/service/a');

    return yield $response1->toArray();
}

private function getResponse2(): Generator
{
    $response2 = $this->httpClient->request('GET', 'https://127.0.0.1:8000/service/b');

    return yield $response2->toArray();
}

$response1 = $this->getResponse1();
$response2 = $this->getResponse2();

$array1 = $response1->current();
$array2 = $response2->current();

If you are familiar with JS, this current() call on Generator object is somewhat similar to await construct.

Finally, the question is why doesn't this approach work? Since measurements show that total time is 10 seconds of waiting with generators, while only 5 seconds without them.

Here's the Console command, which may be used to reproduce the issue: https://github.com/rela589n/generator-as-promise-poc/blob/master/src/Command/TestSyncSfClientGeneratorsCommand.php

Ans also there's a working example of code not using generators: https://github.com/rela589n/generator-as-promise-poc/blob/master/src/Command/TestAsyncSfHttpClientCommand.php

Barmar
  • 741,623
  • 53
  • 500
  • 612
rela589n
  • 817
  • 1
  • 9
  • 19
  • 1
    Since `current()` is like `await`, it waits for the generator to yield something, so this is no better than the original. – Barmar Aug 17 '23 at 19:59
  • You also seem to be conflating async with parallel. PHP is single-threaded and cannot get anything ready "in the background". Generators simply allow us to more efficiently arrange work and spread "overhead" time costs across the whole execution, rather than being an up-front delay, and also let us save on RAM by not having to return sets as full arrays. Yielding a single result from a generator is the same as a regular function call, just with extra steps. – Sammitch Aug 17 '23 at 20:27

1 Answers1

0

The problem is likely that the first yield of your generator is the total result:

    return yield $response1->toArray();

(apart from that return yield is somewhat bogus now as I pasted it in...)

Now the generator that is also doing the resource acquistion:

    $response1 = $this->httpClient->request('GET', 'https://127.0.0.1:8000/service/a');

right in front of the returning yield, it means that the generator will wait for this request to finish.

same for the second generator.

Therefore, when you distribute both requests in two different generators, you're loosing the ability to invoke them in parallel (as in after each other) as you already make - via the generator implementation - the generator waiting for the first yield.

Therefore, fake the first yield and work with the return construction.

private function getResponse2(): Generator
{
    $response2 = $this->httpClient->request('GET', 'https://127.0.0.1:8000/service/b');

    yield $response2; # first current() (implicit rewind()) stops here after invocation.

    return yield $response2->toArray(); # second current() fetches the result
}

This could probably work, however it remains still unclear to me what toArray() actually does as I don't known that library.

However, in regard to the PHP Generator implementation, it is that it always fully initializes, which is why it can't rewind and needs to acquire the first yield result.

Therefore the first yield must be the non-blocking result from the http-client-request protocol.

hakre
  • 193,403
  • 52
  • 435
  • 836