Skip to content
This repository has been archived by the owner on Jan 8, 2020. It is now read-only.

[ZF3] [RFC] Embracing stateless/immutable state #5599

Closed
1 of 9 tasks
Ocramius opened this issue Dec 10, 2013 · 29 comments
Closed
1 of 9 tasks

[ZF3] [RFC] Embracing stateless/immutable state #5599

Ocramius opened this issue Dec 10, 2013 · 29 comments

Comments

@Ocramius
Copy link
Member

ZF's slowness and bootstrapping

The problem of ZF2's performance is mainly related with having a huge "bootstrap" step (loading modules, configuring the service manager, instantiating core services).

Today, I got back at looking at OcraHopHop after looking into @rdlowrey's amazing job in building Aerys (not yet released), which is basically the kickass nonblocking stuff of node.js minus the horror of javascript (that is my take on it, feel free to insult me :) ).

Avoiding bootstrap

The idea is simple: run PHP without a web server as an own http service. Something like:

./run-my-application.php --port=80 &

This could work with any PHP application, as long as we embrace immutable state.

OcraHopHop worked fine with this concept, and I obtained impressive results with an 80% or faster ZF2 application, but there are some limitations related to state, which prevents asynchronous operations and eventually threading.

What "stateless" means

For those who don't have a clear mind on what I mean, this is what a stateless application should look like:

public function testApplicationIsStateless()
{
    $application = $this->bootstrapApplication();

    $application->run();

    $serialized = serialize($application);

    $application->run();

    $this->assertSame(
        $serialized,
        serialize($application),
        'Ensure that state in the application has not changed after the first run'
    );
}

This is just pseudo-code, and of course we cannot serialize an application since it contains un-serializable objects, but you get the point.

You can read up more about what I mean with "stateless" on @igorw's blog

What the problem is

Try it for yourself! Try running OcraHopHop against the normal "Hello World" skeleton application example - it will slow down after a dozen requests, and the output will change at every request (more and more broken at every iteration).

That is because ZF2 internals are not immutable. We have to get rid of that.

TODOs

This issue is mainly here to:

  • collect thoughts on this approach
  • find downsides in this approach
  • identify components that retain state
  • track PRs and issues about making those components stateless or immutable

Identified components with problems so far

  • the router component instantiates different routes depending on request type
  • the view manager is instantiated depending on the request type
  • view helpers are not stateless (make view helper manager un-shared?)
  • request, response and mvcevent should not be stored in the application object
  • ...

I don't yet have a list of components that retain state, but so far I can tell that a lot of plugins in the view layer keep an internal state, as well as the Request object (which is a service, see this page again on why we should NOT do that!), the Response object, the MvcEvent and other stuff that should not be available via getters.

@devosc
Copy link
Contributor

devosc commented Dec 10, 2013

In ZF2 the Request and Response objects from the ServiceManager, I didn't change or introduce that.

@Ocramius
Copy link
Member Author

@devosc yes, and we should change that

@Slamdunk
Copy link
Contributor

👍

1 similar comment
@texdc
Copy link
Contributor

texdc commented Dec 12, 2013

👍

@cdekok
Copy link
Contributor

cdekok commented Dec 15, 2013

Interesting how about parts that need to remain a state like everything with a session?

@Ocramius
Copy link
Member Author

@mech7 they should be created and discarded during Application::run(). For example by making the service non-shared

@ronan-gloo
Copy link
Contributor

If think this is potentially the most fruitful RFC for ZF3.
Most benefits of the approach are widely explained in links you provide. The obvious downside is a large re-think / re-write of the Zf's Mvc part.

@Ocramius
Copy link
Member Author

@ronan-gloo the MVC is actually not that big problem. Areas that are hairy are:

  • some services that are not stateless
  • the view layer and its helpers, which are not stateless by default (for example things like $this->headScript()->addBlaBla())
  • moving all the state transitions to the MVCEvent only
  • removing some getters from the application object

Otherwise it looks already really good for basic usage: by excluding the view layer entirely (using the JsonRenderer, for example) you can get quite impressive results already, but you cannot get any parallelism (if using pthreads) yet.

@ronan-gloo
Copy link
Contributor

I'm just thinking about routing stuffs: Actually, the router, which is a shared service, is created depending on request (console / http), which means, if i understood you well, this part should becomes, in stateless and actual implementation perspective, a non-shared service.

While we are, in mostly apps we wrote with Zf2, creating some shared services depending on request (route, verb, and so on), it have some consequences in the userland code.

Parallelism is one of the great goal of this approach, by scoping some state-full services in request life-cycle.
By the way, this is an interesting think about where / how must be the frontier of those scopes.
I'm gonna to try HopHop for experiments :)

@Ocramius
Copy link
Member Author

I'm just thinking about routing stuffs: Actually, the router, which is a shared service, is created depending on request (console / http), which means, if i understood you well, this part should becomes, in stateless and actual implementation perspective, a non-shared service.

Yes, the router should not be instantiated depending on the request, but should instead fetch the correct instance to dispatch against at runtime (pseudo - ignore CS):

class BaseRoute implements RouteInterface {
    public function __construct(HttpRoute $httpRoute, ConsoleRoute $consoleRoute) { /* ... */ }
    public function match(Request $request) {
        if ($request instanceof HttpRequest) { return $this->httpRoute->match($request); }
        if ($request instanceof ConsoleRequest) { return $this->consoleRoute->match($request); }
        throw new Nope();
    }
}

While we are, in mostly apps we wrote with Zf2, creating some shared services depending on request (route, verb, and so on), it have some consequences in the userland code.

Yes, that is a common mistake, and we can't really fix it in userland's apps. What we can do is providing some base tests that show how broken an app could become.

By the way, this is an interesting think about where / how must be the frontier of those scopes.
I'm gonna to try HopHop for experiments :)

I am not sure if OcraHopHop still runs, but I achieved impressive results with it running as a simple JSON REST-ish service.

@devosc
Copy link
Contributor

devosc commented Feb 14, 2014

Sort of defeats the purpose of lazy loading?

@texdc
Copy link
Contributor

texdc commented Feb 14, 2014

While we are, in mostly apps we wrote with Zf2, creating some shared services depending on request (route, verb, and so on), it have some consequences in the userland code.

Yes, that is a common mistake, and we can't really fix it in userland's apps. What we can do is providing some base tests that show how broken an app could become.

Documentation, documentation, documentation. (this is how it's done ... not like this)

Poor documentation should not be a monetezation scheme (e.g. paid training, certifications, support).

@Ocramius
Copy link
Member Author

Sort of defeats the purpose of lazy loading?

Lazy loading is not required in that scenario

Poor documentation should not be a monetezation scheme (e.g. paid training, certifications, support).

Never been like that. Yes, a test case running multiple dispatches and verifying results is worth writing. Also OT.

@devosc
Copy link
Contributor

devosc commented Feb 14, 2014

We use the shared service indicator so that we can have one service manager for the entire application. However I think in order to ensure scope correctly, each component should have their own service manager; but this becomes problematic from a configuration point of view, hence there is one manager that supports the shared flag. I don't think it would be feasible to achieve that type of perfection - most applications really wouldn't need it (and those components could be their own applications on different machines). And that is only one part of the problem.

If the service manager was configured with instances, instead of lazy loading them, then it should be possible to achieve a stateless system as you described; anything not immutable would be passed as parameters. That would be hope anyway. Anything further down the chain would need to have a listener further up to be able to capture that dependency and inject, e.g. the view url help would need a listener for the RouteMatch event to inject itself with.

@Ocramius
Copy link
Member Author

What you are talking about is using a real injector instead of a container. That doesn't make much of a difference in my opinion, but it surely mitigates the risk of having mutable objects all around the place.

It is possible to build a stateless system even with the current lazy loading approach, we'd just need to remove the "shared" flag for all these fake services as a first step (and then see what breaks, because it will break).

Perfection is not a requirement - you will never reach it anyway. It may be a good use case for revamping Zend\Di, but honestly, I don't see who would have the time to work on it right now.

@Ocramius
Copy link
Member Author

I've added some of the discussed flaws to the todo list - please add more bullet points if you think you found one.

@devosc
Copy link
Contributor

devosc commented Feb 14, 2014

I wasn't able to not share the Config, Event Manager, RouteMatch, Response and Service Manager; things still seem to work. The response probably could/should not be shared... still pondering the RouteMatch.

@Ocramius
Copy link
Member Author

The RouteMatch is a value and should therefore not be treated as a service

@devosc
Copy link
Contributor

devosc commented Feb 14, 2014

:) see my previous comment about it ...

@Ocramius
Copy link
Member Author

Anything further down the chain would need to have a listener further up to be able to capture that dependency and inject, e.g. the view url help would need a listener for the RouteMatch event to inject itself with.

Yes, you wouldn't be able to access the RouteMatch (for example) if it wasn't passed to you as a parameter somehow - that's perfectly acceptable IMO, but it indeed requires some change or to make services that are getting those "values" injected non-shared so that there are no risks.

@devosc
Copy link
Contributor

devosc commented Feb 15, 2014

I've also been wondering whether factories (or the service config), could also indicate whether a service is shared. The calling code may not always be the right end to control it from.

@Ocramius
Copy link
Member Author

@devosc the factory just represents the instantiator for a particular service - it doesn't have any understanding of the lifecycle of the produced object

@devosc
Copy link
Contributor

devosc commented Feb 15, 2014

Yeah but the same argument could be applied the other way around also. Otherwise why not making everything not shared by default, and specify which ones can be shared ... and if its only because the application knows its reusing the same service manager, then its the application config that could/would manage it, and the factory is what transforms the configuration into a service... I've been wondering if both should be allowed to manage it... Anyhow I understand what you're saying...

@Ocramius
Copy link
Member Author

why not making everything not shared by default, and specify which ones can be shared

That's because most users will still use ZF2 the "traditional way"

@devosc
Copy link
Contributor

devosc commented Feb 15, 2014

The service manager uses both a configuration and instance container (e.g. $instances) to retrieve a service. When the service manager is serialized, the container can be reset (emptied). When it is unserialized it should still work as expected as new instances will be created from its configuration and placed into the service container again. This would allow components to have individual service managers with their own configurations (there might be some duplicate configurations across components, which makes it self contained), and those managers can use a shared service container, e.g View Manager could shared the main SM service container and immediately have the RouteMatch available. The service container can also be used as a registry, but unless that component has a configuration for it, it will be lost if the service manager gets serialized.

@Ocramius
Copy link
Member Author

Serialization is not relevant here - it is not possible to serialize a container because you don't know if the objects it keeps can be serialized.
What are you trying to achieve there anyway?

@devosc
Copy link
Contributor

devosc commented Feb 15, 2014

@Ocramius Ocramius added this to the 3.0.0 milestone Apr 2, 2014
@fabiocarneiro
Copy link
Contributor

68747470733a2f2f702e67722d6173736574732e636f6d2f353430783534302f6669742f686f73746564696d616765732f313338313231333736302f343534333039342e676966

@GeeH
Copy link

GeeH commented Jun 27, 2016

This issue has been closed as part of the bug migration program as outlined here - http://framework.zend.com/blog/2016-04-11-issue-closures.html

@GeeH GeeH closed this as completed Jun 27, 2016
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

8 participants