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

  1. Introduce a new PhpArrayContainer and PhpArrayDumper working on the basis of PHP arrays for service definition
  2. Instead of php storage, use a new cache bin cache_container to store the container definition array
  3. Introduce a bootstrap container providing the cache.container service which by default uses the database cache backend
  4. Introduce a setting bootstrap_container_definition which allows sites to swap out the cache backend used to store the container

The advantage of this approach over a PHP-code compiled container:

  1. 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.
  2. The serialized format is storage agnostic, that means it can be stored in MySQL, Memcache, APC, ...
  3. By abstracting out the storage via the bootstrap container, the storage becomes actually pluggable.
  4. 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.
  5. 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

Reference: https://www.drupal.org/core/beta-changes
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.
CommentFileSizeAuthor
#254 interdiff.txt753 byteswim leers
#254 2497243-fast_container-254.patch137.69 KBwim leers
#252 interdiff-2497243-248-252.diff.txt6.64 KBfgm
#252 2497243-fast_container-252.patch137.68 KBfgm
#250 interdiff-2497243-248-250.diff.txt6.64 KBfgm
#250 2497243-fast_container-250.patch137.69 KBfgm
#248 interdiff-2497243-244-248.diff.txt3.66 KBfgm
#248 2497243-fast_container-248.patch136.88 KBfgm
#244 replace_symfony-2497243-244.patch136.26 KBfabianx
#244 interdiff-238-complete.txt4.94 KBfabianx
#244 interdiff.txt635 bytesfabianx
#240 replace_symfony-2497243-236.patch135.14 KBfabianx
#240 interdiff.txt3.85 KBfabianx
#238 replace_symfony-2497243-238.patch136.15 KBhussainweb
#238 interdiff-233-238.txt6.01 KBhussainweb
#233 interdiff.txt19.56 KBwim leers
#233 replace_symfony-2497243-233.patch135.82 KBwim leers
#228 replace_symfony-2497243-228.patch135.58 KBfabianx
#228 interdiff.txt771 bytesfabianx
#216 rebuilding_service-2497243-216.patch135.36 KBfabianx
#216 interdiff-clean.txt27.19 KBfabianx
#216 interdiff.txt230.22 KBfabianx
#212 rebuilding_service-2497243-212.patch136.37 KBfabianx
#212 interdiff.txt7.35 KBfabianx
#209 rebuilding_service-2497243-209.patch135.41 KBfabianx
#209 interdiff.txt12.26 KBfabianx
#188 rebuilding_service-2497243-188.patch134.01 KBfabianx
#188 interdiff.txt22.28 KBfabianx
#179 rebuilding_service-2497243-178.patch132.49 KBfabianx
#179 interdiff.txt38.91 KBfabianx
#173 rebuilding_service-2497243-173.patch130.62 KBfabianx
#173 interdiff.txt4.3 KBfabianx
#172 interdiff.txt3.65 KBpfrenssen
#172 2497243-172.patch131.01 KBpfrenssen
#169 rebuilding_service-2497243-169.patch130.8 KBfabianx
#169 interdiff.txt3.75 KBfabianx
#167 rebuilding_service-2497243-167.patch130.09 KBfabianx
#167 interdiff.txt3.15 KBfabianx
#167 interdiff-rename.txt156.15 KBfabianx
#165 rebuilding_service-2497243-166.patch128.59 KBfabianx
#165 interdiff.txt25.19 KBfabianx
#160 rebuilding_service-2497243-160.patch129.65 KBfabianx
#160 interdiff.txt6.42 KBfabianx
#158 rebuilding_service-2497243-158.patch128.15 KBfabianx
#158 interdiff.txt1.28 KBfabianx
#156 rebuilding_service-2497243-156.patch128.15 KBfabianx
#154 rebuilding_service-2497243-154.patch127.93 KBfabianx
#154 interdiff.txt65.62 KBfabianx
#154 interdiff-cleanup.txt16.61 KBfabianx
#147 rebuilding_service-2497243-147.patch88.38 KBfabianx
#147 interdiff.txt41.21 KBfabianx
#145 rebuilding_service-2497243-145.patch74.38 KBfabianx
#145 interdiff.txt30.01 KBfabianx
#141 rebuilding_service-2497243-141.patch65.03 KBfabianx
#141 interdiff.txt27.47 KBfabianx
#140 rebuilding_service-2497243-140.patch62.71 KBfabianx
#140 interdiff.txt13.47 KBfabianx
#139 rebuilding_service-2497243-139.patch68.33 KBfabianx
#139 interdiff.txt13.81 KBfabianx
#135 rebuilding_service-2497243-135.patch65.75 KBfabianx
#135 rebuilding_service-2497243-135--on-top.txt54.87 KBfabianx
#135 interdiff.txt2.16 KBfabianx
#133 rebuilding_service-2497243-133.patch66 KBfabianx
#133 interdiff.txt1.24 KBfabianx
#131 rebuilding_service-2497243-131.patch65 KBfabianx
#131 interdiff.txt5.79 KBfabianx
#130 rebuilding_service-2497243-130.patch67.43 KBfabianx
#130 interdiff.txt1.35 KBfabianx
#129 interdiff-before.txt29.63 KBfabianx
#127 rebuilding_service-2497243-127.patch66.08 KBfabianx
#119 rebuilding_service-2497243-119.patch68.53 KBznerol
#119 interdiff.txt1.28 KBznerol
#117 interdiff.txt6.46 KBznerol
#117 rebuilding_service-2497243-117.patch68.51 KBznerol
#114 interdiff-upstream.txt19.83 KBznerol
#114 interdiff.txt21.44 KBznerol
#114 rebuilding_service-2497243-114.patch69.12 KBznerol
#105 rebuilding_service-2497243-105.patch47.67 KBfabianx
#105 interdiff.txt4.46 KBfabianx
#103 rebuilding_service-2497243-103.patch45.37 KBfabianx
#103 interdiff.txt10.11 KBfabianx
#100 rebuilding_service-2497243-100.patch38.92 KBfabianx
#100 interdiff.txt933 bytesfabianx
#98 rebuilding_service-2497243-98.patch38.82 KBfabianx
#98 interdiff.txt11 KBfabianx
#93 rebuilding_service-2497243-93.patch41.14 KBneclimdul
#90 2497243-90.patch44.41 KBdarol100
#88 2497243-87.patch42.18 KBdarol100
#76 rebuilding_service-2497243-76.patch41.21 KBnitesh sethia
#71 stampede-2497243-71.patch57.96 KBjhedstrom
#71 interdiff.txt17.12 KBjhedstrom
#58 rebuilding_service-2497243-58.patch40.85 KBjibran
#57 rebuilding_service-2497243-48.patch45.27 KBfabianx
#38 rebuilding_service-2497243-38.patch40.94 KBfabianx
#38 interdiff.txt2.88 KBfabianx
#36 rebuilding_service-2497243-36.patch41.11 KBfabianx
#33 rebuilding_service-2497243-33.patch41.26 KBfabianx
#33 interdiff.txt11.43 KBfabianx
#29 rebuilding_service-2497243-29.patch34.59 KBfabianx
#29 interdiff.txt2.14 KBfabianx
#25 interdiff.txt661 bytesfabianx
#25 rebuilding_service-2497243-23.patch32.68 KBfabianx
#22 interdiff.txt1.93 KBfabianx
#22 rebuilding_service-2497243-21.patch32.65 KBfabianx
#20 interdiff.txt3.49 KBfabianx
#19 rebuilding_service-2497243-19.patch32.49 KBfabianx
#19 interdiff.txt2 KBfabianx
#17 interdiff.txt2 KBfabianx
#17 rebuilding_service-2497243-17.patch30.48 KBfabianx
#15 rebuilding_service-2497243-15.patch31.04 KBfabianx
#13 rebuilding_service-2497243-13.patch31.75 KBfabianx
#11 fast-container--xhprof-before-after.png68.03 KBfabianx
#10 xhprof-before-after.png90.84 KBfabianx
#6 wheeeeee.png626.8 KBAnonymous (not verified)

Comments

fabianx’s picture

Overall 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.

anavarre’s picture

Issue tags: +Performance
wim leers’s picture

Issue tags: +scalability
moshe weitzman’s picture

Issue tags: -Performance

more of a scalability issue than a performance issue.

msonnabaum’s picture

Issue summary: View changes
Anonymous’s picture

StatusFileSize
new626.8 KB

just 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.

wheee

fabianx’s picture

Report 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.

    <service id="page_cache_response_policy" class="Drupal\Core\PageCache\ChainResponsePolicy" lazy="true">
      <tag name="service_collector" tag="page_cache_response_policy" call="addPolicy"/>
      <property name="_serviceId">page_cache_response_policy</property>
      <call method="addPolicy">
        <argument type="service">
          <service class="Drupal\node\PageCache\DenyNodePreview" public="false">
            <tag name="page_cache_response_policy"/>
            <argument type="service" id="current_route_match"/>
          </service>
        </argument>
      </call>

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.

fabianx’s picture

Assigned: Unassigned » fabianx

I 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.

wim leers’s picture

Looking forward to reading that patch :)

fabianx’s picture

Issue summary: View changes
StatusFileSize
new90.84 KB

It 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.

fabianx’s picture

Assigned: fabianx » Unassigned
Issue summary: View changes
StatusFileSize
new68.03 KB

I 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:

=== SUM: 8_0_x-without-proxy-summary..hack-the-container-summary compared (556addd042899..556addd7c32f3):

[ DIVIDE NUMBERS BY 100 for real number ]

ct  : 6,732,282|6,800,233|67,951|1.0%
wt  : 12,547,608|12,676,685|129,077|1.0%
mu  : 2,418,491,112|2,429,629,752|11,138,640|0.5%
pmu : 2,427,074,488|2,438,754,216|11,679,728|0.5%

---
ct = function calls, wt = wall time, cpu = cpu time used, mu = memory usage, pmu = peak memory usage

### XHPROF-LIB REPORT

+---------------------+------------+------------+------------+------------+------------+
| namespace           |        min |        max |       mean |     median |       95th |
+---------------------+------------+------------+------------+------------+------------+
| Calls               |            |            |            |            |            |
|                     |            |            |            |            |            |
| hack-the-container  |     67,962 |     71,995 |     68,002 |     67,962 |     67,962 |
| 8_0_x-without-proxy |     67,282 |     71,364 |     67,323 |     67,282 |     67,282 |
|                     |            |            |            |            |            |
| Wall time           |            |            |            |            |            |
|                     |            |            |            |            |            |
| hack-the-container  |    117,969 |    187,877 |    126,767 |    126,207 |    132,188 |
| 8_0_x-without-proxy |    116,410 |    183,320 |    125,476 |    124,976 |    129,246 |
|                     |            |            |            |            |            |
| Memory usage        |            |            |            |            |            |
|                     |            |            |            |            |            |
| hack-the-container  | 24,270,496 | 25,001,472 | 24,296,298 | 24,270,496 | 24,304,163 |
| 8_0_x-without-proxy | 24,163,688 | 24,895,392 | 24,184,911 | 24,163,688 | 24,164,332 |
|                     |            |            |            |            |            |
| Peak memory usage   |            |            |            |            |            |
|                     |            |            |            |            |            |
| hack-the-container  | 24,361,448 | 25,093,560 | 24,387,542 | 24,361,688 | 24,402,259 |
| 8_0_x-without-proxy | 24,248,584 | 24,975,072 | 24,270,745 | 24,248,584 | 24,249,647 |
|                     |            |            |            |            |            |
+---------------------+------------+------------+------------+------------+------------+

----

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 ...

fabianx’s picture

Assigned: Unassigned » fabianx

I did not want to unassign ...

fabianx’s picture

Status: Active » Needs review
StatusFileSize
new31.75 KB

It 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.

Status: Needs review » Needs work

The last submitted patch, 13: rebuilding_service-2497243-13.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new31.04 KB

Testbot fluke?

Status: Needs review » Needs work

The last submitted patch, 15: rebuilding_service-2497243-15.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new30.48 KB
new2 KB

Removed 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.

Status: Needs review » Needs work

The last submitted patch, 17: rebuilding_service-2497243-17.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new2 KB
new32.49 KB

KernelTestBase oddities and some hardening on serialized services.

fabianx’s picture

StatusFileSize
new3.49 KB

Real interdiff ...

Status: Needs review » Needs work

The last submitted patch, 19: rebuilding_service-2497243-19.patch, failed testing.

fabianx’s picture

StatusFileSize
new32.65 KB
new1.93 KB

Alias support was missing (should have been resolved by compiler passes, but tests can do runtime things ...)

fabianx’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 22: rebuilding_service-2497243-21.patch, failed testing.

fabianx’s picture

StatusFileSize
new32.68 KB
new661 bytes

Hardening is hard, forgot that ::set takes a NULL argument as well.

fabianx’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 25: rebuilding_service-2497243-23.patch, failed testing.

dawehner’s picture

Just a quick note.

+++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
@@ -0,0 +1,358 @@
+
+    if ($definition->getFactoryClass()) {
+      $service['factory_class'] = $definition->getFactoryClass();
+    }
...
+
+    if ($definition->getFactoryMethod()) {
+      $service['factory_method'] = $definition->getFactoryMethod();
+    }
+
+    if ($definition->getFactoryService()) {
+      $service['factory_service'] = $definition->getFactoryService();
+    }

+++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
@@ -0,0 +1,390 @@
+      'factory_class' => '',
+      'factory_method' => '',
+      'factory_service' => '',
...
+      if (!empty($definition['factory_method'])) {
+        $method = $definition['factory_method'];
+
+        if (!empty($definition['factory_class'])) {
+          $factory = $definition['factory_class'];
+        }
+        elseif (!empty($definition['factory_service'])) {
+          $factory = $this->get($definition['factory_service'], $invalidBehavior);
+        }
+        else {
+          throw new RuntimeException(sprintf('Cannot create service "%s" from factory method without a factory service or factory class.', $name));
+        }
+        $service = call_user_func_array(array($factory, $method), $arguments);
+      }

Do you mind just having either service:method or class::method ? See symfony 2.7 update patch

fabianx’s picture

StatusFileSize
new2.14 KB
new34.59 KB

That 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.

fabianx’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 29: rebuilding_service-2497243-29.patch, failed testing.

dawehner’s picture

Hence suddenly there were two different containers ...

Well, it used to work indeed because the same container builder object got used and assigned in \Drupal\simpletest\KernelTestBase::containerBuild()

Also fixed one dependency on the ContainerBuilder. getDefinition() exists on the container, because it originally had the pluginManager Interface.

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.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,390 @@
    +      if ($argument instanceof \stdClass) {
    +        // We need a unique service name.
    +        $name = 'private__' . $this->privateServicesCounter;
    +        $this->privateServicesCounter++;
    +        $this->serviceDefinitions[$name] = $argument->value;
    +        $arguments[$key] = $this->get($name, $invalidBehavior);
    +        unset($this->serviceDefinitions[$name]);
    +        unset($this->services[$name]);
    +        continue;
    +      }
    

    What kind of special feature of the container is that?

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -56,6 +60,45 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
    +  protected static $bootstrapContainerDefinition = [
    

    should we name this defaultBootstrapContainerDefinition? What is the reason that this is static?

  3. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -735,25 +780,30 @@ protected function initializeContainer($rebuild = FALSE) {
    +      // @todo State service does fail tests atm. as table is not created automatically.
    +      // @todo Use cache service?
    

    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.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new11.43 KB
new41.26 KB

In 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! :)

Status: Needs review » Needs work

The last submitted patch, 33: rebuilding_service-2497243-33.patch, failed testing.

fabianx’s picture

#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 ...

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new41.11 KB

Re-roll ... Symfony 2.7 got in ...

Status: Needs review » Needs work

The last submitted patch, 36: rebuilding_service-2497243-36.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new2.88 KB
new40.94 KB

Proper factory support, removed deprecated methods from dumper to support Symfony 2.7 better.

wim leers’s picture

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 ...)

This sounds super sensible!

wim leers’s picture

Pinged @msonnabaum & @beejeebus for reviews.

fabianx’s picture

#40: Little early for that - we still need to decide where to store the PHP dumped array so that we can efficiently retrieve it.

catch’s picture

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 ...)

Yes! (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.

fabianx’s picture

#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 ...

fabianx’s picture

Opened #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.

fabianx’s picture

StatusFileSize
new7.22 KB
new45.1 KB

Removed all strtolower calls again (Performance!) and incorporated #2498293: Only allow lowercase service and parameter names briefly for now.

fabianx’s picture

StatusFileSize
new3.61 KB
new45.25 KB

Quick 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:

diff --git a/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
index 5bcc6f6..2ca8a91 100644
--- a/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
+++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
@@ -285,10 +285,10 @@ protected function expandArguments($arguments, $invalidBehavior = ContainerInter
     foreach ($arguments as $key => $argument) {
       if ($argument instanceof \stdClass) {
         $name = $argument->id;
-        if (!isset($this->serviceDefinitions[$name])) {
-          $this->serviceDefinitions[$name] = $argument->value;
-        }
+        $this->serviceDefinitions[$name] = $argument->value;
         $arguments[$key] = $this->get($name, $invalidBehavior);
+        unset($this->serviceDefinitions[$name]);
+        unset($this->services[$name]);
         continue;
       }
fabianx’s picture

fabianx’s picture

StatusFileSize
new45.27 KB

Uploaded the wrong patch, interdiff see #46 ...

fabianx’s picture

Assigned: fabianx » Unassigned

Out 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

fabianx’s picture

Issue summary: View changes
wim leers’s picture

Issue summary: View changes

Improving the IS.

Aki Tendo’s picture

It'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

dawehner’s picture

@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.

dawehner’s picture

Assigned: Unassigned » fabianx
Issue summary: View changes
  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,351 @@
    +    if (!$this->container->getParameterBag()->all()) {
    +      return [];
    +    }
    

    This is not obvious, why would ->all() not return something properly?

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,404 @@
    + * @file
    + * Contains \Drupal\Core\DependencyInjection\PhpArray\Container
    + */
    

    I'm curious, is anything in that class drupal specific? Could we move that to a component?

fabianx’s picture

Assigned: fabianx » Unassigned

#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 ...

jibran’s picture

Why #45, #46 and #48 are not sent to testbots?

fabianx’s picture

StatusFileSize
new45.27 KB

Re-uploading, testbot fail I assume.

jibran’s picture

StatusFileSize
new40.85 KB

re-roll

dawehner’s picture

Oh, whw was the last patch 5KB smaller?

chx’s picture

Because 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.

jhedstrom’s picture

+++ b/core/includes/common.inc
diff --git a/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
new file mode 100644

+++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
@@ -0,0 +1,387 @@
+        // @todo Allow dynamic class definitions via parameters.
+        $class = $definition['class'];
+        $length = count($arguments);
+
+        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:

Can't we just use call_user_func_array($class, $arguments) here?

dawehner’s picture

Can't we just use call_user_func_array($class, $arguments) here?

Well the reason here was better performance

fabianx’s picture

#61: Unfortunately no, and that (rather ugly) code is just there to avoid a performance regression due to ReflectionClass being quite costly.

chx’s picture

benjy’s picture

Argument unpacking with ... came in PHP 5.6

chx’s picture

And the comment I linked argues for requiring 5.6 because of variadics. Sorry if that was not clear.

benjy’s picture

Ah sorry I didn't click through. I should have known! 5.6, wouldn't that be awesome :)

catch’s picture

Issue tags: +Triaged D8 critical

We discussed this on the maintainer call and agreed the issue is definitely critical to resolve, because... endless stampede.

dawehner’s picture

  1. +++ b/core/includes/common.inc
    @@ -1309,6 +1309,10 @@ function drupal_flush_all_caches() {
    +  // Remove container definition.
    +  //$keys[] = 'container_definition';
    

    I 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.

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,362 @@
    +/**
    + * PhpArrayDumper dumps a service container as a serialized PHP array.
    + */
    +class PhpArrayDumper extends Dumper
    +{
    

    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.

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,362 @@
    +    if (($scope = $definition->getScope()) !== ContainerInterface::SCOPE_CONTAINER) {
    +      $service['scope'] = $scope;
    +    }
    

    Should we skip the support for scope right in the beginning?

  4. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,362 @@
    +   * Dumps callable to YAML format
    

    Nitpick: its php :)

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,387 @@
    +/**
    + * Container is a DI container that provides services to users of the class.
    + *
    + * @ingroup dic
    + */
    +class Container implements IntrospectableContainerInterface {
    

    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 ...

  6. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,387 @@
    +    if (isset($this->services[$name]) || ($invalidBehavior === ContainerInterface::NULL_ON_INVALID_REFERENCE && array_key_exists($name, $this->services))) {
    

    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

  7. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,387 @@
    +        // @todo Allow dynamic class definitions via parameters.
    

    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

  8. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,387 @@
    +      throw new \BadMethodCallException("Container parameters can't be changed on runtime.");
    

    Let's adapt the message and tell people that the container is frozen instead

  9. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1038,6 +1088,16 @@ protected function attachSynthetic(ContainerInterface $container) {
    +  protected function compileContainerWithLock() {
    +    // @todo Implement lock here.
    +    return $this->compileContainer();
    +  }
    

    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.

  10. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1161,36 +1221,24 @@ protected function getContainerBuilder() {
    +    $code = str_replace("stdClass::__set_state", "(object)", $code);
    

    Do we need that? I thought that its optional ... is that done to make the string smaller?

  11. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1161,36 +1221,24 @@ protected function getContainerBuilder() {
    +    return $this->storage()->save('container_definition.php', '<?php return ' . $code . ';');
    

    storage() is not really a helpful method name to begin with, but that is not the fault of this patch

  12. +++ b/core/vendor/symfony/dependency-injection/Compiler/ResolveParameterPlaceHoldersPass.php
    @@ -30,6 +30,8 @@ class ResolveParameterPlaceHoldersPass implements CompilerPassInterface
    +// @todo Remove compiler pass via getPasses() / setPasses().
    +return;
    

    That part shouldn't be that hard, right?

jhedstrom’s picture

Assigned: Unassigned » jhedstrom

I 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.

jhedstrom’s picture

Assigned: jhedstrom » Unassigned
StatusFileSize
new17.12 KB
new57.96 KB

After 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:

  • This implementation makes assumptions about the cache backends in use (eg, it assumes $item->checksum will be set)
  • Given that, the unit test is failing since the memory cache backend doesn't set checksum at all
  • There are also some other failing cache tests that need to be resolved in this approach
  • I also did not have time to address any of #69 in this patch.

Status: Needs review » Needs work

The last submitted patch, 71: stampede-2497243-71.patch, failed testing.

jhedstrom’s picture

Assigned: Unassigned » jhedstrom
jhedstrom’s picture

Assigned: jhedstrom » Unassigned
Status: Needs work » Needs review

Opened #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.

effulgentsia’s picture

Issue tags: +Needs reroll

#74 says to ignore #71. Latest patch before that is #58, but that needs a reroll.

nitesh sethia’s picture

StatusFileSize
new41.21 KB

Rerolled the patch as per #58.

nitesh sethia’s picture

Issue tags: -Needs reroll

Removing the tag of Rerolling the patch.

chx’s picture

Since we are dumping into PHP why are we serializing?

dawehner’s picture

+ // @todo State service does fail tests atm. as table is not created automatically.
+ // @todo Use cache service?

@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.

fabianx’s picture

Assigned: Unassigned » fabianx
Issue tags: +D8 Accelerate

Using cache is the correct way as that can be easier made pluggable. I have the factory setup locally.

Thanks for the re-rolls @all :).

chx’s picture

I have another idea. We are going PHP 5.5 and PHP 5.5 is opcache land and opcache can cache file:// and phar:// 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 coopt file://

file://foo will be safely passed along while file://///bar or some other crazy invalid path (throw in a few chr(0) for good measure?) will be going against the database.

Edit: phenaproxima suggests file://./foo which 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.

msonnabaum’s picture

Maybe 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?

jhedstrom’s picture

  1. +++ b/core/includes/common.inc
    @@ -1304,6 +1304,19 @@ function drupal_flush_all_caches() {
    +  // Remove the cache of the active theme's info file information in state.
    +  // @see \Drupal\Core\Theme\ThemeInitialization::getActiveByThemeName()
    +  $keys = [];
    +  foreach ($theme_handler->listInfo() as $theme_name => $theme) {
    +    $keys[] = 'theme.active_theme.' . $theme_name;
    +  }
    

    Given #2499943: system.active_theme.THEMENAME state should not cache theme data, is this still needed?

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,362 @@
    +/*
    +    $tagsCode = '';
    +    foreach ($definition->getTags() as $name => $tags) {
    +      foreach ($tags as $attributes) {
    +        $att = array();
    +        foreach ($attributes as $key => $value) {
    +          $att[] = sprintf('%s: %s', $this->dumper->dump($key), $this->dumper->dump($value));
    +        }
    +        $att = $att ? ', '.implode(', ', $att) : '';
    +
    +        $tagsCode .= sprintf("      - { name: %s%s }\n", $this->dumper->dump($name), $att);
    +      }
    +    }
    +    if ($tagsCode) {
    +      $code .= "    tags:\n".$tagsCode;
    +    }
    +*/
    

    This code is commented out, so just remove it?

jhedstrom’s picture

+++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
@@ -0,0 +1,362 @@
+use Symfony\Component\ExpressionLanguage\Expression;

We 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.

darol100’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
error: patch failed: core/lib/Drupal/Core/DrupalKernel.php:1161
error: core/lib/Drupal/Core/DrupalKernel.php: patch does not apply
darol100’s picture

Assigned: fabianx » darol100

I'm working on the re-roll.

darol100’s picture

Status: Needs work » Needs review

@jhedstrom,

Do you want me to created a patch with the suggestion from #83 and #84 ? Or are we are waiting feedback ?

darol100’s picture

StatusFileSize
new42.18 KB

Here is the re-roll patch. This patch does not have any of the suggestion from #83 or #84.

Status: Needs review » Needs work

The last submitted patch, 88: 2497243-87.patch, failed testing.

darol100’s picture

StatusFileSize
new44.41 KB

I forgot to close a bracket. Sorry..

darol100’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 90: 2497243-90.patch, failed testing.

neclimdul’s picture

Status: Needs work » Needs review
StatusFileSize
new41.14 KB

I think you might be having some trouble with manually applying the changes. here's a rebase/merge.

neclimdul’s picture

I 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/php just shows an empty container directory for 100's of thousands of requests.

fabianx’s picture

Assigned: darol100 » fabianx

Uhm, yes I am working on loading the definition from the DB.

Status: Needs review » Needs work

The last submitted patch, 93: rebuilding_service-2497243-93.patch, failed testing.

fabianx’s picture

To 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.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new11 KB
new38.82 KB

Cleanup, Fixes, Next: Tests

Status: Needs review » Needs work

The last submitted patch, 98: rebuilding_service-2497243-98.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new933 bytes
new38.92 KB

->set does not return a value, so the best we can do is to check for an Exception happening.

Status: Needs review » Needs work

The last submitted patch, 100: rebuilding_service-2497243-100.patch, failed testing.

dawehner’s picture

Just some early/quick feedback

  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -805,7 +828,7 @@ protected function initializeContainer($rebuild = FALSE) {
    +    if ($this->containerNeedsDumping && !$this->cacheDrupalContainer($container_definition)) {
           $this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be written to disk');
         }
    

    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

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1159,35 +1182,23 @@ protected function getContainerBuilder() {
    +      // -1 == Cache::Permanent, hardcoded for performance reasons.
    +      $this->bootstrapContainer->get('cache.container')->set($this->getCacheKey(), $container_definition, -1, ['container']);
    

    I really don't get that. How are you able to deal with the cache, but then not load CacheBackendInterface, where the constant exists ...

  3. +++ b/core/lib/Drupal/Core/Site/Settings.php
    @@ -107,15 +107,17 @@ public static function getAll() {
        *   exposed to the local scope of settings.php, so as to allow it to be
        *   decorated with Symfony's ApcClassLoader, for example.
    +   * @param array $settings
    +   *   Initial settings, set by DrupalKernel.
        *
        * @see default.settings.php
        */
       public static function initialize($app_root, $site_path, &$class_loader) {
         // Export these settings.php variables to the global namespace.
         global $base_url, $config_directories, $config;
    -    $settings = array();
         $config = array();
         $databases = array();
    +    $settings = array();
     
         if (is_readable($app_root . '/' . $site_path . '/settings.php')) {
           require $app_root . '/' . $site_path . '/settings.php';
    

    All the changes here are pretty pointless

  4. +++ b/core/lib/Drupal/Core/Site/Settings.php
    @@ -107,15 +107,17 @@ public static function getAll() {
    +   * @param array $settings
    +   *   Initial settings, set by DrupalKernel.
    ...
       public static function initialize($app_root, $site_path, &$class_loader) {
    

    Seems not :)

  5. +++ b/core/modules/simpletest/src/KernelTestBase.php
    @@ -298,7 +299,7 @@ public function containerBuild(ContainerBuilder $container) {
    -      ->addArgument(Database::getConnection())
    +      ->addArgument(new Reference('database'))
    

    How did that ever worked?

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new10.11 KB
new45.37 KB

New 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.

Status: Needs review » Needs work

The last submitted patch, 103: rebuilding_service-2497243-103.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new4.46 KB
new47.67 KB

The 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.

fabianx’s picture

Self-review:

Also need to port the unit tests over from service_container, but needs to port them to prophecy (as they use Mockery currently).

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,344 @@
    +    if ($definition->getFile()) {
    +      $service['file'] = $definition->getFile();
    +    }
    

    @todo Need to support that in PhpArray/Container.

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,344 @@
    +    if (($scope = $definition->getScope()) !== ContainerInterface::SCOPE_CONTAINER) {
    +      $service['scope'] = $scope;
    +    }
    

    @todo per it being deprecated in Symfony 3.0, we should probably throw an Exception already here and not at run-time.

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,344 @@
    +    if (($decorated = $definition->getDecoratedService()) !== NULL) {
    +      $service['decorates'] = $decorated;
    +    }
    

    @todo Need to support that in PhpArray/Container. (Symfony test suite seems to have not tested that in the tests that I did run ...)

  4. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,344 @@
    +    if ($callable = $definition->getConfigurator()) {
    +      $service['configurator'] = $this->dumpCallable($callable);
    +    }
    

    @todo Need to support that in PhpArray/Container.

    The current implementation is buggy.

    Unit tests will show.

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,344 @@
    +        'alias' => '@' . $id,
    +      ];
    +    } else {
    +      return [
    +        'alias' => '@' . $id,
    +        'public' => FALSE,
    +      ];
    

    That is the wrong syntax as found out in 'service_container' module.

    Need to sync fix from "upstream" base.

  6. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,344 @@
    +      'type' => 'service',
    

    Should be:

    type => 'private_service'

  7. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,344 @@
    +    } elseif ($value instanceof Expression) {
    +      return $this->getExpressionCall((string) $value);
    

    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.

  8. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,344 @@
    +      if (isset($value->_serviceId)) {
    +        return '@' . $value->_serviceId;
    +      }
    

    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.

  9. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,344 @@
    +    if (null !== $reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) {
    +      return sprintf('@?%s', $id);
    +    }
    

    This is supported by current container, but a little buggy as the default behavior is overwritten, needs to be fixed.

  10. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,389 @@
    +  /**
    +   * The instantiated services.
    +   *
    +   * @var array
    +   */
    +  protected $services = array();
    

    The best way to ensure private services are shared (yes they are), is to add a $privateServices array here.

  11. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,389 @@
    +    ); // @codeCoverageIgnore
    

    Just remove and push default definition as a property of the class instead.

  12. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,389 @@
    +    try {
    +      if (!empty($definition['arguments'])) {
    +        $arguments = $this->expandArguments($definition['arguments'], $invalidBehavior);
    +      } else {
    +        $arguments = array();
    +      }
    ...
    +    catch (\Exception $e) {
    +      unset($this->loading[$name]);
    +      throw $e;
    +    }
    

    This try { catch block is the one to deal with the passed invalidBehavior value, not expandArguments, etc.

  13. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,389 @@
    +      elseif (!empty($definition['factory_method'])) {
    +        $method = $definition['factory_method'];
    +
    +        if (!empty($definition['factory_class'])) {
    +          $factory = $definition['factory_class'];
    +        }
    +        elseif (!empty($definition['factory_service'])) {
    +          $factory = $this->get($definition['factory_service'], $invalidBehavior);
    +        }
    +        else {
    +          throw new RuntimeException(sprintf('Cannot create service "%s" from factory method without a factory service or factory class.', $name));
    +        }
    

    This is deprecated and can be removed.

  14. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,389 @@
    +        // @todo Allow dynamic class definitions via parameters.
    

    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.

  15. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,389 @@
    +    foreach ($definition['calls'] as $call) {
    +      $method = $call[0];
    +      $arguments = array();
    +      if (!empty($call[1])) {
    +        $arguments = $this->expandArguments($call[1], $invalidBehavior);
    +      }
    +      call_user_func_array(array($service, $method), $arguments);
    +    }
    

    This is usually part of the other try { catch } block.

  16. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,389 @@
    +    if (isset($definition['configurator'])) {
    +      $configurator = $definition['configurator'];
    +      if (is_array($configurator)) {
    +        $configurator = $this->expandArguments($configurator);
    +      }
    ...
    +      call_user_func_array(array($service, $method), $arguments);
    

    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).

  17. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,389 @@
    +  public function set($id, $service, $scope = self::SCOPE_CONTAINER) {
    +    if (isset($service)) {
    +      $service->_serviceId = $id;
    +    }
    +    $this->services[$id] = $service;
    +  }
    

    Should throw an Exception for any scope not self::SCOPE_CONTAINER.

  18. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
    @@ -0,0 +1,389 @@
    +      if ($argument instanceof \stdClass) {
    +        $name = $argument->id;
    +        $this->serviceDefinitions[$name] = $argument->value;
    +        $arguments[$key] = $this->get($name, $invalidBehavior);
    +        unset($this->serviceDefinitions[$name]);
    +        unset($this->services[$name]);
    +        continue;
    +      }
    

    Missing comment:

    // This is for private service instantiations.

    Also private services can safely be 'cached' as they can and usually are shared.

  19. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -55,6 +55,38 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
    +      'cache_tags_provider.container' => [
    +        'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum',
    +        'arguments' => ['@database'],
    +      ],
    

    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?

fabianx’s picture

Assigned: fabianx » Unassigned
Issue tags: -Needs reroll

If 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.

znerol’s picture

Issue summary: View changes

Adding issue summary template with proposed resolution section.

znerol’s picture

Issue summary: View changes

Adding remaining tasks section

znerol’s picture

Re #54

+++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
@@ -0,0 +1,404 @@
+ * @file
+ * Contains \Drupal\Core\DependencyInjection\PhpArray\Container
+ */

I'm curious, is anything in that class drupal specific? Could we move that to a component?

+++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
@@ -0,0 +1,389 @@
+  public function set($id, $service, $scope = self::SCOPE_CONTAINER) {
+    if (isset($service)) {
+      $service->_serviceId = $id;
+    }
+    $this->services[$id] = $service;
+  }

The _serviceId attribute on service instances is Drupal specific.

fabianx’s picture

#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.

znerol’s picture

Assigned: Unassigned » znerol

Nothing in the interface says that this can or cannot be done.

Not 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.

xjm’s picture

Issue tags: +D8 Accelerate London
znerol’s picture

StatusFileSize
new69.12 KB
new21.44 KB
new19.83 KB

This contains the minimum amount of changes to make tests pass (except alias, which currently does not work).

znerol’s picture

+++ b/core/lib/Drupal/Core/DependencyInjection/PhpArray/Container.php
@@ -0,0 +1,389 @@
+          default:
+            $r = new ReflectionClass($class);
+            $service = $r->newInstanceArgs($arguments);
+            break;
...
+    if (isset($definition['configurator'])) {
+      $configurator = $definition['configurator'];
+      if (is_array($configurator)) {
+        $configurator = $this->expandArguments($configurator);
+      }
+
+      $method = $configurator[1];
+      $arguments = array();
+      if (!empty($configurator[0])) {
+        $arguments = [ $configurator[0] ];
+      }
+
+      call_user_func_array(array($service, $method), $arguments);
+    }
...
+      throw new InvalidArgumentException(sprintf('You have requested a non-existent parameter "%s".', $name));

Coverage report indicates missing test coverage in this areas.

Status: Needs review » Needs work

The last submitted patch, 114: rebuilding_service-2497243-114.patch, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new68.51 KB
new6.46 KB

Moving the test double into the container test.

Status: Needs review » Needs work

The last submitted patch, 117: rebuilding_service-2497243-117.patch, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new1.28 KB
new68.53 KB

It 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.

Status: Needs review » Needs work

The last submitted patch, 119: rebuilding_service-2497243-119.patch, failed testing.

fabianx’s picture

Assigned: znerol » fabianx

Taking over again

znerol’s picture

Filed #2527478: Resolve infinite stampede in mtime protected PHP storage which fixes the issues with NFS outlined in the issue summary.

fabianx’s picture

Thanks 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.

fabianx’s picture

Spoke 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

fabianx’s picture

Quick note to self:

- Prevent overwriting 'service_container' in set

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new66.08 KB

Re-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.

Status: Needs review » Needs work

The last submitted patch, 127: rebuilding_service-2497243-127.patch, failed testing.

fabianx’s picture

StatusFileSize
new29.63 KB

Here is the interdiff from before - not that helpful as it contained newer changes fixed in other issues, but provided for completeness.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new1.35 KB
new67.43 KB

Fixed 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.

fabianx’s picture

StatusFileSize
new5.79 KB
new65 KB

Cleanup, preparation to split all test changes out.

Status: Needs review » Needs work

The last submitted patch, 131: rebuilding_service-2497243-131.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new1.24 KB
new66 KB

Yes, I obviously missed something.

Fixed by updating the container after updateModules calls.

Status: Needs review » Needs work

The last submitted patch, 133: rebuilding_service-2497243-133.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new2.16 KB
new54.87 KB
new65.75 KB

Opened #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.

bjaspan’s picture

Hi. 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.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -1,29 +1,393 @@
    + * Container is a DI container that provides services to users of the class.
    

    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. :-)

  2. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -770,25 +789,30 @@ protected function initializeContainer() {
    +    $class = Settings::get('container_base_class', '\Drupal\Core\DependencyInjection\Container');
    

    It is not obvious to me why this is not BOOTSTRAP_CONTAINER_BASE_CLASS. Worth an explanatory comment?

chx’s picture

> 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

fabianx’s picture

#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?

fabianx’s picture

StatusFileSize
new13.81 KB
new68.33 KB

Started with the cleanup, other issue is in good shape, but likely need a different solution.

Added lots of documentation to the Container class.

fabianx’s picture

StatusFileSize
new13.47 KB
new62.71 KB

Opened #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.

fabianx’s picture

StatusFileSize
new27.47 KB
new65.03 KB

Re-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.

wim leers’s picture

This is now an exact replica of ContainerBuilder, just way faster and based on arrays and \stdClass.

Probably the answer is no, but I feel like we have to ask the question at least: Any chance we can get this into upstream?.

fabianx’s picture

#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?

wim leers’s picture

but I would like to not make a critical dependent on an upstream decision

Of course not! :) Major follow-up.

fabianx’s picture

StatusFileSize
new30.01 KB
new74.38 KB

And 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

Status: Needs review » Needs work

The last submitted patch, 145: rebuilding_service-2497243-145.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new41.21 KB
new88.38 KB

Okay, thats it:

- Split off: done
- Cleanup: done
- Benchmarks: done

Missing:

- Full unit test coverage (65% currently)
- Reviews

Current Benchmarks

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.

wim leers’s picture

This 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.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    + * Provides an interface compatible alternative to the default Symfony
    

    s/interface compatible/interface-compatible/

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    + * dependency injection container specially optimized for Drupal's needs.
    

    s/specially//

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    + * with a structure very similar to the YAML file format.
    

    s/to the YAML file format/to the YAML container definition/

  4. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +class BootstrapContainer extends Container {
    

    This suggests it's not a generic container, but one for bootstrap services only. Which contradicts with the class docs.

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +    if (isset($container_definition['machine_format']) && $container_definition['machine_format'] === TRUE) {
    +      throw new InvalidArgumentException('The machine format is not supported by this class. Use a human-readable format instead.');
    +    }
    

    When can this occur? An @see could be helpful here.

  6. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +    // Do not call the parents constructor as it would bail on the machine format.
    

    80 cols.

  7. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +    if (isset($definition['synthetic']) && $definition['synthetic'] === TRUE) {
    +      throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id));
    +    }
    

    I think this means synthetic services are not supported? If so, why not have the exception say that instead?

  8. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +      // @todo All of this is deprecated in Symfony 2.7 - remove it?
    +      $method = $definition['factory_method'];
    

    D8 HEAD just started using Symfony 2.7. Does this mean we can already remove this?

  9. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +      switch ($length) {
    +        case 0:
    ...
    +        case 1:
    ...
    +        case 10:
    ...
    +        default:
    

    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.

  10. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +    if (isset($definition['configurator'])) {
    ...
    +        throw new InvalidArgumentException(sprintf('The configure callable for class "%s" is not a callable.', get_class($service)));
    

    s/configure/configurator/ ?

  11. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +        // Create private service.
    +        if ($type == 'private_service') {
    

    Why does this only need to care about private services, and not public services?

  12. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +          // Does the private service already exist.
    

    Bad sentence.

  13. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +          // Create the private service.
    

    Same comment as above; confusing.

  14. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    + * Provides an interface compatible alternative to the default Symfony
    + * dependency injection container specially optimized for Drupal's needs.
    

    Hrm, same as my first comment. I suspect that therefore the docs in my first comment are wrong, and theyre they are correct.

  15. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    + * This container is based on a PHP array container definition with a structure
    + * very similar to the YAML file format.
    

    Same remarks as my second comment.

  16. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    + * - It only allows lowercase service and parameter names, though it does not
    + *   enforce it for performance reasons.
    

    Let's add a todo to add an assertion for checking the lowercaseness once #2408013: Adding Assertions to Drupal - Test Tools. lands?

  17. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +  protected $parameters = array();
    ...
    +  protected $aliases = array();
    ...
    +  protected $serviceDefinitions = array();
    ...
    +  protected $privateServices = array();
    ...
    +  protected $loading = array();
    

    Let's use the short array syntax notation.

  18. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +    if (!empty($container_definition) && (!isset($container_definition['machine_format']) || $container_definition['machine_format'] !== TRUE)) {
    +      throw new InvalidArgumentException('The human readable format is not supported by this class. Use a machine-readable format instead.');
    +    }
    

    Same remark as earlier.

  19. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +    // is used, the actual wanted behavior is to re-try getting the service at a later point.
    

    80 cols.

  20. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +  protected function createService($definition, $id) {
    

    Missing docs.

  21. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +      throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id));
    

    Same remark as above.

  22. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +      // @todo All of this is deprecated in Symfony 2.7 - remove it?
    

    Same remark as above.

  23. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +   * For performance reasons also a \stdClass based approach is supported.
    

    What does this mean?

  24. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/DebugPhpArrayDumper.php
    @@ -0,0 +1,76 @@
    +class DebugPhpArrayDumper extends PhpArrayDumper
    +{
    
    +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +class PhpArrayDumper extends Dumper
    +{
    

    We put opening curly braces on the same line.

  25. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +   * Returns the aliases as a PHP Array.
    

    s/Array/array/

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    + * @file
    + * Contains \Drupal\Core\DependencyInjection\Container.
    + */
    ...
    + * Provides an interface compatible alternative to the default Symfony
    + * dependency injection container specially optimized for Drupal's needs.
    ...
    +class BootstrapContainer extends Container {
    

    Is that a copy and paste from somewhere? Why is that called BootstrapContainer ....

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +    if (isset($container_definition['machine_format']) && $container_definition['machine_format'] === TRUE) {
    ...
    +    if (isset($definition['synthetic']) && $definition['synthetic'] === TRUE) {
    

    You could use just !empty() instead.

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  protected function createService($definition, $id) {
    

    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

  4. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +      throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id));
    

    Let's not use short names here.

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +    if (isset($definition['file'])) {
    +      $file = $this->frozen ? $definition['file'] : current($this->resolveServicesAndParameters(array($definition['file'])));
    +      require_once $file;
    +    }
    

    Does that mean we could have used 'file' for proxies?

  6. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +      // @todo All of this is deprecated in Symfony 2.7 - remove it?
    +      $method = $definition['factory_method'];
    +
    +      if (!empty($definition['factory_class'])) {
    +        $factory = $definition['factory_class'];
    +      }
    +      elseif (!empty($definition['factory_service'])) {
    +        $factory = $this->get($definition['factory_service']);
    +      }
    +      else {
    +        throw new RuntimeException(sprintf('Cannot create service "%s" from factory method without a factory service or factory class.', $id));
    +      }
    

    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

  7. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +
    +      switch ($length) {
    +        case 0:
    

    Do you mind putting some documentation why we need this construct (avoid reflection, if possible)

  8. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,245 @@
    +      // Optimized code path.
    

    Do you mind explaining in which way this is a optimized code path?

  9. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +  public function __construct(array $container_definition = array(), $frozen = FALSE) {
    

    I'm curious, why would it make sense to have a container without any container definition?

  10. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +
    +  protected function createService($definition, $id) {
    

    Needs docs with maybe some high level overview

  11. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +   *
    +   * For performance reasons also a \stdClass based approach is supported.
    +   *
    

    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

  12. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,554 @@
    +    throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__));
    ...
    +    throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__));
    ...
    +    throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__));
    ...
    +    throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__));
    ...
    +    throw new \BadMethodCallException(sprintf("'%s' is not implemented", __FUNCTION__));
    

    We could explicit tell that its not supported, you see the small difference to not implemented

  13. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/DebugPhpArrayDumper.php
    @@ -0,0 +1,76 @@
    +   $code = array();
    +
    ...
    +      return sprintf('@?%s', $id);
    ...
    +    return sprintf('@%s', $id);
    ...
    +    return sprintf('%%%s%%', $name);
    

    Any reason why we can't just use string concat.?

  14. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +   * @return array
    

    Note: Many places in this patch could use string[]

  15. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +      while (isset($aliases[$id])) {
    +        $id = (string) $aliases[$id];
    +      }
    +      $alias_definitions[$alias] = $id;
    

    Do we need protection against infinite aliases or is that solved already on the container build time?

  16. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -770,25 +789,40 @@ protected function initializeContainer() {
    +      } catch (\Exception $e) {
    +        // Ignore exceptions thrown by the database. In case it is really not
    +        // available, it will fail during container compilation.
    +        $cache = FALSE;
    

    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?

  17. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1186,35 +1218,22 @@ protected function getContainerBuilder() {
    +    catch (\Exception $e) {
    +      $saved = FALSE;
         }
    

    Another catch all, which is IMHO a bad idea

  18. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/ContainerTest.php
    @@ -0,0 +1,734 @@
    +class ContainerTest extends \PHPUnit_Framework_TestCase {
    

    +1 for not extending from UnitTestCase unless you need it

  19. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/ContainerTest.php
    @@ -0,0 +1,734 @@
    +  public function test_resolveServicesAndParameters_optional() {
    

    Is there any reason you did not went with camelCase?

  20. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/ContainerTest.php
    @@ -0,0 +1,734 @@
    +  protected function getMockContainerDefinition() {
    

    I don't see properties nor configurator in the definitions. I think for proper test coverage we should provide them as well, right?

neclimdul’s picture

+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -52,6 +50,41 @@
+   * Holds the boostrap container instance.

ghostly boostrap typo. Also it doesn't hold the interface it hold the bootstrap container.

fabianx’s picture

#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.

fabianx’s picture

#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:

$container = new Container();
$container->setParameter('x','y');
$container->set('foo', $foo);
$container->get('foo');

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.

dawehner’s picture

Thank you fabian for the quick response!

12. That's _your_ original wording :p. Yes, lets use not supported.

Ha :)

15. Should be solved by ContainerBuilder pass. Could add if we really wanted to.

Are you sure there is not something already? Well I think this is a small detail.

16. InstallerTest fails without it. The database is not available at first.

We should document this then. This is about early installer then, I guess?

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.

Another place where some docs would be great!

20. Properties should be there, Configurator is indeed missing. Test coverage shows that.

Ah, great!

fabianx’s picture

StatusFileSize
new16.61 KB
new65.62 KB
new127.93 KB

Please 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.

Status: Needs review » Needs work

The last submitted patch, 154: rebuilding_service-2497243-154.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new128.15 KB

Clean rebase, conflicted on UncaughtExceptionTest, but resolved automatically.

Status: Needs review » Needs work

The last submitted patch, 156: rebuilding_service-2497243-156.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new1.28 KB
new128.15 KB

Fixtures need to have a non-php extension.

Status: Needs review » Needs work

The last submitted patch, 158: rebuilding_service-2497243-158.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new6.42 KB
new129.65 KB

#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.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,269 @@
    +use ReflectionClass;
    ...
    +          $r = new ReflectionClass($class);
    
    +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,570 @@
    +use ReflectionClass;
    

    nitpick: We don't use use statements for global level classes but rather \ReflectionClass itself

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,269 @@
    +    elseif (isset($definition['factory_method'])) {
    +      // @todo All of this is deprecated in Symfony 2.7 - remove it?
    +      $method = $definition['factory_method'];
    +
    +      if (!empty($definition['factory_class'])) {
    +        $factory = $definition['factory_class'];
    +      }
    +      elseif (!empty($definition['factory_service'])) {
    +        $factory = $this->get($definition['factory_service']);
    +      }
    +      else {
    +        throw new RuntimeException(sprintf('Cannot create service "%s" from factory method without a factory service or factory class.', $id));
    +      }
    +      $service = call_user_func_array(array($factory, $method), $arguments);
    

    Doesn't the original container throws an E_DEPRECTATED warning if factory_method/factory_class/factory_service is used?

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,269 @@
    +      $length = isset($definition['arguments_count']) ? $definition['arguments_count'] : count($arguments);
    +
    +      $length = count($arguments);
    

    I think the second $length = is wrong

  4. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,269 @@
    +
    +        if ($type !== NULL) {
    +          throw new InvalidArgumentException(sprintf('Undefined type "%s" while resolving parameters and services.', $type));
    +        }
    

    Would that be a good place for an assertion in the future?

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/BootstrapContainer.php
    @@ -0,0 +1,269 @@
    +  }
    +}
    
    +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +class PhpArrayDumper extends Dumper {
    +  /**
    +   * {@inheritdoc}
    

    Missing new line

  6. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,570 @@
    +use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
    

    not used

  7. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,570 @@
    + * Provides an interface compatible alternative to the default Symfony
    + * dependency injection container optimized for Drupal's needs.
    

    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 ...

  8. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,570 @@
    + *
    + * As the container is unfrozen by default, a second parameter can be passed to
    + * the container to "freeze" the parameter bag.
    

    Should we explain when the parameters should be frozen? I would argue that it should be frozen at least once $this->handle() is executed?

  9. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,570 @@
    + * - Scopes are explicitly not allowed, because Symfony 3.0 has deprecated
    + *   them.
    + * - Synchronized services are explicitly not supported, because Symfony 3.0 has
    + *   deprecated them.
    

    Afaik symfony 3.0 will remove both of those features

  10. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/DebugPhpArrayDumper.php
    @@ -0,0 +1,75 @@
    +/*
    
    +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +/*
    

    Missing "*"

  11. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/DebugPhpArrayDumper.php
    @@ -0,0 +1,75 @@
    +use Symfony\Component\DependencyInjection\Alias;
    +use Symfony\Component\DependencyInjection\ContainerInterface;
    +use Symfony\Component\DependencyInjection\Definition;
    +use Symfony\Component\DependencyInjection\Parameter;
    +use Symfony\Component\DependencyInjection\Reference;
    +use Symfony\Component\DependencyInjection\Exception\RuntimeException;
    +use Symfony\Component\DependencyInjection\ContainerBuilder;
    +use Symfony\Component\DependencyInjection\Dumper\Dumper;
    +use Symfony\Component\ExpressionLanguage\Expression;
    

    Just ContainerInterface is used

  12. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +use Symfony\Component\DependencyInjection\Alias;
    ...
    +use Symfony\Component\DependencyInjection\ContainerBuilder;
    

    unused

  13. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    + * The format of this dumper is very similar to the ContainerBuilder based
    + * format, but based on PHP arrays and \stdClass objects instead of rich value
    + * objects for performance reasons.
    

    I think it would be still really nice if you could explain here, why this is faster

  14. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +  public function getArray() {
    

    Nothing uses this method afaik, so this could be protected?

  15. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +   * @param \Symfony\Component\DependencyInjection\Definition $definition
    ...
    +  protected function getServiceDefinition($definition) {
    ...
    +   * @param callable $callable
    +   *   The callable to process.
    ...
    +  protected function dumpCallable($callable) {
    

    Do you mind adding a typehint?

  16. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +   * @param mixed $collection
    +   *   A collection to process.
    +   *
    ...
    +  protected function dumpCollection($collection, &$resolve = FALSE) {
    

    Resolve is not documented yet

  17. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,477 @@
    +  protected function getReferenceCall($id, Reference $reference = null) {
    ...
    +    if ($reference !== NULL) {
    

    null vs. NULL

  18. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -52,6 +50,41 @@
    +  const BOOTSTRAP_CONTAINER_BASE_CLASS = '\Drupal\Core\DependencyInjection\BootstrapContainer';
    +  const PHP_ARRAY_DUMPER_CLASS = '\Drupal\Core\DependencyInjection\Dumper\PhpArrayDumper';
    

    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?

  19. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -711,24 +740,14 @@ public function updateModules(array $module_list, array $module_filenames = arra
       /**
    -   * Returns the classname based on environment.
    +   * Returns the cache key based on environment.
        *
        * @return string
    -   *   The class name.
    +   *   The cache key.
        */
    -  protected function getClassName() {
    +  protected function getCacheKey() {
    

    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

  20. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Dumper/PhpArrayDumperTest.php
    @@ -0,0 +1,579 @@
    +use Symfony\Component\DependencyInjection\Exception\LogicException;
    +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
    

    Unused

  21. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Dumper/PhpArrayDumperTest.php
    @@ -0,0 +1,579 @@
    +    $this->containerBuilder = $this->prophesize('\Symfony\Component\DependencyInjection\ContainerBuilder');
    +    $this->containerBuilder->getAliases()->willReturn(array());
    +    $this->containerBuilder->getParameterBag()->willReturn(new ParameterBag());
    +    $this->containerBuilder->getDefinitions()->willReturn(NULL);
    

    Jus that shows how much easier it is than ordinary phpunit mocking

  22. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/ContainerTest.php
    @@ -0,0 +1,1185 @@
    + * Contains \Drupal\Tests\Core\DependencyInjection\PhpArray\ContainerTest.
    ...
    +namespace Drupal\Tests\Core\DependencyInjection\PhpArray;
    

    Do you mind moving it to the more appropriate namespace: Drupal\Tests\Core\DependencyInjection ?

  23. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/PhpArray/ContainerTest.php
    @@ -0,0 +1,1185 @@
    +   * @var \Drupal\Core\DependencyInjection\PhpArray\Container
    

    That class does not exist

fabianx’s picture

#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 ...

chx’s picture

callable is a typehint since 5.4 . Sincerely: your obscure PHP fact collector.

dawehner’s picture

19. Lets call it getContainerCacheKey, that is way more obvious.

+1

+ $this->containerBuilder = $this->prophesize('\Symfony\Component\DependencyInjection\ContainerBuilder');

You should be able to use ::class for mocks in PHP 5.5 now.

fabianx’s picture

StatusFileSize
new25.19 KB
new128.59 KB

Here is #162 addressing #161.

Not yet finished the rename, are there any helpful tools to help with that?

dawehner’s picture

Not yet finished the rename, are there any helpful tools to help with that?

PhpStorm /me hides

fabianx’s picture

StatusFileSize
new156.15 KB
new3.15 KB
new130.09 KB

- 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.

Status: Needs review » Needs work

The last submitted patch, 167: rebuilding_service-2497243-167.patch, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new3.75 KB
new130.8 KB

Uhm, yes.

It helps to run unit tests before pushing a patch ...

- Fixed tests and test coverage (back to 100%)

fabianx’s picture

Added #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.

dawehner’s picture

Just another small review.

+++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
@@ -608,4 +610,14 @@ public function __sleep() {
+  /**
+   * Retrieves all defined service ids.
+   *
+   * @return array
+   *   An array of all defined service ids.
+   */
+  public function getServiceIds() {
+    return array_unique(array_merge(array_keys($this->serviceDefinitions), array_keys($this->services)));
+  }

+++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
@@ -36,6 +36,13 @@
   /**
+   * Whether to serialize service definitions or not.
+   *
+   * @var bool
+   */
+   protected $serialize = TRUE;

This needs documentation of why we are doing it.

We should open an upstream issue to add it to the Interface for 3.0.

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.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,599 @@
    +use ReflectionClass;
    

    nitpick: use \ReflectionClass ...

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,599 @@
    +   * @throws Symfony\Component\DependencyInjection\Exception\RuntimeException\RuntimeException When the service is a synthetic service.
    

    I think we move the explanation into its own line

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,504 @@
    +   * @param bool  $escape
    

    nitpick: two spaces before $escape

  4. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArrayContainer.php
    @@ -0,0 +1,261 @@
    +      switch ($length) {
    +        case 0:
    +          $service = new $class();
    +          break;
    +        case 1:
    

    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

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArrayContainer.php
    @@ -0,0 +1,261 @@
    +      // Forward compatibility fix for Symfony 2.8 update.
    +      if (!isset($definition['shared']) || $definition['shared'] !== FALSE) {
    +        $this->services[$id] = $service;
    +      }
    

    Nice!

  6. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArrayContainer.php
    @@ -0,0 +1,261 @@
    +        call_user_func_array(array($service, $method), $arguments);
    

    would it be worth to do a switch for the amount of parameters to avoid cufa?

  7. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArrayContainer.php
    @@ -0,0 +1,261 @@
    +          throw new InvalidArgumentException(sprintf('Undefined type "%s" while resolving parameters and services.', $type));
    

    I think we now no longer use sprintf for exceptions, just use inline "'$type'", this improves readability a bit

  8. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -770,25 +802,42 @@ protected function initializeContainer() {
    +      } catch (\Exception $e) {
    +        // In the InstallerKernel the database is not available at this point.
    +        // Not being able to load from cache is not a fatal condition, hence
    +        // the error is ignored here.
    +        // If the database is really not available, then it will fatal during
    +        // service construction.
    +        $cache = FALSE;
    

    I'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

  9. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerTest.php
    @@ -7,35 +7,1183 @@
    +  public function test_get_classFromParameter() {
    

    Do you plan to adapt your test function name?

pfrenssen’s picture

StatusFileSize
new131.01 KB
new3.65 KB

This is a bit over my head but I could address the remarks from #171 :)

  1. Fixed.
  2. Fixed.
  3. Fixed.
  4. I don't know, would be surprised if this would be a measurable difference. Maybe worth to try?
  5. Nice indeed :)
  6. Fixed.
  7. Fixed.
  8. Leaving this for @FabianX.
  9. I left it as is because it is consistent with the other function names in this test.
fabianx’s picture

StatusFileSize
new4.3 KB
new130.62 KB

I 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.

dawehner’s picture

Issue tags: +4%

We seriously need profiling on this particular issue. My poor profiling made 4% less performance, which would be quite bad.

dawehner’s picture

Issue tags: -4% +needs profiling

.

fabianx’s picture

Issue summary: View changes
Issue tags: -needs profiling

Added profiling data to issue summary, can't reproduce 4% slower.

neclimdul’s picture

I 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).

fabianx’s picture

I 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.

fabianx’s picture

StatusFileSize
new38.91 KB
new132.49 KB

The missing files for #178 ...

dawehner’s picture

It could be indeed that the difference is the php version. I see kind of similar percentages with ab as well:

Head

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:    43   46  35.5     44    1168
Waiting:       37   40  27.9     39     921
Total:         43   46  35.5     44    1168 // these high numbers thought are interesting. File system access is potentially problematic, even the file should be in FS cache all the time.

Patch

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:    46   48   5.6     48     218
Waiting:       40   42   5.2     42     200
Total:         46   49   5.6     48     218
wim leers’s picture

Wow, that is a significant reduction in the standard deviation!

// these high numbers thought are interesting. File system access is potentially problematic, even the file should be in FS cache all the time.

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?

dawehner’s picture

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?

Mh, 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).

benjy’s picture

Read the code and tried to review, mainly just questions a few small doc things.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,600 @@
    +   * For testing purposes the container needs to be changed.
    ...
    +  public function __construct(array $container_definition = array(), $frozen = FALSE) {
    

    If $frozen is truly only for testing purposes, should we make the default TRUE?

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,600 @@
    +    if (!empty($container_definition) && (!isset($container_definition['machine_format']) || $container_definition['machine_format'] !== TRUE)) {
    

    Un-needed brackets around (!isset($container....

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,511 @@
    + * It is machine readable, for a human readable version based on this one see
    + * \Drupal\Core\DependencyInjection\Dumper\PhpArrayDumper.
    

    Maybe we should have an option in the example.local.settings.php file for switching the human readable version on?

  4. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,511 @@
    +   * Returns the aliases as a PHP array.
    ...
    +   * Returns parameters of the container as a PHP array.
    ...
    +   * Returns services of the container as a PHP array.
    

    Nitpick but I think we tend to use "Gets" by convention on getters.

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,511 @@
    +    if (!$definition->isPublic()) {
    +      $service['public'] = FALSE;
    +    }
    

    Could be just $service['public'] = $definition->isPublic();

  6. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,511 @@
    +   * @return string|\stdClass
    +   *   A suitable representation of the parameter reference.
    +   */
    +  protected function getParameterCall($name) {
    

    Can't return a string, just \stdClass

  7. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArrayContainer.php
    @@ -0,0 +1,274 @@
    +  protected function createService($definition, $id) {
    

    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?

dawehner’s picture

If $frozen is truly only for testing purposes, should we make the default TRUE?

I 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.

fabianx’s picture

#183:

1. The reason is to allow unit tests e.g. to use just:

$container = new Container();
$container->set('foo'. new \stdClass());
$container->setParameter('llama', 'foo');

which dawehner argued for should be valid usage, based on how one would use the plain Symfony container.

instead of having to do:

$container = new Container([], FALSE);
$container->set('foo'. new \stdClass());
$container->setParameter('llama', 'foo');

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!

chx’s picture

Regarding 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.

benjy’s picture

Okay, that I can fix. So s/Retrieves/Gets/g?

Yeah.

The rest sounds fine. Patch is looking good to me, great work!

fabianx’s picture

Issue tags: +Needs change record
StatusFileSize
new22.28 KB
new134.01 KB

#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.

fabianx’s picture

Issue tags: -Needs change record

Created:

- 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.

dawehner’s picture

Adding a related issue.

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 ...

I still don't get why we allow setParameter() during runtime, we should freeze it at some point during bootstrap.

fabianx’s picture

#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:

$container = new Container();
$container->setParameter('foo', 'bar');
// $container->isFrozen() == FALSE
// $container->getParameterBag() == new ParameterBag($container->parameters);
$container->compile();
// $container->isFrozen() == TRUE
// $container->getParameterBag() == new FrozenParameterBag($container->parameters);

But we could also discuss this as a follow-up.

geerlingguy’s picture

Note 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 :)

dawehner’s picture

But we could also discuss this as a follow-up.

Agree, but it is good to keep that in mind.

It would be great if @znerol could review this

wim leers’s picture

General 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.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    + * - The following functions, that are not part of the interface are explictly
    + *   not supported:
    + *     getParameterBag(), isFrozen(), compile(),
    + *     getAServiceWithAnIdByCamelCase().
    

    Nit: strange formatting.

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +  protected $parameters = array();
    ...
    +  protected $aliases = array();
    ...
    +  protected $serviceDefinitions = array();
    …
    

    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.

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +   * Can the container parameters still be changed.
    

    s/Can/Whether/
    s/still/can still/

  4. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +    // is used, the actual wanted behavior is to re-try getting the service at
    +    // a later point.
    

    Nit: 80 cols.

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +      // ReflectionClass is noticeable slow.
    

    Nit: s/noticeable/noticeably/

  6. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +   * @throws RuntimeException
    

    Needs FQCN.

  7. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +  protected function resolveServicesAndParameters($arguments) {
    +
    +    // Check if this collection needs to be resolved.
    

    Nit: extraneous blank line.

  8. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    + * The format of this dumper is very similar to the ContainerBuilder based
    + * format, but based on PHP arrays and \stdClass objects instead of rich value
    

    Nit: "ContainerBuilder based format" is not quite correct.

  9. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    + * objects for performance reasons.
    

    "rich value objects for perf reasons" -> that sounds like it could use some extra explanation.

  10. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    + * By removing the abstraction and optimizing some cases like deep collections,
    

    What are deep collections?

  11. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    + * less classes need to be loaded, less function calls need to be executed and
    + * less run time checks need to be made.
    

    Nit: s/less/fewer/

  12. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    + * Symfony service container builder and PHP dumper shared private services can
    

    s/dumper shared/dumper, shared/

  13. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    + * It is machine readable, for a human readable version based on this one see
    

    s/machine readable/machine-readable/
    s/human readable/human-readable/

  14. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    +   * unserialize the whole container on loading time, which improves early
    

    s/on loading time/at run time/ ?

  15. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    +   * @param \Symfony\Component\DependencyInjection\Reference $reference
    +   *   (optional) The reference object to process, needed to get the invalid
    +   *              behavior.
    

    "behavior." is indented incorrectly.

  16. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    +   * Gets a service reference for an id in a suitable PHP array format.
    

    Nit: s/id/ID/

  17. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,77 @@
    + * The format of this dumper is a human readable serialized
    + * PHP array, which is very similar to the YAML based format, but
    + * based on PHP arrays instead of YAML strings.
    

    80 cols.

  18. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,77 @@
    + * It is human readable, for a machine optimized version based on this one see
    

    s/human readable/human-readable/
    s/machine optimized/machine-optimized/

    Also: elsewhere, "machine-readable" was written, should that have been "machine-optimized"?

  19. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArrayContainer.php
    @@ -0,0 +1,274 @@
    +class PhpArrayContainer extends Container {
    

    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?

  20. +++ b/core/lib/Drupal/Core/DependencyInjection/PhpArrayContainer.php
    @@ -0,0 +1,274 @@
    +    // Do not call the parents constructor as it would bail on the machine
    

    s/parents/parent's/

  21. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -53,6 +51,55 @@
    +   * dump to the human writable PHP array format to debug the container
    

    s/human writable/human-readable/? :)

  22. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerTest.php
    @@ -7,35 +7,1211 @@
    +  }
    +
    +
    +  /**
    

    Nit: extraneous blank line.

fabianx’s picture

From 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.

pwolanin’s picture

If 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?

pwolanin’s picture

We should not be using crc32 for anything, so we should fix this code also. It's not clear why a hash is even needed?

-  protected function getClassName() {
+  protected function getContainerCacheKey() {
     $parts = array('service_container', $this->environment, hash('crc32b', \Drupal::VERSION . Settings::get('deployment_identifier')));
-    return implode('_', $parts);
-  }

catch’s picture

The hash is needed because Drupal::VERSION always has a . in it, and PHP function names cannot.

pwolanin’s picture

@catch - the method name says it's a cache key?

catch’s picture

Well that's a point, the method previously was for the classname. So yes likely don't need to hash any more.

fabianx’s picture

#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.

fabianx’s picture

Issue summary: View changes

#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.

catch’s picture

Didn'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.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +    if (!empty($container_definition) && (!isset($container_definition['machine_format']) || $container_definition['machine_format'] !== TRUE)) {
    +      throw new InvalidArgumentException('The non-optimized format is not supported by this class. Use an optimized machine-readable format instead, e.g. as produced by \Drupal\Core\DependencyInjection\Dumper\OptimizedPhpArrayDumper.');
    +    }
    

    Should this be an assert or can it really happen on runtime somehow?

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +    if (isset($this->aliases[$id])) {
    +      $id = $this->aliases[$id];
    +    }
    

    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.

  3. +++ 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.

  4. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +            // This can never be reached as getParameter() throws an Exception.
    

    I'm a bit lost by this comment, surely it doesn't throw an exception all the time?

  5. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,23 +7,617 @@
    +   * Provides alternatives for a given array and key.
    

    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.

  6. +++ b/core/lib/Drupal/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    +      $hash = sha1(serialize($service_definition));
    

    Any particular reason why sha1? And if so why not hash('sha1')?

  1. +++ b/core/modules/system/src/Tests/ServiceProvider/ServiceProviderWebTest.php
    @@ -0,0 +1,41 @@
    +    // The event subscriber method in the test class calls drupal_set_message
    

    Nit: missing parens.

  2. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
    @@ -0,0 +1,601 @@
    +     * Dummy class to ensure non-existant Symfony component can be tested.
    

    nit: existent.

znerol’s picture

Its 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\Core and Drupal\Component designated 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.

wim leers’s picture

I just wanted to say: thank you znerol, for such a wonderfully informative, balanced, constructively critical comment!

catch’s picture

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.

The 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.

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 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.

dawehner’s picture

Thank 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

fabianx’s picture

#204:

Thank you znerol for the great and insightful feedback.

I don't think it is crazy to replace the container, because:

  • * Compared to other critical subsystems like Renderer, Router, Config, Cache, Forms, TypedData, Entity, etc. the container is a very lightweight thing. Without the extensive test coverage it is just 40 kB, probably without the optimized version it would be just 30 kB or such.
  • * Especially PhpArrrayContainer should be extremely easy to understand.
  • * It gives us back control about a central part of the system, but I agree we need to ensure BC compatibility with upstream long-term.
  • * It allows us to change container parameters at run time, to e.g. remove the app.root needing to be a service, language manager to rebuild the container, etc. This is impossible with a compiled container.
  • * It potentially gives us other possibilities to change the container between loading from cache and running 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 think this cannot be compared to earlier forked code, because - for the examples I know:

  • - Drupal never updated third party components, but relied on stone-age-old jquery versions e.g.
  • - Simpletest was simply unmaintained upstream, so it was not worth to sync.
  • - There was no 'component dependency management' as we have now.

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.


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.

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?)


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).

It is a good idea to list what we loose in the IS, however:

  • - Read-only container we already lost as it is non-functional right now and has been since 2012.
  • - OpCache is just a small local cache. As the cache backend is pluggable if someone wanted they could load the container definition from:

* 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.

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\Core and Drupal\Component designated for stuff we forked and which needs special attention when updating vendor.

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

fabianx’s picture

StatusFileSize
new12.26 KB
new135.41 KB

Here 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)

fabianx’s picture

#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

Status: Needs review » Needs work

The last submitted patch, 209: rebuilding_service-2497243-209.patch, failed testing.

fabianx’s picture

StatusFileSize
new7.35 KB
new136.37 KB

Fixing 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.

fabianx’s picture

Status: Needs work » Needs review
klausi’s picture

Issue summary: View changes

Steps 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.

znerol’s picture

Re #206: Thanks, this very much contributes to a better understanding.

Re #208:

some infrastructure people I know were like: "Eh, compiled PHP code written at run-time in the file-system? What are you doing?

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.

fabianx’s picture

StatusFileSize
new230.22 KB
new27.19 KB
new135.36 KB

Moved to Component now:

  • interdiff.txt - the normal interdiff
  • interdiff-clean.txt - the readable interdiff (the meaningful changes)
  • and a new patch file.
wim leers’s picture

The IS lists these remaining tasks:

  1. #107 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
  2. #106
  3. #84 Remove support for expression (however, in that case the array container container should probably not be moved to a component)
  4. #69
  5. #54 move array container/dumper to a component
  6. #49 cache rebuild stampede protection

But at least #54 is now done, as of #216. So… which of these are actually still relevant?

fabianx’s picture

Issue summary: View changes

No remaining tasks left. This is ready.

fabianx’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Updated the IS some more.

dawehner’s picture

One 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.

chx’s picture

Title: Rebuilding service container results in endless stampede » Replace Symfony container with a Drupal one, stored in cache
chx’s picture

Issue summary: View changes
Crell’s picture

Can 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.

jibran’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update

IS needs update as per #223 and #220 so NW.

Status: Needs work » Needs review
martin107’s picture

fixed broken link in issue summary

[#2547827: PhpStorage] becomes #2547827: PhpStorage: past, present and future

martin107’s picture

Issue summary: View changes
fabianx’s picture

Issue summary: View changes
StatusFileSize
new771 bytes
new135.58 KB

As 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:

  • 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.
dawehner’s picture

I 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.

  1. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +class Container implements IntrospectableContainerInterface {
    

    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?

  2. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +   *
    +   * @var array
    

    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?

  3. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +    if (!empty($container_definition) && (!isset($container_definition['machine_format']) || $container_definition['machine_format'] !== TRUE)) {
    +      throw new InvalidArgumentException('The non-optimized format is not supported by this class. Use an optimized machine-readable format instead, e.g. as produced by \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper.');
    +    }
    

    Sounds like an assertion for me ...

  4. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +    $this->aliases = isset($container_definition['aliases']) ? $container_definition['aliases'] : array();
    +    $this->parameters = isset($container_definition['parameters']) ? $container_definition['parameters'] : array();
    +    $this->serviceDefinitions = isset($container_definition['services']) ? $container_definition['services'] : array();
    +    $this->frozen = isset($container_definition['frozen']) ? $container_definition['frozen'] : FALSE;
    +
    

    I'm curious, would it ever make sense to not have all those keys defined? We could also do a $container_definition += ['aliases' => [], ...] instead

  5. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +    $definition = isset($this->serviceDefinitions[$id]) ? $this->serviceDefinitions[$id] : NULL;
    

    Given 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?

  6. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +   * @throws Symfony\Component\DependencyInjection\Exception\RuntimeException\RuntimeException
    ...
    +   * @throws Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
    

    Nitpick: Let's start with \

  7. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +    if (isset($definition['synthetic']) && $definition['synthetic'] === TRUE) {
    +      throw new RuntimeException(sprintf('You have requested a synthetic service ("%s"). The service container does not know how to construct this service. The service will need to be set before it is first used.', $id));
    +    }
    

    This is IMHO an assertion as well

  8. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +      if ($arguments->type !== 'collection') {
    +        throw new InvalidArgumentException(sprintf('Undefined type "%s" while resolving parameters and services.', $arguments->type));
    +      }
    

    assertion?

  9. +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    +use Symfony\Component\DependencyInjection\Alias;
    

    This usestatement is unused

  10. +++ b/core/lib/Drupal/Core/DependencyInjection/Container.php
    @@ -7,17 +7,17 @@
    -  public function set($id, $service, $scope = SymfonyContainer::SCOPE_CONTAINER) {
    +  public function set($id, $service, $scope = DrupalContainer::SCOPE_CONTAINER) {
    

    IMHO we should change it and use the constant on the interface instead

  11. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -1196,35 +1245,25 @@ protected function getContainerBuilder() {
    +      // There is no way to get from the Cache API if the cache set was
    +      // successful or not, hence an Exception is caught and the caller
    +      // informed about the error condition.
    +      $saved = FALSE;
    

    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

  12. +++ b/core/modules/system/src/Tests/System/UncaughtExceptionTest.php
    @@ -196,14 +189,7 @@ public function testExceptionContainer() {
    diff --git a/core/modules/update/src/Tests/UpdateTestBase.php b/core/modules/update/src/Tests/UpdateTestBase.php
    
    diff --git a/core/modules/update/src/Tests/UpdateTestBase.php b/core/modules/update/src/Tests/UpdateTestBase.php
    index 33ee105..faec496 100644
    
    index 33ee105..faec496 100644
    --- a/core/modules/update/src/Tests/UpdateTestBase.php
    
    --- a/core/modules/update/src/Tests/UpdateTestBase.php
    +++ b/core/modules/update/src/Tests/UpdateTestBase.php
    
    +++ b/core/modules/update/src/Tests/UpdateTestBase.php
    +++ b/core/modules/update/src/Tests/UpdateTestBase.php
    @@ -35,7 +35,7 @@ protected function setUp() {
    
    @@ -35,7 +35,7 @@ protected function setUp() {
     
         // Change the root path which Update Manager uses to install and update
         // projects to be inside the testing site directory. See
    -    // \Drupal\updateUpdateRootFactory::get() for equivalent changes to the
    +    // \Drupal\update\UpdateRootFactory::get() for equivalent changes to the
         // test child site.
         $request = \Drupal::request();
         $update_root = $this->container->get('update.root') . '/' . DrupalKernel::findSitePath($request);
    

    +1 for this change but feels out of change for this particular issue

  13. +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
    @@ -0,0 +1,1172 @@
    +      'file' => __DIR__ . '/Fixture/container_test_file_service_test_service_function.data',
    

    Should we use a .php file instead? I don't see a reason to "lie" here :)

twistor’s picture

Regarding #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.

catch’s picture

I also don't know where the issue is, but remember the same thing with if/else no longer being faster than ternary.

dawehner’s picture

Ah perfect

wim leers’s picture

StatusFileSize
new135.82 KB
new19.56 KB

Fixed 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.

  1. +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/PhpArrayDumper.php
    @@ -0,0 +1,77 @@
    +class PhpArrayDumper extends OptimizedPhpArrayDumper {
    

    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?

  2. +++ b/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php
    @@ -0,0 +1,277 @@
    +    // - Arguments are traversed recursively, there is no information how deep
    +    //   to traverse.
    

    "there is no information how deep to traverse" -> that's a broken sentence, let's fix that :)

  3. +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,523 @@
    +  /**
    +   * Whether this supports the machine format or not.
    +   *
    +   * @return bool
    +   *   TRUE if this supports machine format, FALSE otherwise.
    +   */
    +  protected function getMachineFormat() {
    +    return TRUE;
    +  }
    

    The documentation and function body suggests this method should be renamed to supportsMachineFormat().

Aki Tendo’s picture

Status: Needs review » Needs work
line 120:
+   *   An array containing the following keys:
+   *   - aliases: The aliases of the container.
+   *   - parameters: The parameters of the container.
+   *   - services: The service definitions of the container.
+   *   - frozen: Whether the container definition came from a frozen
+   *     container builder or not.
+   *   - machine_format: Whether this container definition uses the optimized
+   *     machine-readable container format.
+   */
+  public function __construct(array $container_definition = array()) {
+    if (!empty($container_definition) && (!isset($container_definition['machine_format']) || $container_definition['machine_format'] !== TRUE)) {
+      throw new InvalidArgumentException('The non-optimized format is not supported by this class. Use an optimized machine-readable format instead, e.g. as produced by \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper.');
+    }

1. 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:

public function __construct(array $container_definition = array()) {
  assert('static::assertValidConstructParameters($container_definition)');
  ...
  }

  public static function assertValidConstructParameters($container_definition) {
  // exhaustive test here.
  }

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.

line 143
+  /**
+   * {@inheritdoc}
+   */
+  public function get($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) {

4. Should $id 's data type be asserted? It's format? Should assert(is_bool) be checked on ContainerInterface?

line 360
+  /**
+   * {@inheritdoc}
+   */
+  public function set($id, $service, $scope = self::SCOPE_CONTAINER) {

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.

+  /**
+   * Resolves arguments from \stdClass objects to the real values.
+   *
+   * @param array|\stdClass $arguments
+   *   The arguments to resolve.
+   *
+   * @return array
+   *   The resolved arguments.
+   *
+   * @throws \Symfony\Component\DependencyInjection\Exception\RuntimeException
+   *   If a parameter/service could not be resolved.
+   */
+  protected function resolveServicesAndParameters($arguments) {
+    // Check if this collection needs to be resolved.
+    if ($arguments instanceof \stdClass) {

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.

line 702
+  public function getArray() {
+    $definition = array();
+    $definition['aliases'] = $this->getAliases();
+    $definition['parameters'] = $this->getParameters();
+    $definition['services'] = $this->getServiceDefinitions();
+    $definition['frozen'] = $this->container->isFrozen();
+    $definition['machine_format'] = $this->getMachineFormat();
+    return $definition;
+  }

9. Doesn't it run slightly faster to just return the array?

return [
  'aliases' => $this->getAliases(),
  'parameters' => $this->getParameters(),
  ...
];

10. Line 1281 -- Everything said on comment #1 applies here. Since this is a child method it could call the same assertion.

fabianx’s picture

#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.

fabianx’s picture

Status: Needs work » Needs review

#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

dawehner’s picture

Do you mind opening up all those follow ups? We should not forget about it.

13. Simpletest tries to load that file, it does not like such Fixtures, so using .data is the way to avoid a fatal.

Do you mind documenting it?

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.

Should we at least try to call watchdog_exception() ?

3. As described already, this is a fatal condition that cannot be recovered from. That is like asking PHP to run a Python script.

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.

hussainweb’s picture

StatusFileSize
new6.01 KB
new136.15 KB

I 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-injection instead of drupal/core-dependency-injection.

bojanz’s picture

-    // \Drupal\update\UpdateRootFactory::get() for equivalent changes to the
+    // \Drupal\updateUpdateRootFactory::get() for equivalent changes to the

Accidental revert in #238?

fabianx’s picture

StatusFileSize
new3.85 KB
new135.14 KB

X-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.

hussainweb’s picture

@bojanz: Actually, no. I did that based on @Fabianx's response in #235.

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.

EDIT: Cross-post again on #240. :)

fabianx’s picture

Thanks, #238. I actually forgot the supportsMachineFormat() change. :)

I will merge that interdiff now into mine.

hussainweb’s picture

+++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
@@ -410,7 +410,7 @@ public function initialized($id) {
+   * Resolves arguments that represent services or variables to the real values.

+++ b/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php
@@ -191,8 +191,8 @@ protected function resolveServicesAndParameters($arguments) {
-    // - Arguments are traversed recursively, there is no information how deep
-    //   to traverse.
+    // - The depth of the tree representing the arguments is not known in
+    //   advance, so it needs to be fully traversed recursively.

+1 on these. These are better than my changes in #238.

fabianx’s picture

StatusFileSize
new635 bytes
new4.94 KB
new136.26 KB

interdiff-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.

fabianx’s picture

More feedback:

#237:

1. Fixed by Hussainweb already (thanks so much!)

2. The caller in question does:

    // If needs dumping flag was set, dump the container.
    if ($this->containerNeedsDumping && !$this->cacheDrupalContainer($container_definition)) {
      $this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be saved to cache.');
    }

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.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Alright, thank you for answering the questions!

Everything can be follow ups, also the composer.json file.

fgm’s picture

Status: Reviewed & tested by the community » Needs work

A few PHPdoc changes are needed, which does not impact the actual RTBC status once addressed, since these are just doc comments.

  1. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    + * \Drupal\Compontent\DependencyInjection\Dumper\OptimizedPhpArrayDumper::getArray().
    

    typo: Component, not Compontent.

  2. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    + * This container is different in behavior to the default Symfony container in
    

    different from, not different to

  3. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +   *   Thrown when the configurator callable is not callable.
    

    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.

  4. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +   */
    

    It may also throw a \ReflectionException if for some reason the class cannot be instantiated in the 'default' case.

  5. +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
    @@ -0,0 +1,623 @@
    +   */
    

    Can also throw InvalidArgumentException (lines 429 and 520)

  6. +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,522 @@
    +   */
    

    Can throw InvalidArgumentException (scope != SCOPE_PROTOTYPE, or decorated definition.

  7. +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,522 @@
    +   *   When trying to dump an expression.
    

    Since it /always/ happens, maybe modify the exception comment, like "Always thrown, because this container cannot use expressions."

fgm’s picture

Status: Needs work » Needs review
StatusFileSize
new136.88 KB
new3.66 KB

Rerolled to account for these remarks.

alexpott’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
    @@ -0,0 +1,526 @@
    +      return $this->getExpressionCall((string) $value);
    

    Why not just throw an exception here rather than in getExpressionCall()?

  2. +++ b/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php
    @@ -0,0 +1,276 @@
    +        // @see \Drupal\Component\DependecyInjection\Dumper\OptimizedPhpArrayDumper::getPrivateServiceCall
    

    Dependency is not spelt right

  3. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -816,9 +867,8 @@ protected function initializeContainer() {
    +    if ($this->containerNeedsDumping && !$this->cacheDrupalContainer($container_definition)) {
    

    I think it might be possible that $container_definition is undefined.

  4. +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
    @@ -0,0 +1,1187 @@
    +use Symfony\Component\DependencyInjection\Exception\LogicException;
    

    Not used

  5. +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
    @@ -0,0 +1,1187 @@
    +    $this->assertNull($this->container->get('service_not_exists', ContainerInterface::NULL_ON_INVALID_REFERENCE, 'Not found service does nto throw exception.'));
    

    I guess 'Not found...' is meant to be the assertion message.

  6. +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php
    @@ -0,0 +1,1187 @@
    +      throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Simulating the test failure.');
    

    Doesn't need to be a FQDN

  7. +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
    @@ -0,0 +1,601 @@
    +      $this->containerBuilder = $this->prophesize('\Symfony\Component\DependencyInjection\ContainerBuilder');
    ...
    +      $this->containerDefinition = $definition;
    ...
    +      $this->dumper = new $this->dumperClass($this->containerBuilder->reveal());
    

    Let's add a protected property for these.

  8. +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
    @@ -0,0 +1,601 @@
    +      // Test a private shared servied, denoted by having a Reference.
    

    service

fgm’s picture

Status: Needs work » Needs review
StatusFileSize
new137.69 KB
new6.64 KB

1) 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

Status: Needs review » Needs work

The last submitted patch, 250: 2497243-fast_container-250.patch, failed testing.

fgm’s picture

Status: Needs work » Needs review
StatusFileSize
new137.68 KB
new6.64 KB

That was 5), fixed incorrectly all 3 times.

Status: Needs review » Needs work

The last submitted patch, 252: 2497243-fast_container-252.patch, failed testing.

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new137.69 KB
new753 bytes

Just a super silly super tiny docs fix.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -867,7 +867,7 @@ protected function initializeContainer() {
+    if ($this->containerNeedsDumping && isset($container) && !$this->cacheDrupalContainer($container_definition)) {

The point was $container_definition might not be set. Not sure what is actually the correct thing to do here.

alexpott’s picture

Also we should remove the getExpressionCall() method

wim leers’s picture

Status: Needs work » Reviewed & tested by the community

The point was $container_definition might not be set. Not sure what is actually the correct thing to do here.

That's indeed what PHPStorm indicates.

But:

  protected function initializeContainer() {
    $this->containerNeedsDumping = FALSE;

    …

    // If there is no container and no cached container definition, build a new
    // one from scratch.
    if (!isset($container) && !isset($container_definition)) {
      $container = $this->compileContainer();

      // Only dump the container if dumping is allowed. This is useful for
      // KernelTestBase, which never wants to use the real container, but always
      // the container builder.
      if ($this->allowDumping) {
        $dumper = new $this->phpArrayDumperClass($container);
        $container_definition = $dumper->getArray();
      }
    }

    …

    // 
    // THIS IS THE PART THAT ALEX QUOTED
    //
    if ($this->containerNeedsDumping && isset($container) && !$this->cacheDrupalContainer($container_definition)) {
      $this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be saved to cache.');
    }
  }

  protected function compileContainer() {
    // We are forcing a container build so it is reasonable to assume that the
    // calling method knows something about the system has changed requiring the
    // container to be dumped to the filesystem.
    if ($this->allowDumping) {
      $this->containerNeedsDumping = TRUE;
    }

    …
  }

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_definition if $this->allowDumping === TRUE.

Therefore $container_definition is 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.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

I 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!

diff --git a/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
index 23e8df6..23290b6 100644
--- a/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
+++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
@@ -501,19 +501,6 @@ protected function getParameterCall($name) {
   }
 
   /**
-   * Gets an expression reference in a suitable PHP array format.
-   *
-   * @param string $expression
-   *   The expression to get a reference for.
-   *
-   * @throws RuntimeException
-   *   Always thrown, since this container cannot use expressions.
-   */
-  protected function getExpressionCall($expression) {
-    throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
-  }
-
-  /**
    * Whether this supports the machine-optimized format or not.
    *
    * @return bool
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 2ee663a..8ae5924 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -867,7 +867,7 @@ protected function initializeContainer() {
     \Drupal::setContainer($this->container);
 
     // If needs dumping flag was set, dump the container.
-    if ($this->containerNeedsDumping && isset($container) && !$this->cacheDrupalContainer($container_definition)) {
+    if ($this->containerNeedsDumping && !$this->cacheDrupalContainer($container_definition)) {
       $this->container->get('logger.factory')->get('DrupalKernel')->notice('Container cannot be saved to cache.');
     }
 

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.

  • alexpott committed 22fbcd4 on 8.0.x
    Issue #2497243 by Fabianx, znerol, fgm, Wim Leers, darol100, jhedstrom,...
fabianx’s picture

Thanks so much @all for finding those remaining issues and fixing those directly.

Also: YES!!!!!!!


Super simple nit in docs introduced in one interdiff:

+++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php
@@ -35,6 +49,13 @@ class OptimizedPhpArrayDumperTest extends \PHPUnit_Framework_TestCase {
+     * @var \Symfony\Component\DependencyInjection\DumperDumperInterface
+     */
+    protected $dumper;

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!

chx’s picture

Someone 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?

dawehner’s picture

Someone 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?

chx, 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 ...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

The last submitted patch, 172: 2497243-172.patch, failed testing.