Problem/Motivation
As #2547827: PhpStorage: past, present and future documented, currently Drupal 8 can't be used on multiple webheads without writing files to a shared filesystem. This is undesirable because a shared filesystem at best is slow at worst doesn't exist at all if stream wrappers are used to store upload files on some mass file hoster.
As detailed in both that issue and #2544932: Twig should not rely on loading php from shared file system a decision was made to solve this problem only for core. This is the container part of it.
Proposed resolution
- Introduce a new
PhpArrayContainerandPhpArrayDumperworking on the basis of PHP arrays for service definition - Instead of php storage, use a new cache bin
cache_containerto store the container definition array - Introduce a bootstrap container providing the
cache.containerservice which by default uses the database cache backend - Introduce a setting
bootstrap_container_definitionwhich allows sites to swap out the cache backend used to store the container
The advantage of this approach over a PHP-code compiled container:
- The definition is changeable at run-time in between loading it and starting the Container, e.g. app.root can be a container parameter that is set at run-time.
- The serialized format is storage agnostic, that means it can be stored in MySQL, Memcache, APC, ...
- By abstracting out the storage via the bootstrap container, the storage becomes actually pluggable.
- Drupal is never run with a ContainerBuilder, but always with a real container, because on a rebuild the new definition can be loaded. This makes testbot overall around 1-2 min faster. It also gives predictability.
- As a little side-effect we can already now remove functionality that is removed in Symfony 3.0 and deprecated in 2.8.
Remaining tasks
- None
Profiling
Empty front-page
| Run #55a3c86ae8589 | Run #55a3ccc78a513 | Diff | Diff% | |
|---|---|---|---|---|
| Number of Function Calls | 44,728 | 44,673 | -55 | -0.1% |
| Incl. Wall Time (microsec) | 81,007 | 81,530 | 523 | 0.6% |
| Incl. MemUse (bytes) | 18,439,480 | 17,954,264 | -485,216 | -2.6% |
| Incl. PeakMemUse (bytes) | 18,521,672 | 18,049,048 | -472,624 | -2.6% |
Fluctuating a little, the most is the 242 unserialize() calls (1.6 ms), but given we load from DB cache and lazy unserialize this is a linear effort, which should be acceptable.
Page Cache
| Run #55a3d01a10424 | Run #55a3d5636a3da | Diff | Diff% | |
|---|---|---|---|---|
| Number of Function Calls | 1,849 | 1,749 | -100 | -5.4% |
| Incl. Wall Time (microsec) | 4,471 | 4,465 | -6 | -0.1% |
| Incl. MemUse (bytes) | 1,979,664 | 1,969,400 | -10,264 | -0.5% |
| Incl. PeakMemUse (bytes) | 2,004,968 | 1,995,064 | -9,904 | -0.5% |
Overall kinda the same (within fluctuations), it can be faster, it can be 100 us (0.1 ms) slower, but that again is okay and given in "[meta] Page Cache" I show lots of possibilities to save overall 2 ms and that we load from the DB here and we could use memcache, APC, whatever here.
unserialize() is called 4 times here and takes 200-300 us in total, so that is fine.
User interface changes
* None
API changes
* Introduces DrupalKernel::getCachedContainerDefinition() as new public method.
* Removes the following things that are already deprecated in Symfony 2.8 and will be removed in 3.0:
- Synchronized services
- Scopes (Use: share: false from Symfony 2.8 on; scope: prototype is still supported for now while Drupal uses Symfony 2.7)
- The old syntax for factory_method and factory_class / factory_service, use the "factory" key instead introduced in Symfony 2.7.
Data model changes
* None
Note: This patch uses PHP 5.3 array() syntax on purpose, for easier backwards compatibility with the original upstream. (service_container contrib project)
Beta phase evaluation
| Issue category | Bug, because there is a critical race condition when using Drupal 8 currently on a clustered filesystem. |
|---|---|
| Issue priority | Critical, because there is no easy work-around and the race condition can lead to endless container rebuilding. |
| Prioritized changes | The main goal of this issue is performance and scalability and decoupling the service container from the need to be stored in PHPStorage, hence fixing the bug. |
| Disruption | Potentially disruptive for core/contributed and custom modules/themes only when they relied on some internal container behavior, like methods that are not declared on the interface, or on the fact that the container was stored in PHP Storage. |
| Comment | File | Size | Author |
|---|---|---|---|
| #254 | interdiff.txt | 753 bytes | wim leers |
| #254 | 2497243-fast_container-254.patch | 137.69 KB | wim leers |
| #252 | interdiff-2497243-248-252.diff.txt | 6.64 KB | fgm |
| #252 | 2497243-fast_container-252.patch | 137.68 KB | fgm |
| #250 | interdiff-2497243-248-250.diff.txt | 6.64 KB | fgm |
Comments
Comment #1
fabianx commentedOverall I think our container building is very problematic also for multiple web heads and coordination.
My proposal for that is to use three steps:
- a) Compile the container using the symfony container builder
- b) Dump the container to a list of services and parameters - read an array, store that in a cache (DB / memcache / redis)
- c) On each web head compile the container definition to e.g. /tmp/container/container_{hash-of-configuration}.php for speed reasons
Then the only thing we need to take care of is:
If /tmp/container/container_{hash-of-configuration}.php does not exist, but someone builds it already, should we wait on the lock or run somehow just with the configuration.
This also allows us to change the configuration e.g. for dynamic language parts in the Kernel, would then just compile to different containers.
Comment #2
anavarreComment #3
wim leersComment #4
moshe weitzman commentedmore of a scalability issue than a performance issue.
Comment #5
msonnabaum commentedComment #6
Anonymous (not verified) commentedjust a data point, i tried to reproduce this by putting drupal's files on an NFS share from my laptop to a VM running on the laptop.
and yeah, load + any fs latency + rebuild == sad, sad server.
Comment #7
fabianx commentedReport from the front:
- Dumping into XML and back to YAML is already supported.
Only "public: false" services fail with YAML dumper, because those are put as definitions, e.g.
Note that the service is inlined here and not asked for by ID.
If I resolve this back to the original ID OR create a random id OR just support nesting in YAML (which we could do in PHP) then we can nicely use all dumpers.
Note also that private services are never auto-serializable atm. as they miss our _serviceId property ... (should revisit in some issue)
Writing a PHP Definition dumper is probably not much effort either ...
The only thing missing is then:
- checksum
- store in DB / D7-like cache layer
- add checksum to class
- Create / Load service_container based on checksum of definitions
and see how we can _efficiently_ get from e.g. XML or PHP or whatever to a PhpDumper dumped container again.
I will also give service_container (my own symfony interface compatible container taking PHP arrays) a whirl and do some performance comparisons.
Comment #8
fabianx commentedI have a patch running my own service_container successfully. Only around 5% function calls more and from -2 ms to +2 ms for an admin page (not yet profiled in depth).
Dumping a container builder to PhpArray itself takes around 7-8 ms.
And with omitting the ResolveParameters Kernel pass we can even change the config (with the caveat that container passes don't see such changes obviously) before loading the container.
Container is 140k in definition, so easily storable in the database or memcache or redis or ... as a serialized array.
Will upload a patch around Monday.
Comment #9
wim leersLooking forward to reading that patch :)
Comment #10
fabianx commentedIt is looking pretty good:
Container using "compiled" PHP array of services and parameters, % parameters are resolved at run-time, container loaded from serialized.data file.
~ 2.3 ms to give us back sanity, yes!
And we can still compile to PHP, too.
Comment #11
fabianx commentedI am not even sure we need to compile to PHP anymore (we still compile), with some performance hacks (not calling any functions, but preparing output partially in a typed, but fast way), I get:
554 of those calls are to substr and could easily be avoided.
Means we have probably around 500 services loaded.
Granted, the variation is still 1 ms (see below), BUT:
Being completely independent of anything we could compile this PHP Array based container to a native PHP extension - similar to how Pimple can and _then_ we are likely faster than native compiled PHP code.
( https://github.com/LionsAd/service_container/blob/7.x-1.x/src/Dependency... is the basis - needs some small bug fixes to work with D8 core, but around 300 loc, maybe in the end 400 loc ... That is doable to convert to a PHP extension.)
Variation:
----
Completely unrelated thought, we should allow _one_ drupal_static to hold the container, which we clear in DrupalKernel when rebuilding, so e.g. t() and other legacy-call-the-container functions can use the container without going via \Drupal::service() and use good old static fast pattern ...
Comment #12
fabianx commentedI did not want to unassign ...
Comment #13
fabianx commentedIt turns out state() is not "stateless", but depends on system module being installed via hook_schema().
Therefore this first version still caches to a PHP array, but it works with state(), too - just not in tests ...
Performance (as seen) is really good.
Also:
"I heard you like your containers, so I put a container for your container so you can load your container."
--
In this patch:
- Adds a PhpArrayDumper
- Adds a PhpArray/Container.php (based on service_container)
- Removes the variable replacement pass via a hack (@todo need to do properly)
- Uses the same PhpArray container to add a quick bootstrap container (6 loc!), that can be changed via settings.php ('bootstrap_container_definition')
- Has @todo's for all stages where locking and caching needs to be implemented.
I think the default variable_initialize D7 is a good role model for a first start.
I think in the ideal case it is stored in the chained_fast backend and the counter result persisted from bootstrap_container to real container.
No idea if that is sane or not, but for re-using the same cache_bootstrap counter it would be fast and still use DB as backend.
--
First patch mostly for testbot.
Comment #15
fabianx commentedTestbot fluke?
Comment #17
fabianx commentedRemoved the initial setting of settings as testbot did not like that.
Override via settings.php is now all or nothing, which is fine with me, too.
Comment #19
fabianx commentedKernelTestBase oddities and some hardening on serialized services.
Comment #20
fabianx commentedReal interdiff ...
Comment #22
fabianx commentedAlias support was missing (should have been resolved by compiler passes, but tests can do runtime things ...)
Comment #23
fabianx commentedComment #25
fabianx commentedHardening is hard, forgot that ::set takes a NULL argument as well.
Comment #26
fabianx commentedComment #28
dawehnerJust a quick note.
Do you mind just having either service:method or class::method ? See symfony 2.7 update patch
Comment #29
fabianx commentedThat was funny:
KernelTestBase leaked the container builder into the test class.
Hence suddenly there were two different containers ...
This fixes it ...
Also fixed one dependency on the ContainerBuilder. getDefinition() exists on the container, because it originally had the pluginManager Interface.
Comment #30
fabianx commentedComment #32
dawehnerWell, it used to work indeed because the same container builder object got used and assigned in
\Drupal\simpletest\KernelTestBase::containerBuild()So do we really need to allow to fetch this information on runtime? I would vote to better not allow to do that, we should hide the implementation detail of our container, as much as possible.
What kind of special feature of the container is that?
should we name this defaultBootstrapContainerDefinition? What is the reason that this is static?
At least a @todo reference to #2371709: [PP-x] Move the on-demand-table creation into the database API would be cool. I would really like to get that one in one day.
Comment #33
fabianx commentedIn this patch:
- Add factory support. (2.7 ready, yeah!)
- Fix Drupal Kernel Test for new assumptions
- Fix keyvalue.memory service that should have been synthetic.
- Expose container builder for simple definition checks during tests.
- Fix services with uppercase. (argh! - Symfony during compile moves all parameters and services to have lowercase. Can we standardize in Drupal on lowercase and throw an Exception in case an uppercase is used somewhere? 1000s of unnecessary strtolower() calls are not nice to have ...)
Should be green! :)
Comment #35
fabianx commented#32:
Thanks for the review!
1. Private services, to make those efficient and distinguish from other services, a stdClass with [ 'type' => 'service' and 'value' => $definition ] is used.
I used to have a PrivateServiceDefinition object, but performance was way way slower, so using pseudo-typed objects instead ...
2. Yes we should rename, it originally was set on Settings before those were initialized, but the DrupalTestRunnerKernel did not like that as it thought there was a settings.php already ...
3. Will speak with catch where to store and what services to use for that.
Theoretically we can persist services from bootstrap container to real container ...
Comment #36
fabianx commentedRe-roll ... Symfony 2.7 got in ...
Comment #38
fabianx commentedProper factory support, removed deprecated methods from dumper to support Symfony 2.7 better.
Comment #39
wim leersThis sounds super sensible!
Comment #40
wim leersPinged @msonnabaum & @beejeebus for reviews.
Comment #41
fabianx commented#40: Little early for that - we still need to decide where to store the PHP dumped array so that we can efficiently retrieve it.
Comment #42
catchYes! (I really, really dislike this about the Symfony container, and https://github.com/symfony/symfony/pull/14558 was closed recently, so while I didn't put any effort into resolving the complaints on that PR there doesn't seem to be much appetite for fixing this upstream).
Haven't reviewed patch yet.
Comment #43
fabianx commented#42: To be fair, they did merge https://github.com/symfony/symfony/pull/14643/files, which resolves the issue.
I still think it makes no sense however ...
Comment #44
fabianx commentedOpened #2498293: Only allow lowercase service and parameter names for the mean time, this can go in anyway as its a legit performance improvement, saves one unnecessary for loop and one levenshtein.
--
Spoke with catch.
To avoid taking a performance hit we will use a 'atomic_counter' service for chained fast backend.
That 'atomic_counter' service and the 'database' we can then persist to the real container, that should be the best compromise.
Comment #45
fabianx commentedRemoved all strtolower calls again (Performance!) and incorporated #2498293: Only allow lowercase service and parameter names briefly for now.
Comment #46
fabianx commentedQuick fix for private services as discussed with dawehner.
Not sure how PhpDumper knows the ID of the private service as its not part of the definition. (Oh it does not distinguish, it only optimizes away for private services only used once.)
Anyway, this is not a problem, the hash as unique ID is better approach and next patch will include:
Comment #47
fabianx commentedComment #48
fabianx commentedUploaded the wrong patch, interdiff see #46 ...
Comment #49
fabianx commentedOut of time for now, unassigning.
Actionable items:
- #32.2 - rename and make non-static
- Register a cache factory and bin in the bootstrap container (Database for now) and use it (no idea how that works)
- Remove the hack from Symfony - can be follow-up to be able to change parameters after loading config. (should still pass tests with the compiler pass again in).
- Implement locking around rebuilding the container to avoid stampedes.
Suggestion:
cache_miss:
acquire_lock()
yes => compileContainer
no => wait short time (to be determined)
cache_get
cache_miss => compileContainer, but do not dump\
- Reviews
Comment #50
fabianx commentedComment #51
wim leersImproving the IS.
Comment #52
Aki Tendo commentedIt's far, far too late to change direction now but I am curious, why wasn't the Reflection API employed to manage the majority of DI rather than build this monster table and class -- like this one I've used in the past
https://r.je/dice.html
Comment #53
dawehner@Aki Tendo
We want to have a DI which maps as directly to the existing configuration as possible. Having a DI based upon reflection IMHO doens't solve all the usecases
we have, like for example having multiple cache backends with the same class at the end.
Comment #54
dawehnerThis is not obvious, why would
->all()not return something properly?I'm curious, is anything in that class drupal specific? Could we move that to a component?
Comment #55
fabianx commented#54:
a) Verbatim copied from Dumper/YamlDumper.php (have we a policy how to credit that?) and also taken inspiration from XmlDumper.php.
- So ask upstream? :) It looks fine however for the case that a container has no parameters.
b) No, both classes can totally be Component. (could theoretically even live upstream, but I think its fine to ship our own for the moment to get D8 releasable).
Can you add that to the IS tasks to move over from core to component - if we think its a good idea?
Probably my main reason was that there was already Core/DependencyInjection/... while there was nothing for Component.
However maybe the _serviceId things are Core code - not sure if we need that, but we have quite some in Container right now - that we need to re-visit ...
Comment #56
jibranWhy #45, #46 and #48 are not sent to testbots?
Comment #57
fabianx commentedRe-uploading, testbot fail I assume.
Comment #58
jibranre-roll
Comment #59
dawehnerOh, whw was the last patch 5KB smaller?
Comment #60
chx commentedBecause the changes to set and the addition of register and setParameter in ContainerBuilder were done in #2498293: Only allow lowercase service and parameter names . This comment was generated by your friendly diffdiff service :) interdiff won't show it.
Comment #61
jhedstromCan't we just use
call_user_func_array($class, $arguments)here?Comment #62
dawehnerWell the reason here was better performance
Comment #63
fabianx commented#61: Unfortunately no, and that (rather ugly) code is just there to avoid a performance regression due to ReflectionClass being quite costly.
Comment #64
chx commented#2296557-100: [policy] Require PHP 5.5
Comment #65
benjy commentedArgument unpacking with
...came in PHP 5.6Comment #66
chx commentedAnd the comment I linked argues for requiring 5.6 because of variadics. Sorry if that was not clear.
Comment #67
benjy commentedAh sorry I didn't click through. I should have known! 5.6, wouldn't that be awesome :)
Comment #68
catchWe discussed this on the maintainer call and agreed the issue is definitely critical to resolve, because... endless stampede.
Comment #69
dawehnerI think its fine to not clear the container definitions, given that its just an implementation detail that they are stored in cache and not somewhere else. Ideally you would never rebuild them.
We should document the design decision to not resolve the parameters on compile time, but rather make it potential dynamically on runtime, before a freeze.
Should we skip the support for scope right in the beginning?
Nitpick: its php :)
The important part we should document here is that its using a PHP array in order to know which services exists etc. Related question: Do we need our container to be introspectable? We did not needed that before ...
Do we need the check here? array_key_exists seems to be enough, given otherwise we would have already thrown the exception on the previous call
I'm not sure whether we should really support that ... Even symfony itself realized that its actually quite a bad thing to do, because you have to change the dependencies so often that just changing the class is not that helpful at all. Its also pretty annoying to have that extra level of lookup as a developer
Let's adapt the message and tell people that the container is frozen instead
General question ... in case we would use state, we would have a relyable storage across webheads, right? Could we then rebuild the container, like we do now, just when we need it, and not "lazy" like here? In that case we would have much less of a problem of locking.
Do we need that? I thought that its optional ... is that done to make the string smaller?
storage() is not really a helpful method name to begin with, but that is not the fault of this patch
That part shouldn't be that hard, right?
Comment #70
jhedstromI chatted with Fabianx, and am going to work on integrating the atomic counter concept from #2301163: Create a phpstorage backend that works with a local disk but coordinates across multiple webheads into the ChainedFastBackend.
Comment #71
jhedstromAfter further discussion with Fabianx, we decided to pursue using cache tags as a centralized invalidator for the chained fast backend rather than the atomic counter. I ran out of time today for this, but here is progress in that direction.
Current stumbling blocks:
$item->checksumwill be set)checksumat allI also did not have time to address any of #69 in this patch.
Comment #73
jhedstromComment #74
jhedstromOpened #2508417: ChainedFastBackend should have a centralized invalidation service for the work to move toward a centralized invalidation service for the chained fast backend. Patch in #71 can be ignored.
Comment #75
effulgentsia commented#74 says to ignore #71. Latest patch before that is #58, but that needs a reroll.
Comment #76
nitesh sethia commentedRerolled the patch as per #58.
Comment #77
nitesh sethia commentedRemoving the tag of Rerolling the patch.
Comment #78
chx commentedSince we are dumping into PHP why are we serializing?
Comment #79
dawehner@chx
The idea is to store it in state, and don't deal with the issues with storing it on disk.
@Fabianx
Why don't you just use $this->installSchema() in
KernelTestBase.php, this should pretty much allow this, if I understand it correctly.As alternative we could simply use a PHP based KV there and be happy.
Comment #80
fabianx commentedUsing cache is the correct way as that can be easier made pluggable. I have the factory setup locally.
Thanks for the re-rolls @all :).
Comment #81
chx commentedI have another idea. We are going PHP 5.5 and PHP 5.5 is opcache land and opcache can cache
file://andphar://wrappers. Let's write a stream wrapper then which loads the specified files from database and then we can opcache them!For a quick verification this is doable I have overridden
phar://with the existing public stream wrapper https://twitter.com/chx/status/613882731215237120 and I am planning to write a class that will cooptfile://file://foowill be safely passed along whilefile://///baror some other crazy invalid path (throw in a fewchr(0)for good measure?) will be going against the database.Edit: phenaproxima suggests
file://./foowhich indeed looks like quite invalid unlike my idea of slashes.Edit2: nope, we can't do file:// because there's not really a good and efficient way after that to reach the normal file handling functions but we definitely can do phar://. Here's a wrapper https://gist.github.com/chx/0db97d97661b50381907 that maintains normal phar functionality. The same dance would be necessary for file:// which is a bit excessive as it would effect every file open after.
Edit3: my idea is compatible with Fabian's: write the array as a bit of PHP code into the database and save serialize / unserialize.
Comment #82
msonnabaum commentedMaybe we could just try to keep it simple right now and get a working patch in, then revisit the opcache stuff as a follow up optimization?
Comment #83
jhedstromGiven #2499943: system.active_theme.THEMENAME state should not cache theme data, is this still needed?
This code is commented out, so just remove it?
Comment #84
jhedstromWe don't currently include this component, so either this needs to be removed (and the corresponding code the uses
Expression), or we need to add that component to core.Comment #85
darol100 commentedComment #86
darol100 commentedI'm working on the re-roll.
Comment #87
darol100 commented@jhedstrom,
Do you want me to created a patch with the suggestion from #83 and #84 ? Or are we are waiting feedback ?
Comment #88
darol100 commentedHere is the re-roll patch. This patch does not have any of the suggestion from #83 or #84.
Comment #90
darol100 commentedI forgot to close a bracket. Sorry..
Comment #91
darol100 commentedComment #93
neclimdulI think you might be having some trouble with manually applying the changes. here's a rebase/merge.
Comment #94
neclimdulI tested this some and this patch does seem to keep rebuilds from writing millions of containers. However, if I delete the container directory a couple times I can cause it to never get recreated.
watch find sites/default/files/phpjust shows an empty container directory for 100's of thousands of requests.Comment #95
fabianx commentedUhm, yes I am working on loading the definition from the DB.
Comment #97
fabianx commentedTo give an update:
- The Container passes Symfony test suite now - it was a little difficult, because Symfony does test a lot of internals too (e.g. compile(), frozen(), getDefinedServiceIds()) and does rely very much on Fixtures for everything. (e.g. of the compiled container)
- The PhpArrayDumper passes the converted Yaml Test suite, which however is not very extensive - so I have added my own.
The main thing missing was 'scope' and 'configurator' support.
I have an OptimizedPhpArrayDumper, which still uses an array format, but does several optimizations so we can avoid digging e.g. into deep arrays, that don't need any service resolving.
I have an PhpArray/OptimizedContainer, which just overrides two methods to parse the optimized format, but needs test coverage.
--
I will roll however a patch next that fixes the test failure (just need to add to the cache key) and uses the normal cache backend for loading the definition, so some more progress can be directly seen here.
Comment #98
fabianx commentedCleanup, Fixes, Next: Tests
Comment #100
fabianx commented->set does not return a value, so the best we can do is to check for an Exception happening.
Comment #102
dawehnerJust some early/quick feedback
This throws potentially an exception right? In case you start with no container, there is also no way to call out to the container and log things. Maaybe try catch it, and provide a message that makes it clear we are dealing with a helpless situation
I really don't get that. How are you able to deal with the cache, but then not load CacheBackendInterface, where the constant exists ...
All the changes here are pretty pointless
Seems not :)
How did that ever worked?
Comment #103
fabianx commentedNew patch should be green again - I fixed all occurrences of direct access to PhpStorage and added a proper invalidation API to DrupalKernel, where it IMHO belongs.
Feedback:
#102
1. No longer, exception thrown by the cache backend is caught in cacheDrupalContainer; The logger should not fail at this stage, because the container builder is already successfully build and the container long loaded.
2. The cache backends have their own constant on CacheBackendInterface, but removed as I decided against using tags to reduce complexity for now.
3. / 4. Yes, removed and cleaned up.
5. It did work, because tests had been run with the container builder. They are run with a real container now.
Comment #105
fabianx commentedThe Installer did not like the database to throw an Exception.
Also split ServiceProviderTest into one KernelTestBase and one WebTest - as a web test cannot get the definition of a service by design, That was only due to the side effect of running tests with the container builder.
Comment #106
fabianx commentedSelf-review:
Also need to port the unit tests over from service_container, but needs to port them to prophecy (as they use Mockery currently).
@todo Need to support that in PhpArray/Container.
@todo per it being deprecated in Symfony 3.0, we should probably throw an Exception already here and not at run-time.
@todo Need to support that in PhpArray/Container. (Symfony test suite seems to have not tested that in the tests that I did run ...)
@todo Need to support that in PhpArray/Container.
The current implementation is buggy.
Unit tests will show.
That is the wrong syntax as found out in 'service_container' module.
Need to sync fix from "upstream" base.
Should be:
type => 'private_service'
We should already throw an Exception here that Expression component does not exist.
I don't think we will add it to Drupal anytime soon.
This is Drupal specific, but that best we can do, when someone sets a service into the container.
But both the compiler passed and ContainerBuilder::set and Container::set enforce it.
So should be fine, might need a comment.
This is supported by current container, but a little buggy as the default behavior is overwritten, needs to be fixed.
The best way to ensure private services are shared (yes they are), is to add a $privateServices array here.
Just remove and push default definition as a property of the class instead.
This try { catch block is the one to deal with the passed invalidBehavior value, not expandArguments, etc.
This is deprecated and can be removed.
Remove the @todo, the ContainerBuilder already took care of resolving that and I don't think we want to allow changing that on run-time.
This is usually part of the other try { catch } block.
This is wrong and needs to be done like factory, but documentation on configurator was sparse.
Also missed decorated - unless that is a ContainerBuilder thing (need to check).
Should throw an Exception for any scope not self::SCOPE_CONTAINER.
Missing comment:
// This is for private service instantiations.
Also private services can safely be 'cached' as they can and usually are shared.
It might be better to use a NULL cache tags backend , but on the other hand if no tags are passed or loaded maybe checksum is not computed?
Comment #107
fabianx commentedIf someone wants to, they could work on it now.
Lots of actionable items.
Likely the best one to start though:
- Port unit tests from https://github.com/LionsAd/service_container/blob/7.x-1.x/tests/src/Depe... over and needs to use Prophecy instead of mockery.
Comment #108
znerol commentedAdding issue summary template with proposed resolution section.
Comment #109
znerol commentedAdding remaining tasks section
Comment #110
znerol commentedRe #54
The
_serviceIdattribute on service instances is Drupal specific.Comment #111
fabianx commented#110: Yes, it is, but besides PhpArrayDumper, it is just an implementation detail.
Nothing in the interface says that this can or cannot be done.
So I am not sure that would prevent us to move it to a Drupal Component - as its generically useful.
Comment #112
znerol commentedNot wanting to be pedantic here, but the interface is only part of the contract. In this case we are adding application specific behavior and adding that to a generic component violates the Liskov substitution principle. It is subtle though.
Instead of moving this to a component, we also might simply merge in the remaining customizations from
\Drupal\Core\DependencyInjection\Container.Anyway, I'll start porting the tests now.
Comment #113
xjmComment #114
znerol commentedThis contains the minimum amount of changes to make tests pass (except alias, which currently does not work).
Comment #115
znerol commentedCoverage report indicates missing test coverage in this areas.
Comment #117
znerol commentedMoving the test double into the container test.
Comment #119
znerol commentedIt seems to me that the container expects alias being references including the
@prefix (i.e.'alias' => '@orig.class'). Also replicated upstream Fixed tests and alias handling in PhpArrayDumper.Comment #121
fabianx commentedTaking over again
Comment #122
znerol commentedFiled #2527478: Resolve infinite stampede in mtime protected PHP storage which fixes the issues with NFS outlined in the issue summary.
Comment #123
fabianx commentedThanks znerol,
Note: This issue also solves #2301163: Create a phpstorage backend that works with a local disk but coordinates across multiple webheads implictly and not only the race condition, while it could still allow people for single server web things to put things to a raw php storage cache backend - if wanted.
Also another one is solved implictly here to have a central invaliation service for the service container.
I am gonna work on the reviews now.
Comment #124
fabianx commentedSpoke with catch, we can do the invalidation service anyway, which is helpful to reduce fragility regardless of the state of this issue.
#2493665: Add centralized container invalidation method
Comment #125
fabianx commentedQuick note to self:
- Prevent overwriting 'service_container' in set
Comment #126
fabianx commented#2493665: Add centralized container invalidation method is fixed.
The 6 Tests fails have been generically covered with:
- #2527710: Decouple Error testing from running with container builder - fixed
- #2528292: Decouple Error testing from relying on a cached on disk-container that is created by a different Kernel - green, waiting for reviews
Comment #127
fabianx commentedRe-roll now that the blockers are in.
Little fixes to unit-tests and replacing main Container.php.
No interdiff - sorry (will see if I can provide one later)
Edit:
There is more test-only changes that are useful in general that I can split off from here.
Comment #129
fabianx commentedHere is the interdiff from before - not that helpful as it contained newer changes fixed in other issues, but provided for completeness.
Comment #130
fabianx commentedFixed tests, now that the container is used always, rebuildContainer() obviously fails, so we can finally switch it over to invalidateContainer(), which makes more sense anyway.
Comment #131
fabianx commentedCleanup, preparation to split all test changes out.
Comment #133
fabianx commentedYes, I obviously missed something.
Fixed by updating the container after updateModules calls.
Comment #135
fabianx commentedOpened #2529516: Decouple tests from relying that $container is a container builder for the test changes.
This is now build on-top of that (see --on-top.txt)
Also reverted some more changes and made the container unfrozen by default, so that new Container() works.
Comment #136
bjaspan commentedHi. I'm a crufty old Drupal core developer that has not contributed to Drupal core in any meaningful way in five years. I've never looked at Drupal 8 before this week. However, I'm trying to get up to speed so that I can help push Drupal 8 out the door, and pwolanin suggested I take a look at this issue.
You should assume I have no idea what I'm talking about. :-) If this is actually not a good issue for me to "practice review" on, just let me know, and I'll shut up.
Comparing this implementation to Symfony\Component\DependencyInjection\Container, it seems like the major differences are that service names are treated as fixed strings (as opposed to being case insensitive, etc.), presumably for efficiency, and several methods are not implemented (presumably because Drupal does not need/use them). If that's true, it might be helpful to include that motivation in the class documentation. If that's false, then since I'm confused it seems even more important to include the real motivation in the class documentation. :-)
It is not obvious to me why this is not BOOTSTRAP_CONTAINER_BASE_CLASS. Worth an explanatory comment?
Comment #137
chx commented> Introduce a bootstrap container providing the cache.container service which by default uses the database cache backend
We decided that was not a great idea in #1849004: One Service Container To Rule Them All
Comment #138
fabianx commented#136: Thanks for the great feedback!
1. Indeed, well spotted that this does not allow lowercase service IDs. Fortunately we already have taken care of that in #2498293: Only allow lowercase service and parameter names for speed reasons, we should document the difference however - yes.
2. Because bootstrap container and real container are two different things. The bootstrap container provides low-level DIC services only to be consumed by DrupalKernel, while the real container is for the rest of the system.
#137: Thanks for the reminder. I remember that discussion. However that would have not blocked e.g. donquixotes low-level DIC from going in.
The difference is that this is a very lightweight container that uses a human-readable format and is easily overridable. It is also completely and 100% separate and isolated from the real container (e.g. no one besides DrupalKernel will ever interface with it), while what we had before was a weird hybrid of services (as far as I remember).
Therefore, even a mid-request cache clear would still fallback to the original cache service of the bootstrap container.
It would also allow easily allow pre-container middlewares, which can be handy for great performance. (e.g. #2501989: [meta] Page Cache Performance)
What concerns do you have?
Comment #139
fabianx commentedStarted with the cleanup, other issue is in good shape, but likely need a different solution.
Added lots of documentation to the Container class.
Comment #140
fabianx commentedOpened #2530586: Read-Only Container is not working properly. as I found that while re-working the DrupalKernelTest to match the Expectations.
And postponed #2529516: Decouple tests from relying that $container is a container builder, because I was able to change the logic to take $this->allowDumping into account which means that KernelTestBase can continue to always use the ContainerBuilder for everything.
So lots of reverts in the interdiff and less changes to tests in this patch.
Comment #141
fabianx commentedRe-organized the whole container and made a line-by-line comparison of both Container and ContainerBuilder by Symfony.
This is now an exact replica of ContainerBuilder, just way faster and based on arrays and \stdClass.
createService will move back (for performance reasons) into the main get(), but for now this is for being able to compare safely.
Comment #142
wim leersProbably the answer is , but I feel like we have to ask the question at least: .
Comment #143
fabianx commented#142: The chance exists obviously, but I would like to not make a critical dependent on an upstream decision. Also it means code style will need to be re-worked again, though apparently CS-Fixer can do that automatically.
What do you think? Should we open an upstream issue?
Comment #144
wim leersOf course not! :) Major follow-up.
Comment #145
fabianx commentedAnd it turns out ContainerBuilder has a bug if a service is public: false and shared ...
That means whenever we run with ContainerBuilder a
->get('menu.tree_storage')will always instantiate a new version ...Fixed in my version, by splitting of private services to its own $privateServices space and not mixing them.
Uploading a work-in-progress on the machine format for test bot, benchmarks look good.
I decided on a lazy unserialize strategy per service definition, that ensures the default unserialize call is very very fast and over 242 services that we load on an empty front-page (242? uh, wow ....) that is still 1-2 ms faster than loading them all at once.
Overall performance now is back on-par with compiled Container, sometimes it was even faster. (within the normal fluctuations)
Next task:
- Split off human-readable format to BootstrapContainer.php
- Cleanup
- Test some more optimizations
- Unit tests
- Done
Comment #147
fabianx commentedOkay, thats it:
- Split off: done
- Cleanup: done
- Benchmarks: done
Missing:
- Full unit test coverage (65% currently)
- Reviews
Current Benchmarks
Empty front-page
Fluctuating a little, the most is the 242 unserialize() calls (1.6 ms), but given we load from DB cache and lazy unserialize this is a linear effort, which should be acceptable.
Page Cache
Overall kinda the same (within fluctuations), it can be faster, it can be 100 us (0.1 ms) slower, but that again is okay and given in "[meta] Page Cache" I show lots of possibilities to save overall 2 ms and that we load from the DB here and we could use memcache, APC, whatever here.
unserialize() is called 4 times here and takes 200-300 us in total, so that is fine.
Comment #148
wim leersThis is a lot to review. Here's a partial review. Many nitpicks, because they make it harder to read the patch. But also some questions. I think several of my remarks/questions are stupid/silly from the POV of somebody who knows the container implementation very well. But I don't. Which also makes me a less than ideal person to review this patch probably, but hopefully my review is useful to ensure that the final code is understandable by somebody who does not know container internals.
s/interface compatible/interface-compatible/
s/specially//
s/to the YAML file format/to the YAML container definition/
This suggests it's not a generic container, but one for bootstrap services only. Which contradicts with the class docs.
When can this occur? An
@seecould be helpful here.80 cols.
I think this means synthetic services are not supported? If so, why not have the exception say that instead?
D8 HEAD just started using Symfony 2.7. Does this mean we can already remove this?
So we optimize for services with up to 10 arguments, after that we fall back to reflection.
Should this be documented? Not sure. If my initial understanding is correct, it's probably clear enough. Just want to make sure.
s/configure/configurator/ ?
Why does this only need to care about private services, and not public services?
Bad sentence.
Same comment as above; confusing.
Hrm, same as my first comment. I suspect that therefore the docs in my first comment are wrong, and theyre they are correct.
Same remarks as my second comment.
Let's add a todo to add an assertion for checking the lowercaseness once #2408013: Adding Assertions to Drupal - Test Tools. lands?
Let's use the short array syntax notation.
Same remark as earlier.
80 cols.
Missing docs.
Same remark as above.
Same remark as above.
What does this mean?
We put opening curly braces on the same line.
s/Array/array/
Comment #149
dawehnerIs that a copy and paste from somewhere? Why is that called BootstrapContainer ....
You could use just
!empty()instead.It would be great to explain here what we override from the parent method ... I mean its a big method so some overview would be nice
Let's not use short names here.
Does that mean we could have used 'file' for proxies?
I would be totally fine to not keep BC code here and JUST use 'factory' and otherwise throw an exception. Drupal does not need to be BC compatible yet
Do you mind putting some documentation why we need this construct (avoid reflection, if possible)
Do you mind explaining in which way this is a optimized code path?
I'm curious, why would it make sense to have a container without any container definition?
Needs docs with maybe some high level overview
Do you mind explaining why a \stdClass based approach is faster? Is it because we don't copy the memory of the array around? I thought php is copy on write. Such thing s would be nice as part of the documentation
We could explicit tell that its not supported, you see the small difference to not implemented
Any reason why we can't just use string concat.?
Note: Many places in this patch could use string[]
Do we need protection against infinite aliases or is that solved already on the container build time?
I don't see why we need the protection here. In case you have no database you are screwed anyway and we should fallback to the exception handler ... Are there other exceptions beside the database?
Another catch all, which is IMHO a bad idea
+1 for not extending from UnitTestCase unless you need it
Is there any reason you did not went with camelCase?
I don't see properties nor configurator in the definitions. I think for proper test coverage we should provide them as well, right?
Comment #150
neclimdulghostly boostrap typo. Also it doesn't hold the interface it hold the bootstrap container.
Comment #151
fabianx commented#148:
High level feedback for helping with understanding:
There is 2 different "container definition format"s:
1. Human readable, very similar to YAML syntax - just in PHP -- used here for BootstrapContainer. Drupal\Core\DependencyInjection\Dumper\DebugPhpArrayDumper is able to produce this format. It is a joy to write this format by hand. The DebugPhpArrayDumper is a strange name, maybe HRFPhpArrayDumper (HRF == Human-readable-format?)
2. Machine readable, still similar to YAML syntax, but collections, variable references and private and public services are specially encoded as \stdClass objects for performance reasons. Drupal\Core\DependencyInjection\Dumper\PhpArrayDumper is able to produce this format and Container can read it. It is a pain to write this format yourself.
1. fixed
2. fixed
3. fixed
4. HumanContainer or HumanReadableContainer sounded strange. Any ideas for a better name? And yes, it is generic, just within Drupal used as BootstrapContainer class. Maybe HRFContainer or such?
5. This can occur when someone dumps data with PhpArrayDumper() and tries to put the resulting definition into BootstrapContainer().
6. fixed
7. This is the verbatim message from Symfony upstream. Happy to change that, just saying ... ;)
8. Yes, we could remove support for factory_method, etc.. Should we?
9. That understanding is correct. I added a comment for clarification.
10. Again that is a message from Symfony upstream. Happy to change, though.
11. Private services are processed in a special way by the ContainerBuilder. If a private service is just used once, then the ID is removed and just the Definition of the service is left in the tree. Public services are denoted (in this human readable container format) with '@service' as in Yaml in the code below this one. In the Container class also normal services are handled.
12. Fixed to: "Check if the private service already exists."
13. Fixed to: "Create a private service from a service definition."
14. Typo fixed, rest: ?
15. fixed
16. Genius idea! - fixed
17. No, don't want to as I still need this for D7 / PHP 5.3 and per our coding conventions one can choose IIRC. Not a blocker though if someone insists.
18. See above and high level explanation.
19. Fixed
20. Oops, fixed
21. See above
22. See above
23. This should be removed as only a \stdClass approach is supported now. Fixed by removal.
24. Yes, indeed that slipped through. Fixed.
25. Fixed
--
Patch with the fixes above will come shortly.
Comment #152
fabianx commented#149:
Thanks for the great review, here are the answers:
1. See #148 high level answer
2. This uses specifically only isset() whenever possible. The reason is that it is then easier to port over to a compiled PHP extension. (I had problems when I compiled the container to C code with !empty vs. isset). Could be changed though.
3. Sure, added some comments. Overall the only thing that is changed is that the parent has an optimization to only resolveServicesAndParameters when it encounters a collection, which is checked for via "instanceof \stdClass". This could be "fixed" by using a $this->machineFormat in the parent, but I felt it was complicating the code for no reason.
4. Okay, message is from Symfony upstream. Will fix.
5. Uhm, yes possibly.
6. Will remove, thanks for a decision.
7. Yes, will add a comment. This is purely performance optimization as ReflectionClass is pretty costly on run-time.
8. The comment is no longer needed as its the only code path now. Could say: Machine-readable format.
9. A container can be used as is:
10. Uhm, yes for sure. I originally had planned to inline that into get(), but after benchmarking this turned out to not be neceessary.
11. The reason I use \stdClass objects is to distinguish between arrays and objects. It is just to be able to use instanceof and optimize some code paths.
e.g. a "collection" / array that is 5 levels deep would in the human-readable-format need to be traversed fully even if there is no '%' or '@' used (no variable or service to resolve).
In the optimized format only collections are resolved and only recursively as long as 'resolve' => TRUE.
12. That's _your_ original wording :p. Yes, lets use not supported.
13. We can, that comes from YAML dumper upstream. Fixed.
14. Will fix, have to find them all first. (@todo)
15. Should be solved by ContainerBuilder pass. Could add if we really wanted to.
16. InstallerTest fails without it. The database is not available at first.
17. This is the only way to see if saving worked. There is no status code coming back from $cache API. Also the caller takes care of the error, so should be fine.
18. :)
19. Yes, that is my own convention. I like to be able to see things clearly as in test_{method}_{context}. But as its not a core convention and coding standards violation I will change it.
20. Properties should be there, Configurator is indeed missing. Test coverage shows that.
--
#150: Ghostly boostrap typo fixed :-D.
Comment #153
dawehnerThank you fabian for the quick response!
Ha :)
Are you sure there is not something already? Well I think this is a small detail.
We should document this then. This is about early installer then, I guess?
Another place where some docs would be great!
Ah, great!
Comment #154
fabianx commentedPlease don't be shocked about the new patch size. That is just unit tests added.
In this patch:
- interdiff-cleanup.txt: Address Wim's and dawehner's review.
- interdiff.txt - Cleanup and 100% test coverage for Container, PhpArrayContainer (BootstrapContainer), PhpArrayDumper (formerly DebugDumper) and OptimizedPhpArrayDumper.
Test coverage with this patch is now complete.
Discussed naming in IRC:
Container + PhpArrayContainer
OptimizedPhpArrayDumper + PhpArrayDumper
- Renaming for that is next.
- Then: Removal of deprecated factory_method, etc. + associated test coverage
- Then: Needs more reviews.
Comment #156
fabianx commentedClean rebase, conflicted on UncaughtExceptionTest, but resolved automatically.
Comment #158
fabianx commentedFixtures need to have a non-php extension.
Comment #160
fabianx commented#2151103: Zend feed plugins are incorrectly registered broke the deprecation of scope we use here.
So I added intermediate support for scope: prototype to the dumper and forward compatibility with Symfony 2.8 for the 'shared' flag.
See: https://github.com/symfony/symfony/blob/2.8/UPGRADE-2.8.md#dependencyinj...
Also added tests for 'scope: prototype' to the Dumper test and tests for non-shared services to the ContainerTest.
Comment #161
dawehnernitpick: We don't use use statements for global level classes but rather \ReflectionClass itself
Doesn't the original container throws an E_DEPRECTATED warning if factory_method/factory_class/factory_service is used?
I think the second $length = is wrong
Would that be a good place for an assertion in the future?
Missing new line
not used
What about splitting this up into: Provides a container optimized for Drupal's need.\n\nThis Container implementation is compatible with the default Syfmony dependency injection container ...
Should we explain when the parameters should be frozen? I would argue that it should be frozen at least once $this->handle() is executed?
Afaik symfony 3.0 will remove both of those features
Missing "*"
Just ContainerInterface is used
unused
I think it would be still really nice if you could explain here, why this is faster
Nothing uses this method afaik, so this could be protected?
Do you mind adding a typehint?
Resolve is not documented yet
null vs. NULL
Is there a reason why we can't make them protected variables on the class and then maybe at some point make it actually possible to swap them out?
I'm curious whether you mind at least adapting the docs to talk about the cache key for the container? Its so not obvious from the method name that we deal with that
Unused
Jus that shows how much easier it is than ordinary phpunit mocking
Do you mind moving it to the more appropriate namespace: Drupal\Tests\Core\DependencyInjection ?
That class does not exist
Comment #162
fabianx commented#161:
1. Fixed
2. We could do that, but we could also just not support it as the dumper already does not support it (else the deprecation notice would have led to failed tests. I vote to just remove it.
3. Indeed, fixed.
4. No, that is a real run-time exception and should never happen unless someone creates a machine-readable definition manually.
5. Fixed
6. Fixed by using it.
7. Fixed
8. I think this is an internal detail.
9. Okay, fixed by saying deprecated in 2.8, will be removed in 3.0.
10. Oops, fixed.
11. Yes, thanks!
12. Fixed.
13. Explained in IRC. TL;DR: Less objects to load, function calls saved, etc.
14. Uhm, not really. DrupalKernel uses it, to avoid a serialize / unserialize dance, because dump() must return a string.
15. I can do that sure, is callable a valid typehint in 5.5+? I assume yes ...
16. Oh, very good point. Fixed.
17. Good catch, fixed.
18. We can totally do that, fixed.
19. Lets call it getContainerCacheKey, that is way more obvious.
20. Fixed
21. YES!!!
22. Yes, There is already a ContainerTest, I guess I should just merge things in.
23. Good catch, fixed.
--
Will upload a new patch with those fixes and the renames shortly ...
Comment #163
chx commentedcallable is a typehint since 5.4 . Sincerely: your obscure PHP fact collector.
Comment #164
dawehner+1
You should be able to use
::classfor mocks in PHP 5.5 now.Comment #165
fabianx commentedHere is #162 addressing #161.
Not yet finished the rename, are there any helpful tools to help with that?
Comment #166
dawehnerPhpStorm /me hides
Comment #167
fabianx commented- Big rename is done (see interdiff-rename.txt)
- Added getServiceIds() and made human readable dumper not serialize service definitions (interdiff.txt)
getServiceIds() will be used by chx' patch in #2531564: Fix leaky and brittle container serialization solution and is used in Drupal Console, so it was best to just add it.
We should open an upstream issue to add it to the Interface for 3.0.
Also rebased on HEAD (no conflicts).
Edit:
Blocked on reviews again.
Comment #169
fabianx commentedUhm, yes.
It helps to run unit tests before pushing a patch ...
- Fixed tests and test coverage (back to 100%)
Comment #170
fabianx commentedAdded #2537714: Add assertions for allowing only lowercase parameter and service names in the container and other things as child issue. I do not really want to blow this patch up more at this moment.
Comment #171
dawehnerJust another small review.
This needs documentation of why we are doing it.
Feel free to try to, but it will be hard, I can tell you that. For now I suggest to have a dedicated interface with this additional method.
nitpick: use \ReflectionClass ...
I think we move the explanation into its own line
nitpick: two spaces before $escape
Does someone know how switch() actually works in PHP? I remember that in C you might have not the typical if elseif elseif internally but rather a branch table, which then doesn't matter in the order of your case statements. I would imagine that most services we use have not 0 but rather 2 or more, so maybe reorder some of them could improve speed a lot? I think it would be quite a micro-opt though, so I just right down my thoughts here
Nice!
would it be worth to do a switch for the amount of parameters to avoid cufa?
I think we now no longer use sprintf for exceptions, just use inline
"'$type'", this improves readability a bitI'm curious whether the installer InstallerKernel could override something so we don't need to catch it? Ideally we would not catch the exception on actual runtime, as we want to see when the database is crashed as soon as possible
Do you plan to adapt your test function name?
Comment #172
pfrenssenThis is a bit over my head but I could address the remarks from #171 :)
Comment #173
fabianx commentedI reverted the call_user_func_array change as for calls a parameter is given 90% of the time, so we would not save anything.
And a count() call is likely slower than call_user_func_array directly; also we can improve that in the future without an API change (by e.g. pushing the argument count for calls into the machine format).
---
Removed the catch-all as InstallerTest was passing locally. Lets see what test bot thinks.
Comment #174
dawehnerWe seriously need profiling on this particular issue. My poor profiling made 4% less performance, which would be quite bad.
Comment #175
dawehner.
Comment #176
fabianx commentedAdded profiling data to issue summary, can't reproduce 4% slower.
Comment #177
neclimdulI did some quick ApacheBench to get another view. Tested a local setup(php 5.6 fpm, mariadb 10, nginx) and on a 2 server setup to see network effects of the db being on the network(php 5.5, apache, mariadb). Results fell mostly in line with what Fabianx said and didn't have any suprises.
Both cases mean and median where faster with the patch.
Both cases the slowest request with the patch was slower then without. Would normally ignore top/bottom 1% but it was interesting and it's possible dawehner hit that 1% longest request?
No surprise really but with the database external, the std dev was much higher. However interestingly all local the std dev was lower. Cant really explain that but they where really close(16.4 and 13.8).
Comment #178
fabianx commentedI fixed all the test names with really descriptive names instead of '_', I ran everything through phpcs with 'Drupal' standard, but left two warnings for too long line, because:
- One is a Contains line and there is no way to make the class name shorter.
- One is a reference to a URL and I don't think we should break that up.
That means everything is addressed now.
Thanks, #177 for the additional benchmarks.
Now we need someone who either reviews it again and has more things for me to fix or RTBC's it.
I think it would be great to close this critical down and start with the next steps to e.g. get the benefits of changing parameters before container initialization and remove some hacks.
Comment #179
fabianx commentedThe missing files for #178 ...
Comment #180
dawehnerIt could be indeed that the difference is the php version. I see kind of similar percentages with ab as well:
Head
Patch
Comment #181
wim leersWow, that is a significant reduction in the standard deviation!
Interesting theory. I wonder if HEAD's compiled container is so large that it ends up neither in PHP 5.5's OpCache nor the operating system's file cache?
Comment #182
dawehnerMh, so I have 64MB of total memory for opcache, 2000 max files (this could be the problem), and max_file_size (so this is not the problem).
Comment #183
benjy commentedRead the code and tried to review, mainly just questions a few small doc things.
If $frozen is truly only for testing purposes, should we make the default TRUE?
Un-needed brackets around (!isset($container....
Maybe we should have an option in the example.local.settings.php file for switching the human readable version on?
Nitpick but I think we tend to use "Gets" by convention on getters.
Could be just $service['public'] = $definition->isPublic();
Can't return a string, just \stdClass
There seems to be a lot of duplicate code between PhpArrayContainer::createService() and Container::createService(), is there no way we can share code? Also, PhpArrayContainer is using inheritdoc, maybe we should say why it's overridden if there are significant changes?
Comment #184
dawehnerI think we should freeze the parameters at some point during the bootstrap. This is needed in order to keep the system in a consistent state, as otherwise you have services with parameters with one value and later with another value.
Comment #185
fabianx commented#183:
1. The reason is to allow unit tests e.g. to use just:
which dawehner argued for should be valid usage, based on how one would use the plain Symfony container.
instead of having to do:
But agree, the docs should be updated, any production usage (as done in DrupalKernel) should just set the second parameter to TRUE to use frozen parameters for sanity, but as we override the constructor with passing in a $container_definition instead of just parameters, I think it is fine to also set the parameters to frozen.
Alternatively we could code the frozen-ness into the $container_definition itself and default it to TRUE?
2. No, those are needed as there is a ||.
3. Possibly, however I guess that will need some discussions, so maybe as potential follow-up?
4. Okay, that I can fix. So s/Retrieves/Gets/g?
5. No, this is on purpose, by keeping the definitions short, memory and speed on unserialize is saved.
6. This gets overwritten in the PhpArrayDumper sub-class to return a string, so the doxygen is correct.
7. There is a large comment at the 2nd createService explaining how it is difficult?
Per coding convention, additional comments should be in the body of the function, not extending the inheritdoc.
Thanks for the review!
Comment #186
chx commentedRegarding freezing, consider #2536012: The app.root and site.path services are invalid . The best solution for that one is to change app.root into a parameter and inject on boot time.
Comment #187
benjy commentedYeah.
The rest sounds fine. Patch is looking good to me, great work!
Comment #188
fabianx commented#183
1. Addressed by moving frozen to the container definition, that is overall cleaner anyway, matches the ContainerBuilder more closely and felt good from a code flow perspective.
Also in this patch round:
- Fixed nits for Retrieves / Returns vs. Get; just left in DrupalKernel, because that uses Return everywhere to keep that being consistent.
- Improved documentation
- Fixed more PHPCS errors (forgot Container.php)
#186:
Agree, however as we deal with a PHP array it is irrelevant if the parameters are frozen or not as I would propose we change them directly in the definition array before starting the container. At least that would be the most performant solution, but follow-up after this one ...
We probably need a small change record for this.
Comment #189
fabianx commentedCreated:
- https://www.drupal.org/node/2540408 - Drupal now has its own Symfony compatible service container and a PHP array dumper
- https://www.drupal.org/node/2540430 - The service container definition is now stored in the database by default
as change records.
Comment #190
dawehnerAdding a related issue.
I still don't get why we allow setParameter() during runtime, we should freeze it at some point during bootstrap.
Comment #191
fabianx commented#190: We don't allow setParameter() at run-time and in this patch never have.
The container_definition now contains a frozen flag, which is set to what $container_builder reports, which after compile() was called is always TRUE.
So we never allow setting paramters at run-time, this is only allowed (as in Symfony upstream), when just creating a Container(), then setting services dynamically.
If we wanted to we could support the whole shebang of compile(), isFrozen(), getParameterBag() to be not only interface compatible, but also 'class'-compatible.
Then you could do:
But we could also discuss this as a follow-up.
Comment #192
geerlingguy commentedNote that I've hit this stampede when using a shared files directory using a GlusterFS mount replicated on four webservers. See: #2540912: Installation fails with files directory on glusterfs: "Warning: mkdir(): File exists". The patch from #188 fixed the first two problems: first, on installation, I kept getting a 'file exists' exception for the service container, and second, same thing would happen after installing an older version of D8 (beta 11) and trying to load a page. The third issue I've encountered seems to be fixed by another race condition in Twig, which is fixed by #2429659-114: Race conditions in the twig template cache.
Still doing a little testing, but this patch at least seems to resolve a 100% reproducible problem (just set up a cluster of servers with a shared files directory mounted on a vanilla GlusterFS share, and you will hit the same problem).
I don't want to set RTBC since I haven't had time to go through the issue fully and read through code, but in terms of practicality and fixing the problem, I'll give a +1 :)
Comment #193
dawehnerAgree, but it is good to keep that in mind.
It would be great if @znerol could review this
Comment #194
wim leersGeneral remark: I don't think any of this is specific to Drupal 8? It could technically be moved to
\Drupal\Component, instead of\Drupal\Core? (And we still need a major follow-up to propose to move this into upstream Symfony. This would benefit a lot of projects.)Impressive patch!
I'm not qualified to review the technical sides of this patch. So here's a review with some questions, but mostly nitpicks.
Nit: strange formatting.
Nit: s/
array()/[].Here and in many other places throughout the patch. Perhaps it's better to not update all of that though, because that'd be quite a bit of work.
s/Can/Whether/
s/still/can still/
Nit: 80 cols.
Nit: s/noticeable/noticeably/
Needs FQCN.
Nit: extraneous blank line.
Nit: "ContainerBuilder based format" is not quite correct.
"rich value objects for perf reasons" -> that sounds like it could use some extra explanation.
What are deep collections?
Nit: s/less/fewer/
s/dumper shared/dumper, shared/
s/machine readable/machine-readable/
s/human readable/human-readable/
s/on loading time/at run time/ ?
"behavior." is indented incorrectly.
Nit: s/id/ID/
80 cols.
s/human readable/human-readable/
s/machine optimized/machine-optimized/
Also: elsewhere, "machine-readable" was written, should that have been "machine-optimized"?
Hrm, despite this extending the
Container, this does repeat a lot of highly similar code that we already have there. Isn't there any way to avoid that?s/parents/parent's/
s/human writable/human-readable/? :)
Nit: extraneous blank line.
Comment #195
fabianx commentedFrom the D8 EU criticals call:
dawehner had some feedback and was concerned that update.php would fatal when trying to load the container from the cache.
Also from the change record it can be seen that getting a container definition is quite a dance right now.
So proposal to fix both is to add:
- getCachedContainerDefinition as public function to DrupalKernel(Interface) and use that; overwrite it in update.php's UpdateKernel to always return FALSE.
Then update.php would never rely on the cache system - regardless which class is used for loading / storing the container.
Comment #196
pwolanin commentedIf we are re-writing the container here, do we need to take into account possible solutions to the problems being addressed in #2531564: Fix leaky and brittle container serialization solution?
Comment #197
pwolanin commentedWe should not be using crc32 for anything, so we should fix this code also. It's not clear why a hash is even needed?
Comment #198
catchThe hash is needed because Drupal::VERSION always has a
.in it, and PHP function names cannot.Comment #199
pwolanin commented@catch - the method name says it's a cache key?
Comment #200
catchWell that's a point, the method previously was for the classname. So yes likely don't need to hash any more.
Comment #201
fabianx commented#196: Not really. The container currently uses the _serviceId property on set() pattern, for builded services our compiler pass puts those into 'properties'.
I did go ahead and added getServiceIds() to the Container (as its used by drupal/console) and that _could_ be used by that issue - so Container support is there.
If we wanted some more native support, that would also be possible to add here or in a follow-up.
If that other issue gets in first, we simply remove the _serviceId property setting from set() and the unit test for it.
Comment #202
fabianx commented#194:
1. Fixed; again the question: Should we not just support it, it is around 20 loc including unit tests.
2. As discussed in IRC, used for PHP 5.3 compatibility as e.g. provided by Ubuntu LTS for my contrib service_container project. Added to issue summary.
3. Fixed
4. Fixed
5. Fixed
6. Fixed
7. Fixed
8. Hmmm, have to think about a better formulation. What I want to say is that overall the 'structure' is very similar, but instead of $container_builder->register('foo', 'SomeClass')->addArgument(new Reference('bar')) one uses \stdClass objects with a simple 'type' value e.g. for a Reference. -- Could be follow-up to fix.
9. This should already be explained in some other part of the code.
10. A deep collection is: [[[[[[[['@foo']]]]]]]] vs. [[[[[[[['foo']]]]]]]]. In the first case, the collection needs to be traversed completely and the reference resolved; in the second case the collection can be passed on as is.
11. Fixed
12. Fixed
13. Fixed
14. No, actually at loading time, when the container definition is loaded from the database.
15. Yes, Fixed. Strange that phpcs did not find that one.
16. Fixed
17. Fixed
18. machine-optimized is much better, changed all occurences.
19. There is a large comment at createService() explaining what the differences are. The TL;DR is: Not without introducing an "|| !$this->machineFormat after each instanceof \stdClass" check.
20. Fixed
21. Yes, lets use human-readable vs. machine-optimized. That likely is a nice distinction. Especially as the machine format (without an API break) could be further optimized.
22. Fixed
Thanks for the review!
Next tasks:
- Post patch with fixes above
- Add getCachedContainerDefinition() to Interface (and override in UpdateKernel to not use it).
- Think if we want to move to \Drupal\Component, we likely can as the _serviceID hack will go away, so its okay to subclass a little longer.
- Think if we want to support isFrozen(), compile() and getParameterBag() - to be class - compatible and not only interface compatible.
Also added beta evaluation and tweaked IS a little.
Comment #203
catchDidn't fully review the test coverage. I've read through the actual container code several times now and have very few comments - this mostly fixes all the things I don't like about Symfony's container implementation both at a macro and micro-level which makes it hard to find things to complain about.
Few minor things below, some of these might duplicate Wim's review, didn't check against each point.
Also as people have mentioned about, feel a bit uncertain about using a cache backend for this, although it has advantages with code re-use.
Should this be an assert or can it really happen on runtime somehow?
It's micro-micro optimisation - but theoretically we could leave this until the definition isn't found, then call recursively with the alias if there is one - would mean the alias check only ever runs if someone is actually using an alias - although gets more expensive if someone is.
In general this method is considerably more readable and better documented than the one in Symfony's own container.
+++ b/core/lib/Drupal/Core/DependencyInjection/Container.php @@ -7,23 +7,617 @@ + + // Optimize class instantiation for services with up to 10 parameters as + // ReflectionClass is noticeable slow. + switch ($length) { + case 0: + $service = new $class(); + break; + + case 1: + $service = new $class($arguments[0]); + break; + + case 2: + $service = new $class($arguments[0], $arguments[1]); + break; + + case 3: + $service = new $class($arguments[0], $arguments[1], $arguments[2]); + break; + + case 4: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3]); + break; + + case 5: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]); + break; + + case 6: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]); + break; + + case 7: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6]); + break; + + case 8: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7]); + break; - // Ensure that the _serviceId property is set on synthetic services as well. - if (isset($this->services[$id]) && is_object($this->services[$id]) && !isset($this->services[$id]->_serviceId)) { - $this->services[$id]->_serviceId = $id; + case 9: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8]); + break; + + case 10: + $service = new $class($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6], $arguments[7], $arguments[8], $arguments[9]); + break;I'm slightly missing an ascii person walking up the megalith.
I'm a bit lost by this comment, surely it doesn't throw an exception all the time?
I think this isn't helpful and adds complexity to the container, see for example https://github.com/symfony/symfony/pull/10536. Although I guess it maintains feature-parity with Symfony's actual container so as long as we never trigger it per the above PR it's not doing much harm.
Any particular reason why sha1? And if so why not hash('sha1')?
Nit: missing parens.
nit: existent.
Comment #204
znerol commentedIts been more than a week since #193 and I looked at this patch at several occasions. Even though the code has matured quite a lot since I first worked on it, I still think it is crazy to replace the container implementation. This is not because I wouldn't trust the people here in this issue that they are capable of rewriting and reviewing it. But rather because history shows that the Drupal community is not too great when it comes to maintain forked code (incompatible features creep in, upstream changes are not replicated). We should not lightly give up the benefits of code sharing for such an important part of the system.
I am not convinced that it is necessary to replace the container implementation to resolve the problem stated in the issue summary. I took the time to analyze and reproduce the issue and I showed that the problem is not related to the (Symfony) container implementation, but rather the way (Drupal) PHP storage currently works.
I understand that this approach might bring in additional benefits, but I'd prefer if those features wouldn't be rushed in as part of a critical bug fix. We should take time to carefully weight up the new possibilities with those we will loose (e.g. read only container, OpCode cache).
That said, if we should end up committing this patch, then I suggest to move the code into a new namespace on the same level as
Drupal\CoreandDrupal\Componentdesignated for stuff we forked and which needs special attention when updating vendor.I have great respect for the work done here and for the people involved. It is possible that despite of my efforts to reproduce the issue I do not fully see the whole picture. It might help if the issue summary had a list of reasons why this approach was chosen and why there is no alternative to it.
Comment #205
wim leersI just wanted to say: thank you znerol, for such a wonderfully informative, balanced, constructively critical comment!
Comment #206
catchThe endless stampede is due to the specific PhpStorage implementation, but #2544932: Twig should not rely on loading php from shared file system is currently open to try to remove the requirement to use PhpStorage on a shared filesystem altogether. I'm not sure yet whether removing that requirement should be release blocking, but it's not good.
When we first added the container, we used ContainerBuilder exclusively, and 'compiling to PHP' was mentioned as a possible optimisation in the future. However the reality is that Symfony's container has to be compiled to PHP to perform in any reasonable way, and they don't support using ContainerBuilder in production at all (at least going by https://github.com/symfony/symfony/pull/10536#issuecomment-38695228). I found that issue because on requests that rebuild the container, we use ContainerBuilder in the kernel instead of the regular one - something this patch fixes, since it only uses ContainerBuilder for building the container. (tried to make that sentence scan better but failed..).
We have a hard requirement that configuration changes either via the UI or a config sync can determine which code is executed on the site. That requires being able to change the container on the live site rather than having it checked into git or in a build step.
Even a 100% command line workflow could not do this properly for Drupal, since even if you compile the container and move that to production alongside a config import, it's the process of the configuration import itself (installing/uninstalling a module) that determines the contents of the current container, not just the code push. So there'd be a period where the wrong one is used. #2507509: Service changes should not result in fatal errors between patch or minor releases got a bit closer to fixing this, but still doesn't fix the period between the deployment and the config sync.
So while there is an obvious bug in PhpStorage and the patch to fix it looks right, I think there's a fundamental incompatibility between the requirement to update the container in-situ and compiling that container to PHP.
I think the answer to this is to either see if we can get this implementation upstream, or if not a shared project where other projects that have some kind of plugin/module/extension interface and are also using the Symfony container could use it. The separate namespace for forked stuff seems like a good idea to make it more explicit why this exists for now. We could probably move ContainerAwareEventDispatcher there too.
Comment #207
dawehnerThank you for your great comments @znerol and @catch
I totally agree with the point that replacing such a fundamental part of Drupal is problematic, because it increases the busfactor we have at the moment. More people looked at the old container in the meantime than the new one, but on the other hand I 100% agree that the assumptions of the symfony container just simply not fit for Drupal itself.
One thing is that the size scales with the amount of services you have. Every Drupal project is more complex than most symfony projects for example. Also the fact that we cannot require the command line and allow to install a new module separate (while this could be solved in a different way for itself, by decoupling the rebuilding from the running instance of Drupal).
There was an issue at some point to allow code in Drupal\Component which just depends on external code but not on other Drupal related code. We might want to revisit that and make it clear that a dedicated namespace would make sense actually, see #2303777: Allow drupal components to depend on other components outside Drupal
Comment #208
fabianx commented#204:
Thank you znerol for the great and insightful feedback.
I don't think it is crazy to replace the container, because:
I think this cannot be compared to earlier forked code, because - for the examples I know:
I am not aware of any more third party code maintenance problems we had in the past.
To the container itself:
At least at this moment an upgrade to Symfony 3.0 would directly show all incompatibilities / upstream features via the test suite failing.
On the other hand if guzzle breaks our tests we also have to fix it - so this is not that different.
If the event dispatcher changes we also have to fix it.
We also have the interface contract, so any break there would also register directly, which is way different from procedural code.
I think especially with the strengths of OOP it is possible to do such a move without anyone even noticing that something is different.
The overall aching problem is that Symfony and Drupal have different assumptions. Symfony assumes that everything is static and determined at application compile time, while Drupal assumes that everything is dynamic. The dynamic-ness however is Drupal's big big strength.
While the ContainerBuilder - even though a little weird - would work for that, it is way too slow. Symfony assumes it is okay to use 'PHP code' as a cache and that is an assumption that only works in a static world, but not in a dynamic world. (the same is btw. true for composer)
So the overall assumption that its okay to use 'PHP code' as a cache is flawed in a Drupal dynamic world. MTimeProtectedPhpStorage solves that in a way, but e.g. some infrastructure people I know were like: "Eh, compiled PHP code written at run-time in the file-system? What are you doing? And that is in D8, too?" when confronted with a project using the PhpStorage component. (This is just paraphrased, but the overall reaction I heard of security-sensitive infrastructure people when asking to write code at run-time. Also in this case they could get away with a jenkins deployment script pre-compiling the container, but that is not true for Drupal 8 as catch explained in #206.)
TL;DR: Writing PHP code at run-time was and always has been problematic from a security standpoint and even with MTimeProtected... it remains a security risk. (What if there is a bug in the code and a race condition allows writing code still somehow?)
It is a good idea to list what we loose in the IS, however:
* APC
* Opcache
* Memcache
* MySQL
* Custom-Very-Fast-Local-KV-Store
* or a chain thereof, etc.
So I don't think in reality we loose anything.
This is not a fork per se, but only a container implementation making use of the ContainerBuilder interface and re-implementing the Container base class while keeping class-equality. It also passes the Symfony test suite.
Unless something tries to use the new features, per the interface contracts, nothing should break.
This was highlighted in this patch that it e.g. could still support old factory_method syntax, while Symfony had it deprecated already.
Just when Drupal started using 'factory' the unit tests started to break.
I very much like the idea of a new namespace though. I will put it first in Drupal\Component\DependencyInjection for now and then we can decide, where it can find its new home.
Lets open an issue to discuss that.
#207
I agree that the bus factor is problematic, but that is equally true for used upstream projects. In reality the components we use are only maintained by a handful of active people, too. If guzzle decides to shut down, there might be a fork, but there also was never a fork of simpletest, despite it once being very active and a good choice for Drupal. The same could happen to phpunit.
However e.g. compared to the router this is really simple code. Really a container is not that complicated. I wrote my first iteration from scratch in around 3-5 hours (without even having looked at the Symfony code).
#206
I agree totally. I also remember those issues from way back in 2012 when the container was introduced. The original assumption was never that compilation was mandatory, but indeed that it is an optional optimization, then we had a 30% test run regression (or something like that) and had to compile it.
And it is great that we matured Drupal OOP-wise to a point so that we can finally solve this properly - as we as a whole community have learned the standards of the OOP world, have learned the advantages, but also the limitations of third-party-code and what it really means to get off the island and join the larger PHP community.
But with all the inter-island exchange I don't think we should forget our Drupal culture, which is run-time dynamicness, a concept foreign to most of the rest of the PHP world.
And I think this patch / approach bridges that cultural gap by using the best out of both worlds:
- The dynamic, flexible and great ContainerBuilder for creating a container definition.
- A fast container for using said container definition.
Next:
- New patch
Comment #209
fabianx commentedHere is a new patch fixing all up to #202, remaining now only:
- #203 (next patch)
- Move to Drupal\Component
( mainly uploaded to ensure tests are still passing on testbot and to show the incremental changes)
Comment #210
fabianx commented#203:
1. No that is a fatal condition as when the machine format is explicitly specified, but used for the wrong container, it will fail to parse it and hence this cannot be recovered from. This cannot happen at random though.
2. I don't think that is wise and adding an isset() was not a deal breaker in performance testing for me. However the additional function call on "not-found" would hurt way way more and adding a while loop just for that would decrease the readability again very much needlessly.
3. Lol, ASCII art patches welcome :)
4. So this is an optimized short-cut for the case the parameter is set, but when it is not, rather than repeating all the code to throw an Exception, it simply calls the getParameter() method in the knowledge that it _will fail_. Not sure how I can phrase this better. (@todo)
5. Uhm, can be any hash that is collision-resistant enough, as it is only used during dumping time and as a dump even with that hash call only takes ~ 8 ms I used sha1. Will change to call hash('sha1') though.
1. Pre-existing, but will fix.
2. Fixed in next patch
Comment #212
fabianx commentedFixing tests and #203.
I think this is ready for review again - the move to Component might only mean two things:
Probably Container would be again moved to OptimizedPhpArrayContainer and probably PhpArrayContainer be made the overall base class, but I don't have time right now to finish that.
--
To fix the tests I decided to add an is_object() call for now. set() is not called that often that it would make a huge difference. I might fix the test properly again, though there is a chicken-egg situation as 'update.root' should be really a container parameter, which it could be after this patch goes in, but this patch cannot go in until tests are fixed, ...
Interdiff and patch attached.
Comment #213
fabianx commentedComment #214
klausiSteps to reproduce with a standard install on Ubuntu:
Remove the compiled container from disk:
cd sites/default/files/php && rm -rf service_container
Terminal 1: start a watch command to see the file name of the compiled container changing all the time:
watch -d "ls -l service_container/service_container_*"
Terminal 2: now start hitting your site hard with ab, 100 concurrent requests:
ab -n 100000 -c 100 http://drupal-8.localhost/
In Terminal 1 you can watch now the file name hash of the compiled container constantly changing after a couple of seconds, which means that it is rebuilt again and again.
Comment #215
znerol commentedRe #206: Thanks, this very much contributes to a better understanding.
Re #208:
I agree very much. However, as we learnt from some of the clever exploits of SA-CORE-2014-005, the database is not necessarily a better place for data structures governing code execution. Though awareness in the broad community is probably higher for db related security problems than for fs related ones.
Comment #216
fabianx commentedMoved to Component now:
Comment #217
wim leersThe IS lists these remaining tasks:
But at least #54 is now done, as of #216. So… which of these are actually still relevant?
Comment #218
fabianx commentedNo remaining tasks left. This is ready.
Comment #219
fabianx commentedUpdated the IS some more.
Comment #220
dawehnerOne thing the issue summary sadly doesn't describe yet, why this patch is the only way to solve it. Why can't we have a lock around
the container rebuilding. For such a rare occurrence than rebuilding the container hardcoding the used lock implementation feels fine for me.
Comment #221
chx commentedComment #222
chx commentedComment #223
Crell commentedCan you update the IS to indicate what deprecated functionality we're removing along the way here? That's a valid argument to make but the IS should make that case.
Comment #224
jibranIS needs update as per #223 and #220 so NW.
Comment #226
martin107 commentedfixed broken link in issue summary
[#2547827: PhpStorage] becomes #2547827: PhpStorage: past, present and future
Comment #227
martin107 commentedComment #228
fabianx commentedAs thought, this needed a rebase after KernelTestBase did go in.
Fortunately changes / conflicts have been minimal and only related to internal structures / logic within DrupalKernel.
Interdiff attached.
Updated the issue summary with the removed, because deprecated functionality:
Comment #229
dawehnerI really think we should better get this in rather sooner than later so that we can get people to use it on live systems.
We need a way to be able to judge whether it solves many of those potential problems.
While sleeping tonight I wondered in general whether a follow up of this issue could be to store a partially initialized container, so we could skip some of those service container calls and replace with one huge unserialize() call. thoughts?
we could document it via array[] so its clear what is in there, oh wait it also maybe contains a serialized instance. Should we document what is in here?
Sounds like an assertion for me ...
I'm curious, would it ever make sense to not have all those keys defined? We could also do a
$container_definition += ['aliases' => [], ...]insteadGiven how many get calls we have (let's say 200) do you think applying http://fabien.potencier.org/the-php-ternary-operator-fast-or-not.html would make sense here?
Nitpick: Let's start with \
This is IMHO an assertion as well
assertion?
This usestatement is unused
IMHO we should change it and use the constant on the interface instead
Mh, I'm not sure whether catching the exception here is the best idea. I mean the caller no longer can do anything about the exception, if the cache API fails I would bet that we would have a fail earlier anyway
+1 for this change but feels out of change for this particular issue
Should we use a .php file instead? I don't see a reason to "lie" here :)
Comment #230
twistor commentedRegarding #5 above, and http://fabien.potencier.org/the-php-ternary-operator-fast-or-not.html. I don't think that's true anymore. I believe it was fixed in PHP 5.4. I can't seem to find the actual issue that fixed it, just "Improved ternary operator performance when returning arrays." in the release notes.
Re-running the same benchmarks (5.6.11), gives me slightly faster times for the ternary actually, but probably just noise.
Comment #231
catchI also don't know where the issue is, but remember the same thing with if/else no longer being faster than ternary.
Comment #232
dawehnerAh perfect
Comment #233
wim leersFixed all the nitpicks I could find while going through the patch once more. Also fixed dawehner's nitpick in point 6.
All other points by dawehner I feel should be addressed by Fabian, I'm not well placed to do so.
Below are the few other remarks I found, which I couldn't address.
This looks kinda strange at first sight; one would expect that the optimized one extends the non-optimized one.
Is it worth documenting why the inheritance goes this direction?
"there is no information how deep to traverse" -> that's a broken sentence, let's fix that :)
The documentation and function body suggests this method should be renamed to
supportsMachineFormat().Comment #234
Aki Tendo commented1. Almost any time an InvalidArgumentException is being tested for a runtime assertion is a likely candidate. The question is though, do we expect this container to ever receive the wrong arguments in a healthy system? If yes an assert() can't be used, if no it can and should be if only to make the context clear - that this condition isn't natural and must be due to newly introduced bad code.
2. The keys of the container_definition - are they all mandatory?
3. Might be useful to write an assertion method to analyze the parameter:
The validation method will describe in code what can be passed and would be a help to those new to the code as a whole by making it clear which keys are required, which ones have specific data types. However, since this single assertion has multiple things that can go wrong it needs to function differently from those seen so far. Instead of returning TRUE or FALSE, the assertion would throw InvalidArgumentException with a different error message for each of its tests (frozen not boolen, aliases not an array, etc) and return TRUE if all the tests pass.
4. Should $id 's data type be asserted? It's format? Should assert(is_bool) be checked on ContainerInterface?
5. Again, do the data types of these need to be asserted? Several other methods in this patch can be subjected to this rigor - as a rule only the public methods need to be so tested though.
6. The description is wrong here - We aren't "resolving arguments from stdClass objects to the real values" because we can receive an argument that isn't an stdClass object at all. Further, if you pass this function a string it will trigger a warning then return the string - I think we should hard fail here. What is allowed as an argument here? From glancing at the code it seems it must pass Drupal\Component\Assertion\Inspector::assertTraversable() at a minimum.
7. I personally hate seeing stdClass - ever. There needs to be an interface here - these objects have two boolean properties to track.
8. With the above noted, this is a protected method - those usually don't assert. But where does this data come from beyond this function, and is it being tested there before it gets here? If not then an assertions here may be best rather than repeating the assertion in several other places.
9. Doesn't it run slightly faster to just return the array?
10. Line 1281 -- Everything said on comment #1 applies here. Since this is a child method it could call the same assertion.
Comment #235
fabianx commented#229:
1. Yes, we could test that out as a follow-up.
2. I don't think it is that useful as the format is described in the dumpers and it is also an internal detail.
3. As described already, this is a fatal condition that cannot be recovered from. That is like asking PHP to run a Python script.
4. We definitely could. I think I refrained from doing so as it screws up xdebug for 100% test coverage and @codeCoverageIgnore is not allowed, so you need to define the default properties as constant on the class instead, but yes, we could do that. However that is an internal non-API breaking change we can do at any time, so follow-up?
5. Answered by comments below.
6. Wim fixed already, thanks Wim!
7. Python can't run PHP either.
8. No, fatal error as that is again something which cannot be ignored and should never happen.
9. @todo Will be fixed. (if Wim did not have caught that one).
10. @todo Agree, will be fixed.
11. The caller asks for a status code to log that condition. The cache API does not allow that, so catching the Exception is the only way to report back success from that function.
12. @todo Oh, yes. I wanted to fix that update problem first in update, started cleaning up, then forgot. Lets move that to a new issue.
13. Simpletest tries to load that file, it does not like such Fixtures, so using .data is the way to avoid a fatal.
#233:
1. There is two reasons to use the inheritance in the "wrong" direction:
a) Historically, before this was in Component, it replaced the Core Container, so then the Core Container needed to be called Container.php, not OptimizedPhpArrayContainer.php, so the other way of inheritance would not have worked.
b) Especially for the dumper, all the helper functions just returning little things don't make sense for the more generic PhpArrayDumper class, but do make sense for the OptimizedPhpArrayDumper class.
Question:
Is it worth to move the code around? It is now possible to fix the inheritance direction.
2. Any suggestions? What I want to say is:
The depth of the tree representing the collection is not known in advance, so it needs to be fully traversed.
3. @todo Agree, will be fixed.
Comment #236
fabianx commented#234: Thanks Aki for the review.
We have #2537714: Add assertions for allowing only lowercase parameter and service names in the container and other things for all assertion related things, so those can be follow-up.
1. Yes, it can happen. It is a fatal error condition.
2. No, they are entirely optional.
3. Follow-up
4. Follow-up
5. Follow-up
6. @todo It can be an array of \stdClass objects, too. However we can fix the description somewhat to not mention \stdClass.
7. No, they are pure internal value objects used instead of arrays. The only reason they are used is for raw performance reasons to be able to distinguish arrays from objects as instanceof is a very fast operation and this optimizes the generated PHP machine code in this way.
8. Follow-up
9. Possibly, but that is micro-optimization for something just called once. The difference is - if at all - just one variable assignment less and I prefer that for readability.
10. Follow-up
Comment #237
dawehnerDo you mind opening up all those follow ups? We should not forget about it.
Do you mind documenting it?
Should we at least try to call
watchdog_exception()?Do we have code in DrupalKernel which recovers from it? I mean this exception would be thrown in DrupalKernel and then, well ends somewhere in our error handling system right?
Even a drush cr would then potentially not trigger it anymore. We should maybe think about a clear recovery strategy.
Another point. Given that we add a new component can we add a composer.json file there? I think its fundamental wrong to support php 5.3, I mean I don't even understand the reasons,
from the perspective of core. PHP 5.3 is freaking old, and Drupal 8 is about the present not the past. If you want to really maintain a PHP 5.3 compatible library in contrib, it should not be responsibility of core of doing it.
It could be that supporting 5.3 requires some additional maintenance costs, so why even do the risk.
Comment #238
hussainwebI am fixing comments for the following points. Most of these are based on @Fabianx's responses.
#229: 9, 10, 12, 13
#233: 3
#234: 6
#237: 1, 3
I have added a composer.json file but it needs review. For one thing, I am thinking we could use
drupal/dependency-injectioninstead ofdrupal/core-dependency-injection.Comment #239
bojanz commentedAccidental revert in #238?
Comment #240
fabianx commentedX-POST with #238, posting first, then resolving the conflicts.
Fixed all the things from the last comments up to #236.
#239: No, this was an unrelated change that should not have been in there in the first place. Thanks for noticing that.
Comment #241
hussainweb@bojanz: Actually, no. I did that based on @Fabianx's response in #235.
EDIT: Cross-post again on #240. :)
Comment #242
fabianx commentedThanks, #238. I actually forgot the supportsMachineFormat() change. :)
I will merge that interdiff now into mine.
Comment #243
hussainweb+1 on these. These are better than my changes in #238.
Comment #244
fabianx commentedinterdiff-238-complete.txt is HussainWeb's changes plus mine, but with conflicts resolved. (git diff HEAD~2)
interdiff.txt only changes to the merge of HussainWeb and mine's. (git diff HEAD~1)
Just a little consistency change.
Edit: Opened #2554993: Fix typo in UpdateTestBase.php as follow-up.
Comment #245
fabianx commentedMore feedback:
#237:
1. Fixed by Hussainweb already (thanks so much!)
2. The caller in question does:
3. This is not expected to be triggered ever during production usage. You would to specifically use the wrong container with a wrong dumper combination. That is a 0.00001% case. (not extending DrupalContainer)
4. Hussainweb did that :).
5. We don't need to officially support PHP 5.3 - not at all. I just don't see the reason to make this incompatible for no reason in the initial patch - especially if the only change is pure cosmetic.
Comment #246
dawehnerAlright, thank you for answering the questions!
Everything can be follow ups, also the composer.json file.
Comment #247
fgmA few PHPdoc changes are needed, which does not impact the actual RTBC status once addressed, since these are just doc comments.
typo: Component, not Compontent.
different from, not different to
Maybe be more specific like this: "Thrown when the configurator callable in $definition is not actually callable.". Since the callable is not a parameter itself, it can seem weird to reference it without explaining whence it comes.
It may also throw a \ReflectionException if for some reason the class cannot be instantiated in the 'default' case.
Can also throw InvalidArgumentException (lines 429 and 520)
Can throw InvalidArgumentException (scope != SCOPE_PROTOTYPE, or decorated definition.
Since it /always/ happens, maybe modify the exception comment, like "Always thrown, because this container cannot use expressions."
Comment #248
fgmRerolled to account for these remarks.
Comment #249
alexpottWhy not just throw an exception here rather than in getExpressionCall()?
Dependency is not spelt right
I think it might be possible that $container_definition is undefined.
Not used
I guess
'Not found...'is meant to be the assertion message.Doesn't need to be a FQDN
Let's add a protected property for these.
service
Comment #250
fgm1) done. Maybe we can even remove the getExpressionCall() method, as it's not part of the Dumper interface and not used anywhere else I can see ?
2) done
3) done
4) LogicException is used as an annotation for testSetParameterWithFrozenContainer() : doesn't it need to be imported for the annotation to work ?
5) done, three times in a row
6) done
7) done
8) done
Comment #252
fgmThat was 5), fixed incorrectly all 3 times.
Comment #254
wim leersJust a super silly super tiny docs fix.
Comment #255
alexpottThe point was
$container_definitionmight not be set. Not sure what is actually the correct thing to do here.Comment #256
alexpottAlso we should remove the
getExpressionCall()methodComment #257
wim leersThat's indeed what PHPStorm indicates.
But:
so, we only execute the line
if ($this->containerNeedsDumping && isset($container) && !$this->cacheDrupalContainer($container_definition)) {if$this->containerNeedsDumping === TRUE. The only place where we set that to TRUE, is in::compileContainer(), and then only if$this->allowDumping === TRUE. The only place where we call::compileContainer()is that first if-branch I was quoting. And there we also always generate$container_definitionif$this->allowDumping === TRUE.Therefore
$container_definitionis in fact guaranteed to be set when it is used here.Conclusion: While this part is a bit hard to follow, it's actually correct. It's just PHPStorm that's confused.
Comment #258
alexpottI think we're at the point where this needs to get into real world testing since we don't have automated load tests. Committed 22fbcd4 and pushed to 8.0.x. Thanks!
Minor tidy ups. The
isset($container)is removed because it was in response to an incorrect comment from me see #257 for an excellent explanation.Comment #260
fabianx commentedThanks so much @all for finding those remaining issues and fixing those directly.
Also: YES!!!!!!!
Super simple nit in docs introduced in one interdiff:
DumperDumperInterface should be Dumper\DumperInterface.
Will open a follow-up issue.
Edit: Here is the issue for the above https://www.drupal.org/node/2555565
Edit 2: Published the change records!
Comment #261
chx commentedSomeone please post a third party code policy upgrade because I am utterly lost. So far I believed the policy was that we use everything third party irregardless of quality for all sorts of demented nonreasons. Now this. May I have an update please?
Comment #262
dawehnerchx, please please calm down. Please! We have subclasses other people's code (like the container) since quite a while,
we replaced many implementations with our own etc.
These are the posts which make me sad ...