Skip to content
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

Lazy services - service proxies #7527

Closed
wants to merge 21 commits into from

Conversation

Ocramius
Copy link
Contributor

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #6140 (supersedes) #5012 #6102 (maybe)
License MIT (attached code) - BSD-3-Clause (transitive optional dependency)
Doc PR Please pester me to death so I do it

This PR introduces lazy services along the lines of zendframework/zendframework#4146

It introduces an OPTIONAL dependency to ProxyManager and transitively to "zendframework/zend-code": "2.*".

Lazy services: why? A comprehensive example

For those who don't know what this is about, here's an example.

Assuming you have a service class like following:

class MySuperSlowClass
{
    public function __construct()
    {
        // inject large object graph or do heavy computation
        sleep(10);
    }

    public function doFoo()
    {
        echo 'Foo!';
    }
}

The DIC will hang for 10 seconds when calling:

$container->get('my_super_slow_class');

With this PR, this can be avoided, and the following call will return a proxy immediately.

$container->getDefinitions('my_super_slow_class')->setLazy(true);
$service = $container->get('my_super_slow_class');

The 10 seconds wait time will be delayed until the object is actually used:

$service->doFoo(); // wait 10 seconds, then 'Foo!'

A more extensive description of the functionality can be found here.

When do we need it?

Lazy services can be used to optimize the dependency graph in cases like:

  • Webservice endpoints
  • Db connections
  • Objects that cause I/O in general
  • Large dependency graphs that are not always used

This could also help in reducing excessive service location usage as I've explained here.

Implementation quirks of this PR

There's a couple of quirks in the implementation:

  • Symfony\Component\DependencyInjection\CompilerBuilder#createService is now public because of the limitations of PHP 5.3
  • Symfony\Component\DependencyInjection\Dumper\PhpDumper now with extra mess!
  • The proxies are dumped at the end of compiled containers, therefore the container class is not PSR compliant anymore

Todos

  • Fix license compatibility
  • Fix: real instantiation of the service swaps the registered instance in the DIC
  • Generate runtime proxies via eval
  • Generate proxies in the compiled DIC
  • 5.3 compatibility
  • Check all links to ProxyManager to ensure the dependency is completely optional
  • Handle disabled eval case
  • Add class resources to handle changes in definitions

@Crell
Copy link
Contributor

Crell commented Mar 30, 2013

@jrobeson pointed me here.

The functionality definitely sounds valuable. I know Lukas has been talking about this for a long time. However, the dependency chain here is not fun.

I cannot speak for Fabien, but I'd be wary of adding a dependency on 2 additional libraries to DependencyInjection just for this functionality. Even that aside, the cg-library package is licensed under Apache 2. That's a problem. Specifically, it means that the DI Component cannot run without Apache 2 code, which in turn means that it has the same impact license-wise as being Apache 2. That is, it becomes incompatible with GPLv2 and GPLv2+ code.

While Symfony itself is not GPL licensed, many of the systems that are now depending on it are. In particular, Drupal and phpBB are both GPLv2+ licensed. GPLv2 is incompatible with Apache 2. That would make the DI component, and by extension Symfony fullstack, unusable for both Drupal and phpBB, as well as likely others. (I don't know the license of any of the other Symfony-based systems off hand.) It also makes it unsuable for bespoke systems by shops that use GPLv2 (and there are a lot of them) that want to build off of Symfony2.

So, yeah. Introducing an Apache2 dependency would be a massive license-breaking and ecosystem-crippling change. :-/ jrobeson indicated that there was an alternative library, or that the cg-library could be relicensed if we convince the maintainer to do so. Either way the licensing issue would go away. The question of introducing additional dependencies I leave to fabpot.

Disclaimer: I'm the former Director of Legal Affairs for the Drupal Association, so I've spent a fair bit of time looking into this subject. I'm not just making it up. :-)

@Ocramius
Copy link
Contributor Author

@Crell I've been told that today, and didn't reallly expect it (I have no idea whatsoever this involves, I write code :( ). If BSD-3-Clause is compliant, I can convert my library to use Zend\Code instead. That would take me a couple of days, but is no problem at all.

@Ocramius
Copy link
Contributor Author

@Crell I removed usage of cg-lib in ProxyManager as of Ocramius/ProxyManager#18 - should be ok now

@lyrixx
Copy link
Member

lyrixx commented Mar 30, 2013

Do you have any benchmark on a "classic" Symfony2 application ? (without sleep ;) ).

@Ocramius
Copy link
Contributor Author

@lyrixx no, I actually don't use Symfony SE. You can actually benchmark it yourself by insulating a portion of your object graph that is not used and marking the root node of that portion as lazy. From what I know, the security component should be quite large.

Another thing this tries to remove is excessive usage of service location for performance optimizations, as I've explained here.

You can also read a detailed description of how lazy loading proxies (as implemented here) work here

OOTB, as it is now, this PR does not introduce any overhead.

@@ -861,8 +864,24 @@ public function findDefinition($id)
* @throws RuntimeException When the service is a synthetic service
* @throws InvalidArgumentException When configure callable is not callable
*/
private function createService(Definition $definition, $id)
public function createService(Definition $definition, $id, $tryProxy = true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to update the phpdoc

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And you should add @internal in the phpdoc to explain that it is only public because of the internal use in a closure, and should not be called by other code

@stof
Copy link
Member

stof commented Mar 30, 2013

Your current implementation breaks on 1 point: recreating the proxy class when the original class changes. The class has to be added as a container resource for lazy services so that the container is dumped again when the class changes, so that the proxies are dumped again. Otherwise, the proxy might not match the class anymore.

What you would need is the logic of ContainerBuilder::addObjectResource but with a class name rather than an instance (which is easy as addObjectResource could then be rewritten as $this->addClassResource(get_class($object))

@stof
Copy link
Member

stof commented Mar 30, 2013

For the ContainerBuilder case, using eval may be better than creating a temporary file on the fly and requiring it. You don't gain anything about caching the generated code (as you regenerate it each time) except potential race conditions (see Doctrine considering to change the generation of proxies when using auto_generate_proxies).
Btw, Twig is already using eval when you disable the template cache.

@stof
Copy link
Member

stof commented Mar 30, 2013

Your code has a big issue: it creates a new proxy instance each time the service is retrieved from the container instead of reusing the same instance for container-scoped services. So $container->get('router') !== $container->get('router') when marking it as lazy.

@stof
Copy link
Member

stof commented Mar 30, 2013

thus, getting the lazy service, then doing a method call (thus initializing the proxy) and then getting it again would give you the wrapped instance after that, being inconsistent.

@Ocramius
Copy link
Contributor Author

@stof very good catch! Couldn't write a for that test because of the conflicting container names. I'll see if I can change the name of a generated container name and write something that covers that.

@mvrhov
Copy link

mvrhov commented Mar 31, 2013

Also note, that the usage of eval may make the symfony2 projects that use lazy loading unusable on some shared hosts.

@Ocramius
Copy link
Contributor Author

@mvrhov ouch! Do they turn it off? I can build a switch for that eventually if there's a way of recognizing it.

@mvrhov
Copy link

mvrhov commented Mar 31, 2013

yeah. I worked for one a few years ago and we were disabling it. The problem was that that a large amount of code injections that were happening then abused eval to send a spam or worse. Trying to get unlisted from the sites that list spam sending IPs every few weeks also become troublesome.
There is disable_functions ini setting where all disabled functions a re listed.

@Ocramius
Copy link
Contributor Author

@mvrhov I spawned Ocramius/ProxyManager#24 from that. It should not be handled in symfony itself.

@Ocramius
Copy link
Contributor Author

@stof can you clarify when I should add a resource? I added addClassResource to the API, but now I'm wondering about what should trigger it. Should I do it in ContainerBuilder#register? Are there other locations where definitions are injected?

@jalliot
Copy link
Contributor

jalliot commented Mar 31, 2013

The feature by itself would be a great addition but I agree with @Crell that the added dependencies are not so good.
Being able to reuse the CG library (if license compliant of course) would be a little bit better as it is already used by some Symfony2 bundles.

Besides, the generated class names of proxies should follow the naming convention used by both the CG library and Doctrine (with a __CG__ prefix and all, see ClassUtils) as it allows easily identifying proxies, wherever they come from, without having to test for all possible proxies interfaces out there.

@Ocramius
Copy link
Contributor Author

To clarify, I modified the PR so that the dependencies are now optional.

@jalliot for CG Lib, I kicked that out and replaced it with Zend\Code, which is BSD-3-clause and therefore compatible. That's quite final, since I don't want to get back and rewrite this backwards without any real benefit.

That's anyway stuff that doesn't affect this PR's code directly. SF2 just benefits from the abstraction layer of ProxyManager and should not really care about how that is achieved except for BC breaks, performance and security issues (and license compatibility, as introduced by @Crell).

For __CG__ I explicitly wanted to use __PM__ (or something different) because ProxyManager allows to hook in its own autoloader with its own rules depending on the naming strategy set in the proxy manager configuration. If that autoloader conflicts with the one in doctrine common, we have a problem. We don't want that, do we? :)

If you want to identify a ProxyManager proxy, you can use the utility within ProxyManager itself. An idea would be to drop the __PM__ constant from there so that you're forced to use the inflector instance and don't rely on a convention anymore.

use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
use ProxyManager\Proxy\LazyLoadingInterface;
use ReflectionClass;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symfony does not add use statements for classes in the global namespace

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will revert :)

@lsmith77
Copy link
Contributor

lsmith77 commented Apr 3, 2013

its an optional dependency but i agree that adding 2 more dependencies can be a concern. doctrine proxy and the cg lib are commonly used already in the Symfony2 ecosystem while proxy manager and zend code are less used.

i didnt check but ideally the code should maybe gracefully ignore the lazy setting in case the dependencies are missing so that no code would break if those optional deps are not installed.

@Ocramius
Copy link
Contributor Author

Ocramius commented Apr 3, 2013

@lsmith77 perfect, I will add the code required to let any lazy markers being ignored in the case where dependencies are not loaded.

*
* @api
*/
public function addClassResource(\ReflectionClass $class)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not passing a class name and building the ReflectionClass internally ? addObjectResource expects an object, not a ReflectionObject

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strict typing. This disallows unexisting classes upfront

@Ocramius
Copy link
Contributor Author

Still need to fix the 5.3 failures

@@ -11,6 +11,9 @@

namespace Symfony\Component\DependencyInjection\Dumper;

use ProxyManager\Generator\ClassGenerator;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again here, can we abstract this against an interface to not violate the DIP? This dependency is optional, but the code makes it "required".

@beberlei
Copy link
Contributor

To clarify my comments above, the proxy manager is an optional dependency and should therefore be hidden in an adapter, maybe even in its own Bridge, implementing a generic interface from DependencyInjection component

@Ocramius
Copy link
Contributor Author

Agreed - fixing later today

@Ocramius Ocramius mentioned this pull request Apr 30, 2013
@Ocramius
Copy link
Contributor Author

I've opened #7890 with the suggestions of @beberlei

@fabpot
Copy link
Member

fabpot commented May 1, 2013

closing in favor of #7890

@fabpot fabpot closed this May 1, 2013
fabpot added a commit that referenced this pull request May 6, 2013
This PR was squashed before being merged into the master branch (closes #7890).

Discussion
----------

ProxyManager Bridge

As of @beberlei's suggestion, I re-implemented #7527 as a new bridge to avoid possible hidden dependencies.

Everything is like #7527 except that the new namespace (and possibly package/subtree split) `Symfony\Bridge\ProxyManager` is introduced

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #6140 (supersedes) #5012 #6102 (maybe) #7527 (supersedes)
| License       | MIT (attached code) - BSD-3-Clause (transitive dependency)
| Doc PR        | Please pester me to death so I do it

This PR introduces lazy services along the lines of zendframework/zendframework#4146

It introduces an **OPTIONAL** dependency to [ProxyManager](https://github.com/Ocramius/ProxyManager) and transitively to [`"zendframework/zend-code": "2.*"`](https://github.com/zendframework/zf2/tree/master/library/Zend/Code).

## Lazy services: why? A comprehensive example

For those who don't know what this is about, here's an example.

Assuming you have a service class like following:

```php
class MySuperSlowClass
{
    public function __construct()
    {
        // inject large object graph or do heavy computation
        sleep(10);
    }

    public function doFoo()
    {
        echo 'Foo!';
    }
}
```

The DIC will hang for 10 seconds when calling:

```php
$container->get('my_super_slow_class');
```

With this PR, this can be avoided, and the following call will return a proxy immediately.

```php
$container->getDefinitions('my_super_slow_class')->setLazy(true);
$service = $container->get('my_super_slow_class');
```

The 10 seconds wait time will be delayed until the object is actually used:

```php
$service->doFoo(); // wait 10 seconds, then 'Foo!'
```

A more extensive description of the functionality can be found [here](https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md).

## When do we need it?

Lazy services can be used to optimize the dependency graph in cases like:

 * Webservice endpoints
 * Db connections
 * Objects that cause I/O in general
 * Large dependency graphs that are not always used

This could also help in reducing excessive service location usage as I've explained [here](http://ocramius.github.com/blog/zf2-and-symfony-service-proxies-with-doctrine-proxies/).

## Implementation quirks of this PR

There's a couple of quirks in the implementation:

 * `Symfony\Component\DependencyInjection\CompilerBuilder#createService` is now public because of the limitations of PHP 5.3
 * `Symfony\Component\DependencyInjection\Dumper\PhpDumper` now with extra mess!
 * The proxies are dumped at the end of compiled containers, therefore the container class is not PSR compliant anymore

Commits
-------

78e3710 ProxyManager Bridge
@Ocramius Ocramius deleted the feature/lazy-services branch December 1, 2021 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants