Proposed commit message (D7)

Issue #1081266 by stefan.r, jeroen.b, mikeytown2, tsphethean, mfb, joseph.olstad, marcelovani, Fabianx: Avoid re-scanning module directory when a filename or a module is missing

Problem/Motivation

Drupal occurs a performance deficit when multiple filenames (ie. modules, themes) are missing or moved. The deficit occurs because we do a file scan perpetually to look for missing modules on certain pages, such as admin/config.

Proposed resolution

For D8: We remove the "missing file scan" that happens in drupal_get_filename() altogether and do various workarounds to deal with this scan in places where we haven't cached file locations yet, such as in the installer and in some tests (see #351). Instead of a file scan, we now do a trigger_error(), informing the developer/administrator about the missing file.

For D7: We cache any missing files in a static variable as well as in cache_bootstrap (for 24 hours, or until we visit the themes/modules overview page). We also do a trigger_error() as soon as we find a missing file in the drupal_get_filename() call.

In a followup issue we can check for this in hook_requirements() as well.

Remaining tasks

None.

User interface changes

None.

API changes

In D8, we remove the file scan from drupal_get_filename(), which also affects drupal_get_path(). The lack of this is only a "problem" in the installer, in tests and during module rebuild, which are not part of the API.

Original report by mfb

This patch provides a performance boost for sites that are missing modules (common on old sites which have been maintained for many years). In this case, drupal_system_listing() and file_scan_directory() will be called over and over looking for missing modules on certain pages, such as admin/config.

This patch adds an additional static variable to drupal_get_filename() to store an array of mask-directory combinations that have been scanned. Once one scan has been performed for a particular extension in a particular directory, there is no need to run the same scan later in the same request.

This information will be cached in 'drupal_get_filename:missing' in cache_bootstrap as well. Invalidation of this cache happens upon running cron, and upon visiting the themes/modules overview pages.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because significant performance slowdown
Issue priority Major because significant performance slowdown.
Prioritized changes The main goal of this issue is performance
Disruption N/A
CommentFileSizeAuthor
#611 interdiff.txt12.78 KBDavid_Rothstein
#611 1081266-611.patch39.79 KBDavid_Rothstein
#611 1081266-546-plus-new-tests-should-fail.patch36.87 KBDavid_Rothstein
#606 interdiff-546-606.txt3.24 KBDavid_Rothstein
#606 1081266-606.patch29.72 KBDavid_Rothstein
#603 interdiff-559-600.txt4.41 KBjeroen.b
#603 1081266-600.patch31.14 KBjeroen.b
#587 interdiff-582-587.txt4.45 KBjeroen.b
#587 interdiff-559-587.txt5.33 KBjeroen.b
#587 1081266-587.patch31.01 KBjeroen.b
#584 interdiff-559-582.txt3.95 KBjeroen.b
#583 1081266-582.patch30 KBjeroen.b
#559 interdiff-1081266-546-559.txt506 bytesjeroen.b
#559 1081266-559.patch29.12 KBjeroen.b
#546 interdiff.txt1013 bytesDavid_Rothstein
#546 1081266-546.patch28.57 KBDavid_Rothstein
#532 interdiff.txt473 bytesDavid_Rothstein
#532 1081266-532.patch28.49 KBDavid_Rothstein
#531 interdiff.txt34.18 KBDavid_Rothstein
#531 1081266-531.patch28.54 KBDavid_Rothstein
#527 interdiff-524-527.txt0 bytesstefan.r
#527 1081266-527.patch23.61 KBstefan.r
#524 1081266-524.patch23.3 KBstefan.r
#524 interdiff-518-524.txt11.85 KBstefan.r
#518 1081266-518.patch22.91 KBstefan.r
#518 interdiff-511-518.txt5.92 KBstefan.r
#512 interdiff-499-511.txt762 bytesstefan.r
#512 1081266-511.patch22.8 KBstefan.r
#499 interdiff-498-499.txt1.43 KBstefan.r
#499 1081266-499.patch22.64 KBstefan.r
#498 1081266-498.patch22.65 KBstefan.r
#498 interdiff-496-498.txt5.9 KBstefan.r
#496 1081266-496.patch20.94 KBstefan.r
#496 interdiff-492-496.txt887 bytesstefan.r
#492 1081266-492.patch20.94 KBstefan.r
#492 interdiff-491-492.txt1.03 KBstefan.r
#491 1081266-491.patch20.94 KBstefan.r
#491 interdiff-483-491.txt3.08 KBstefan.r
#483 interdiff-481-483.txt3.32 KBstefan.r
#483 1081266-483.patch19.92 KBstefan.r
#480 interdiff-480-481.txt2.03 KBstefan.r
#480 1081266-481.patch19.73 KBstefan.r
#479 1081266-480.patch19.78 KBstefan.r
#479 interdiff-479-480.txt9.83 KBstefan.r
#478 interdiff-478-479.txt4.38 KBstefan.r
#478 1081266-479.patch19.09 KBstefan.r
#475 1081266-478.patch19.9 KBstefan.r
#475 interdiff-456-478.txt21.68 KBstefan.r
#456 1081266-456.patch15.89 KBstefan.r
#456 interdiff-453-456.txt786 bytesstefan.r
#453 interdiff-452-453.txt2.23 KBstefan.r
#453 1081266-453.patch15.79 KBstefan.r
#452 interdiff-449-452.txt9.34 KBstefan.r
#452 1081266-452.patch15.44 KBstefan.r
#449 interdiff-437-449.txt5.7 KBstefan.r
#449 interdiff-445-449.txt3.06 KBstefan.r
#449 1081266-449.patch12.27 KBstefan.r
#445 1081266-445.patch12.09 KBstefan.r
#445 interdiff-444-445.txt2 KBstefan.r
#444 interdiff-437-444.txt4.04 KBstefan.r
#444 1081266-444.patch11.59 KBstefan.r
#437 interdiff-425-437.txt9.13 KBstefan.r
#437 1081266-437.patch8.24 KBstefan.r
#436 interdiff-425-436.txt7.34 KBstefan.r
#436 1081266-436.patch8.12 KBstefan.r
#425 1081266-425.patch7.52 KBjeroen.b
#425 interdiff-1081266-419-425.txt2.15 KBjeroen.b
#419 1081266-419.patch7.5 KBjeroen.b
#419 interdiff-1081266-415-419.txt716 bytesjeroen.b
#415 1081266-415.patch7.36 KBjeroen.b
#415 interdiff-1081266-407-415.txt2.56 KBjeroen.b
#408 interdiff-1081266-373-407.txt8.48 KBjeroen.b
#407 1081266-407.patch7.27 KBjeroen.b
#407 interdiff-1081266-402-407.txt1.83 KBjeroen.b
#405 interdiff-1081266-373-403.txt8.36 KBjeroen.b
#403 1081266-402.patch7.16 KBjeroen.b
#403 interdiff-1081266-399-402.txt1.16 KBjeroen.b
#399 1081266-399.patch5.91 KBjeroen.b
#399 interdiff-1081266-397-399.txt865 bytesjeroen.b
#397 interdiff-1081266-391-397.txt473 bytesjeroen.b
#397 1081266-397.patch5.86 KBjeroen.b
#395 1081266-395-do-not-test.patch5.42 KBjeroen.b
#391 1081266-391.patch5.35 KBjeroen.b
#389 interdiff-1081266-379-389.txt3.14 KBjeroen.b
#389 1081266-389.patch5.44 KBjeroen.b
#387 1081266-387.patch5.52 KBjeroen.b
#385 1081266-385.patch5.52 KBjeroen.b
#385 interdiff-1081266-381-385.txt2.24 KBjeroen.b
#383 interdiff-1081266-379-381.txt940 bytesjeroen.b
#381 interdiff-1081266-379-381.patch940 bytesjeroen.b
#381 1081266-381.patch5.24 KBjeroen.b
#379 interdiff-375-379.txt2.71 KBstefan.r
#379 1081266-379.patch5.24 KBstefan.r
#376 interdiff-374-375.txt6.57 KBstefan.r
#375 interdiff-374-375.txt0 bytesstefan.r
#375 drupal-1081266-375-avoid-rescanning-missing-module-D7.patch5.22 KBstefan.r
#374 drupal-1081266-373-avoid-rescanning-missing-module-D7.patch3.31 KBjeroen.b
#367 interdiff-360-367.txt3.13 KBstefan.r
#367 1081266-367.patch36.97 KBstefan.r
#362 interdiff-289-360.txt34.24 KBstefan.r
#361 interdiff-358-360.txt2.16 KBstefan.r
#360 1081266-360.patch36.97 KBstefan.r
#360 1081266-360.patch36.97 KBstefan.r
#358 interdiff-356-358.txt806 bytesstefan.r
#358 1081266-358.patch36.43 KBstefan.r
#357 1081266-356.patch36.41 KBstefan.r
#357 interdiff-354-356.txt652 bytesstefan.r
#355 interdiff-289-354.txt33.54 KBstefan.r
#355 interdiff-352-354.txt10.07 KBstefan.r
#355 1081266-354.patch36.45 KBstefan.r
#353 interdiff-350-352.txt8.08 KBstefan.r
#352 1081266-352.patch35.14 KBstefan.r
#352 1081266-352.patch35.14 KBstefan.r
#350 1081266-350.patch33.3 KBstefan.r
#350 interdiff-347-350.txt3.98 KBstefan.r
#347 1081266-347.patch32.53 KBstefan.r
#347 interdiff-342-347.txt3.76 KBstefan.r
#344 interdiff-335-342.txt4.29 KBstefan.r
#344 1081266-342.patch32.67 KBstefan.r
#335 interdiff-331-335.txt6.09 KBstefan.r
#335 1081266-335.patch32.27 KBstefan.r
#331 1081266-331.patch27.79 KBstefan.r
#331 interdiff-312-331.txt22.99 KBstefan.r
#329 interdiff-325-329.txt6.04 KBstefan.r
#329 1081266-329.patch26.92 KBstefan.r
#325 1081266-325.patch20.81 KBstefan.r
#323 interdiff-314-323.txt10.04 KBstefan.r
#323 1081266-323.patch19.25 KBstefan.r
#321 interdiff-314-321.txt9.75 KBstefan.r
#321 1081266-321.patch18.96 KBstefan.r
#315 interdiff-312-314.txt3.79 KBstefan.r
#315 1081266-314.patch9.34 KBstefan.r
#312 interdiff-289-312.txt2.7 KBstefan.r
#312 1081266-312.patch13.3 KBstefan.r
#289 1081266-289.patch15.11 KBstefan.r
#289 interdiff-266-289.txt4.06 KBstefan.r
#287 1081266-287.patch15.23 KBstefan.r
#287 interdiff-283-287.txt1.24 KBstefan.r
#284 1081266-283.patch13.77 KBstefan.r
#284 interdiff-266-283.txt2.91 KBstefan.r
#282 1081266-282.patch13.77 KBstefan.r
#282 interdiff-281-282.txt1.48 KBstefan.r
#281 1081266-281.patch13.47 KBstefan.r
#281 interdiff-278-281.txt1.23 KBstefan.r
#279 1081266-278.patch12.13 KBstefan.r
#279 interdiff-277-278.txt363 bytesstefan.r
#277 interdiff-274-277.txt907 bytesstefan.r
#277 1081266-277.patch12.17 KBstefan.r
#274 1081266-274.patch11.16 KBstefan.r
#274 interdiff-266-274.txt551 bytesstefan.r
#266 1081266-266.patch11.5 KBstefan.r
#266 interdiff-260-266.txt1.16 KBstefan.r
#261 1081266-260.patch11.95 KBstefan.r
#261 interdiff-254-260.txt1.51 KBstefan.r
#258 interdiff-223-254.txt9.3 KBstefan.r
#254 1081266-254.patch11.94 KBstefan.r
#252 interdiff-233-248.txt9.05 KBstefan.r
#248 interdiff.txt3.56 KBstefan.r
#248 1081266-248.patch11.91 KBstefan.r
#245 1081266-244.patch11.88 KBstefan.r
#243 interdiff.txt3.56 KBstefan.r
#243 1081266-243.patch11.8 KBstefan.r
#241 interdiff.txt7.14 KBstefan.r
#241 1081266-241.patch9.84 KBstefan.r
#233 drupal-1081266-233-avoid-rescanning-missing-module-D8.patch6.96 KBstefan.r
#233 interdiff.txt684 bytesstefan.r
#223 interdiff.txt1.58 KBstefan.r
#223 drupal-1081266-223--avoid-rescanning-missing-module-D8.patch7.01 KBstefan.r
#221 drupal-1081266-219-avoid-rescanning-missing-module-D8.patch6.83 KBstefan.r
#221 interdiff.txt5.64 KBstefan.r
#202 drupal-1081266-202-avoid-rescanning-missing-module-D8.patch4.42 KBjoseph.olstad
#189 drupal-1081266-189-avoid-rescanning-missing-module-D7.patch3.31 KBjeroen.b
#186 drupal-1081266-186-avoid-rescanning-missing-module-D8.patch5.44 KBjeroen.b
#181 includes_menu_inc.png225.58 KBjoelpittet
#173 bootstrap_inc_-.png198.21 KBjoelpittet
#171 drupal-1081266-171-drupal_get_filename-extra-debug-do-not-test-D7.patch1.87 KBjeroen.b
#162 drupal-1081266-162-avoid-rescanning-missing-module-D8.patch3.78 KBjeroen.b
#159 drupal-1081266-159-avoid-rescanning-missing-module-D8.patch3.76 KBmikeytown2
#157 drupal-1081266-157-avoid-rescanning-missing-module-D8.patch3.76 KBmikeytown2
#155 drupal-1081266-155-avoid-rescanning-missing-module-D8.patch3.75 KBmikeytown2
#150 drupal-1081266-137-avoid-rescanning-missing-module-D8.patch5.39 KBmarcelovani
#138 drupal-1081266-138-drupal_get_filename-D7.patch1.8 KBjeroen.b
#137 drupal-1081266-137-avoid-rescanning-missing-module-D8.patch5.39 KBjeroen.b
#132 drupal-1081266-132-avoid-rescanning-missing-module-D8.patch5.54 KBmikeytown2
#127 drupal-1081266-127-avoid-rescanning-missing-module-D8.patch5.32 KBmikeytown2
#124 drupal-1081266-124-avoid-rescanning-missing-module-D8.patch4.12 KBmikeytown2
#120 avoid_rescanning_drupal_get_filename-1081266-120.patch2.89 KBjeroen.b
#102 drupal-1081266-102-drupal_get_filename-D7.patch1.75 KBmikeytown2
#95 drupal_get_filename_cache-1081266-95.patch7.09 KBtsphethean
#93 drupal_get_filename_cache-1081266-93.patch7.09 KBtsphethean
#80 drupal_get_filename_cache-1081266-80.patch7.24 KBtsphethean
#74 drupal_get_filename_cache-1081266-74.patch7.09 KBtsphethean
#69 drupal_get_filename_cache-1081266-69.patch7.1 KBtsphethean
#64 drupal-1081266-64-drupal_get_filename-D8.patch1.78 KBmikeytown2
#62 drupal-1081266-62-drupal_get_filename-D8.patch1.79 KBmikeytown2
#60 drupal-1081266-60-drupal_get_filename-D8.patch1.77 KBmikeytown2
#58 drupal-1081266-58-drupal_get_filename-D7.patch1.75 KBmikeytown2
#30 image1-7.8-default.png54.12 KBKars-T
#30 Image2-7.8-patch_from_1081266.png43.2 KBKars-T
#30 Image3-7.8_patch_from_1081266_and_733308.png35.19 KBKars-T
#30 Image4-7.8_patch_from_733308.png40.7 KBKars-T
2011-10-01_025602.png7.66 KBxandeadx
#9 1081266-drupal_get_filename.patch1.99 KBmfb
#4 1081266-drupal_get_filename-D7.patch2.37 KBmfb
drupal_get_filename.patch1.99 KBmfb
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

catch’s picture

I can't remember which issue this came up in, (edit: now I can, it was #752730: Remove file_exists() during bootstrap) but someone moshe mentioned we should probably disable modules that are missing if we find they don't exist. Old core modules like ping that were removed, contrib modules that are deleted rather than uninstalled, there are lots of ways this happens.

If we did that, are there still circumstances this could run more than once? Also even if we do go for disabling those, I'm split on whether it should be a one-off or built in somewhere.

mfb’s picture

Disabling modules would not address this issue. system_status() calls drupal_load_updates() which calls drupal_get_installed_schema_version() which loads everything of type module from the system table, regardless of status. module_load_install() is called as long as the schema version is 0 or higher, even if the module is disabled.

sun’s picture

Status: Needs review » Needs work
+++ includes/bootstrap.inc
@@ -702,16 +702,19 @@ function drupal_get_filename($type, $name, $filename = NULL) {
+      if (!isset($scanned[$extension][$dir])) {
...
+        $scanned[$extension][$dir] = TRUE;

1) Let's move the variable assignment right below the conditional check, so the purpose of the conditional as well as the assignment is clear.

2) Wouldn't it make more sense to flip $dir and $extension in the array? And when considering that, wouldn't $dirs be a better name than $scanned? Perhaps not, just an idea.

Powered by Dreditor.

mfb’s picture

Status: Needs work » Needs review
FileSize
2.37 KB

OK changed to $dirs and moved that line (hope $dirs variable is ok coding standards wise).

sun’s picture

My brain is filled up with other stuff right now, so just throwing the idea out there: Any remotely potential issues if $dir == $extension (say, 'inc') ?

Also, we should open a separate issue to fix the root cause; namely, fixing {system}.status columns on a certain page/callback.

sun’s picture

mfb’s picture

$dir should be one of themes, themes/engines, modules, profiles and $extension should be one of info, engine, module, profile. But I don't think there'd be any problem if $dir == $extension.

As mentioned in #2, this code is running for all modules, disabled and enabled, so disabling missing modules wouldn't help here.

Damien Tournoud’s picture

Version: 7.x-dev » 8.x-dev
Issue tags: +Needs backport to D7
mfb’s picture

Here's the D8 version.

Peter Bex’s picture

+1 on this patch, this can really slow down some sites!

mfb’s picture

hopefully "+1" means you reviewed the patch? it's not totally clear :)

Peter Bex’s picture

No, I just wanted this bug to get fixed. Didn't get around to testing it. Sorry :)

das-peter’s picture

Could this issue be related to this one #733308: Add static caching for drupal_system_listing()?

mfb’s picture

Related, but this is a higher-level fix to add (more) static caching to drupal_get_filename() so as to avoid unnecessary calls to drupal_system_listing() altogether.

Mark Theunissen’s picture

I've tested the patch and it works - the speed increase is major for my upgraded site that has 17 orphaned module entries in system table (I will be removing those).

The patch provides a working solution, but I wonder if it would be better to implement #733308: Add static caching for drupal_system_listing() as that adds a static cache to drupal_system_listing(), which will catch other places this may crop up. Once that is in place, I think the performance increase by doing this high level fix will be quite small.

mfb’s picture

Hmm #733308: Add static caching for drupal_system_listing() seems like redundant static caching of a large array, when drupal_get_filename() already has a static cache of $files found via drupal_system_listing()?

corbacho’s picture

I think this bug is more important than it seems.

I had only 1 orphaned module in a brand new Drupal 7 site we are building. (It was a feature-module that was removed from our git repository without proper uninstall)
I discover this bug only because I was profiling it, so I can give you some real data:

BEFORE:
The frontpage load taking 20 secs.
"file_scan_directory" was executed 1162 times. Taking 9 seconds in total
*Every single* page load.

AFTER:
Frontpage load taking 7 secs.
'file_scan_directory' wasn't executed at all.

(The load times are quite increased because of the profiler tool. Without profiling, it takes about half of the time in both cases)

So, especially if you have a lot of folders it's really expensive. In my case was even worse because I placed CKEditor source code folder inside the module folder. It has 238 folders. I have put it now in /libraries/ instead. (This reduced file_scan_directory to 868 calls, 7 secs in total)

But I think it's quite easy to end up in this situation and very difficult to realize for normal users, unless you have PHP debugging tools.

corbacho’s picture

That dramatic change of speed was due of my own debug patch:
If ($name == orphaned module) return;

But with the patch of #4 still file_scan_directory is called 434 times taking 4 secs.
The problem is that those 4 secs are in *every* single page load.

mfb’s picture

You might want to figure out why the scan is happening on every page load. Theoretically it should only happen on certain paths, such as admin/config which calls system_status().

MaxWesten’s picture

#4 is a drastic speed improvement.

Kars-T’s picture

Subscribe. This is really needed. The Problem killed 80% performance of 2 our D7 sites.

Kars-T’s picture

Status: Needs review » Reviewed & tested by the community

I did test and profile this on a major site of ours. The bug killed the performance and every page call did use 100% CPU power and took around 30 seconds. With suns query fix from #1096742: Disable enabled but missing modules everything runs fine again CPU usage is very low again. But still file_scan_directory eats up 16% of a page-call. With this patch it goes down to 9%. Still a lot but much nicer and there shouldn't be any endless recursion because the directories are scanned over and over again .

The patch itself is very simple. Just one more static variable. And than a check if the directory was already parsed so it is not checked again for this page-call. I say RTBC.

das-peter’s picture

+1 for getting this in asap. We use this patches (this one too: #733308: Add static caching for drupal_system_listing()) in production, too. And everything works like a charm.

webchick’s picture

Assigned: Unassigned » catch

Since this is a performance patch, and since catch already chimed in above, moving to his queue.

alanburke’s picture

Tested patch at #4
Worked and made a dramatic difference on a site.

catch’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +needs profiling

I'd like to see the following before committing this:

Profile a site where this bug is in effect, then compare:

No patch.

This patch.

This patch and #733308: Add static caching for drupal_system_listing().

#733308: Add static caching for drupal_system_listing() without this patch.

Screenshots from either xdebug or xhprof will do fine - can show detail from drupal_system_listing() or file_scan_directory() whichever is most obvious.

Kars-T’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
40.7 KB
35.19 KB
43.2 KB
54.12 KB

Sure I do this :)

Okay I did
drush dl devel feeds job_scheduler
drush en devel feeds feeds_ui job_scheduler -y

and than deleted those modules.

With latest 7.8 bootstrap the load of "/" takes around 20 seconds.
Default

With patch from 1081266#9 the load of "/" takes around 3 seconds.
Image2-7.8-patch_from_1081266

With patch from 1081266#9 and 733308#8 the load of "/" takes around 3 seconds.
image1-7.8-default

With patch from 733308#8 the load of "/" takes around 3 seconds.
Image4-7.8_patch_from_733308

Performance seems to be equal with any patch. But lowest usage of file_scan_directory() is with both. For D8 I'd say test peters a bit more and add it. BUt for D7 I would really love to see the patch from this issue in the next release. If you reorganise features your site will be dead in the current state.

catch’s picture

Status: Reviewed & tested by the community » Needs work

Thanks for the profiling, that means there's no reason to have both patches then.

#733308: Add static caching for drupal_system_listing() is the older issue but I've marked that duplicate.

However to me it makes more sense to add the static caching to either drupal_system_listing() or file_scan_directory() - since that's called by more places (such as the installer which was the original motivation for opening that issue).

mfb’s picture

@catch: since drupal_get_filename() already has a static cache of $files found via drupal_system_listing(), are you suggesting moving the static cache from drupal_get_filename() to drupal_system_listing()?

catch’s picture

@mfb, the drupal_get_filename() cache is primed by http://api.drupal.org/api/drupal/includes--module.inc/function/module_li... on every request, so removing the cache from there isn't an option. The way that cache is primed is a bit odd so we could consider trying to clean that up elsewhere.

Since drupal_system_listing() is only really called via this error condition or otherwise very rarely it seems fine to have the static cache in two places to me - in the usual case they won't both be used at the same time.

Kars-T’s picture

@catch So is this issue again RTBC or what should be done?

klausi’s picture

Priority: Normal » Critical
Status: Needs work » Needs review

Just ran into this myself, this a huge performance show stopper. Escalating to critical. Patch from #9 still applies. Marking for review.

Kars-T’s picture

Status: Needs review » Reviewed & tested by the community

Than again I set this to RTBC. But for D8 I would say it should follow the more complex path that das-peter showed in #733308: Add static caching for drupal_system_listing(). For D7 I say this is essential or any bigger system that uses features will die eventually. D7 really needs some kind of hot fix.

Dries’s picture

Priority: Critical » Normal
Status: Reviewed & tested by the community » Needs work

Committed to 7.x and 8.x. Thanks!

I'd still like to figure out why we keep looking for these files on every request. It feels like we patched the problem vs eliminated the root cause. I've updated the priority and status accordingly. Hopefully we can continue to investigate this.

das-peter’s picture

Thanks & I fully agree, working on this felt always like patching a symptom and not the cause.
While the static cache is a good thing anyway, I've been asking myself if it would be a good idea to disable missing modules and flag them as broken. Idea behind this is that I would like to know if there's anything wrong with my setup.
Next question would be what's considered as broken?
Only if the full module is missing or even if registered files of a module are missing?
What about a consistency check on the module page or an extra report page?

Kars-T’s picture

system_list() is called on every bootstrap. But there is only a cache for the database table. The directories will be scanned on every call.

So is there any reason why we couldn't put the result from the loop in line 185 into the cache?

Only thing I currently can think of we will have a problem if a module is gone missing.

flock’s picture

Version: 8.x-dev » 7.x-dev

Druapl 7.9
Hi, i'm new to drupal and I believe I am suffering from poor performance because of this same reason.

on my site I keep getting Internal Server errors, not enough memory errors and My webhosting company tells me I have to upgrade to the $19.99 per month plan in stead of shared hosting [thanks greengeeks the super useful drupal hosts]

Also, images seem to drop off
Site Logo
Captcha Image
Views gallery - of small rotating logos
-this curiously only on Mac Safari and Mac Firefox

Can any one please instruct me or direct me as to how I can apply or where exactly do I execute that snippet of code so I can find out what modules are causing the super slow performance?.

This is the only post that I found that had a seeming relationship to my problem, my apologies if this is not the right place to post it.

Many Thanks

oriol_e9g’s picture

Version: 7.x-dev » 8.x-dev

First we have to solve this in D8. The most important problem here was solved in D7 and commited in October.

I think that your problem is a suport request, open a new issue.

bmateus’s picture

I've patched my bootstrap with patch #4 and the performance increase is notorious.
Any idea when this might be updated on a D7 official version?
I think it is very important, as most D7 sites I've built all suffer from the same lack of performance.

klausi’s picture

Tor Arne Thune’s picture

bensnyder’s picture

Priority: Normal » Major
onelittleant’s picture

What would the negative implications be of just eradicating invalid system table entries? Perhaps provide an indication on the module / theme / status report admin screens indicating references to modules or themes that don't exist and providing a button to simply delete the references.

http://drupal.org/node/1052250#comment-5113888

onelittleant’s picture

Priority: Major » Normal

You can try this if the problem is related to system table references to modules that don't exist:

http://drupal.org/node/1052250#comment-5113888

emcniece’s picture

Pretty worried about this. I've followed all the links here, done all the patches to 10 different installations of D7 in different combinations, and the only conclusion I can draw is that I simply have too many modules installed.

Initially, page load times were upwards of 70 seconds and file_scan_directory() was being called over 10,800 times each page view. Since updating to 7.x-dev and monkeying around with reducing the number of available modules, I've got this down to ~1500 calls but the page load is still around 16 seconds.

If it helps anyone, I initialized a variable at the top of /includes/file.inc (outside any function, as a global): $ctest = 0; and then threw at the bottom of file_scan_directory(): global $ctest; echo $ctest++. " $depth: $dir<br />\n";

My site has 77 modules installed on top of core, with 37 available but not installed. Is this simply too complex for Drupal?

catch’s picture

You should do a debug_backtrace() from file_scan_directory() to see what's calling it. It shouldn't be called during normal operation at all, but it can be if you have a badly behaving contrib module installed.

I've seen sites with upwards of 300 modules installed, 100 is unfortunately not unusual at all.

emcniece’s picture

DUDE, catch... THANK YOU for the nudge!! I rolled down that path earlier but abandoned when I couldn't get any meaningful data out of debug_backtrace(). Well this time I found the right output - and here's the results:

  • drupal_bootstrap: 67 calls
  • menu_execute_active_handler: 1434 calls

Crazy. What does that even mean, rogue menus? I'm looking at menu_execute_active_handler right now.

For reference, here's what the last few lines of my file_scan_directory() now looks like:


	$bt = debug_backtrace();
	$end = end($bt);
	var_dump($end);
	global $ctest; echo $ctest++. " $depth: $dir\n";
  return $files;
} // end file_scan_directory()

Any ideas by chance?

catch’s picture

You're looking at the first function in the backtrace, instead find file_scan_directory() in the full backtrace then work up from there - the first couple of functions you find outside file.inc are a good place to concentrate on.

emcniece’s picture

Dear catch: today I learned things.

  1. debug_backtrace() is super handy
  2. I need to be more careful about where I leave my drupal_flush_all_caches() calls
  3. 16 hour days suck
  4. the Drupal community is a godsend

For anyone else: drupal_flush_all_caches() slows sites down BIGTIME and seems to initiate a good number of file_scan_directory() calls through whatever cache-clearing things it does. This didn't really have much to do with the actual thread topic, but the directory scans instead turned up as a symptom. Thanks again for your help.

sertaconay’s picture

Status: Needs work » Needs review

drupal_get_filename.patch queued for re-testing.

ExTexan’s picture

When I went to apply this #9 patch (manually) in D7, I saw that the *new* code is already in the version of D7 I'm running (7.14), but I'm still experiencing very slow initial loading of my front page. This is running locally on a virtual host in Win7 with WAMP. After the initial load time of 15-20 seconds, my pages load within a second or two thereafter.

Anyone have any suggestions of what else to try / look for?

mikeytown2’s picture

Version: 8.x-dev » 7.x-dev
FileSize
1.75 KB

@sertaconay
There is no active patch above to review, Dries in #37 committed patches to D7 (#4) & D8 (#9).

I've attached a patch for 7.x that fixes this issue for us. Once testbot gives the A-OK, go ahead and move the issue back to 8.x.

DerekL’s picture

Wow.

The above patch just turned my glacial D7.14 ZWAMP dev env into a rocketship.

You all deserve beers, thanks.

mikeytown2’s picture

Version: 7.x-dev » 8.x-dev
FileSize
1.77 KB

Patch for D8 that should fix this issue as well.

mikeytown2’s picture

Status: Needs work » Needs review
FileSize
1.79 KB

Using the D8 caching interface now :)

mikeytown2’s picture

What happened to CACHE_TEMPORARY?
http://drupalcode.org/project/drupal.git/blob/58964deb124e55d61f51a167d5...

API says it's still in there: http://api.drupal.org/api/drupal/core!includes!bootstrap.inc/constant/CA... and is referenced here http://api.drupal.org/api/drupal/core!lib!Drupal!Core!Cache!CacheBackend...

Going to re-roll patch with -1 instead of CACHE_TEMPORARY as a temp solution to this problem.

aspilicious’s picture

#730060: Replace CACHE_TEMPORARY, cache_clear_all() and minimum cache lifetime with cache tags support

The api docs have troubles with new commits. It can take weeks before it gets refreshed.

mikeytown2’s picture

So I need to add in 'drupal_get_filename' to the list of caches getting reset in system_list_reset(), or is there a better way to do this?

tsphethean’s picture

I came across this issue when looking at why D6 was querying the DB on every module_load_include call and thinking of how to mitigate.

My understanding of the issue is that it fixes the edge case where the DB isn't available to lookup the module path (or its not specified in system table), and the module directory no longer exists

Is there a reason why even with the improved D7/8 drupal_get_filename doing file scans and then caching the list of bad files, why we wouldn't add the list of found files to a cache table for retrieval by future calls? I guess it would need profiling, and maybe be a configurable option depending on what caching mechanism was being used, but would looking up a value from a cache not be more efficient across requests? My thought is that on a production site the files aren't likely to change much in between requests, so the efficiency gains could be quite sizeable?

If this feels like getting off topic (or has been raised as another defect elsewhere) I'm happy to move it out of this issue and create a new one, and equally happy to attempt some work on a patch if it feels like a valid way of thinking.

tsphethean’s picture

Ok, so I've had a go based on my comment in #68 and @mikeytown2's good work in #64 at creating a cache which stores the "good" cache entries, and an additional cache for the "bad" or missing files being included.

I need to do more profiling to determine if there is any performance benefit of this cache on a vanilla installation, and also with alternative cache backends, but thought it best to see what testbot and others think.

My working assumption is that retrieving from the cache should always be more optimal than a db query even when the cache is in the db, (flawed assumption?) but that the benefits of this would be felt in complex sites with 100+ modules, particularly custom modules, with dedicated caching solutions.

tsphethean’s picture

Hm. Not sure I quite understand this. Output from the test failure is:

Basic standard + all profile upgrade path, bare database 25 passes, 33 fails, and 39 exceptions

Fatal error: Cannot use object of type stdClass as array in /var/lib/drupaltestbot/sites/default/files/checkout/core/includes/bootstrap.inc on line 2901
FATAL Drupal\system\Tests\Upgrade\LanguageUpgradePathTest: test runner returned a non-zero error code (255).
- Found database prefix 'simpletest870634' for test ID 468.
[15-Aug-2012 17:42:17] PHP Fatal error:  Cannot use object of type stdClass as array in /var/lib/drupaltestbot/sites/default/files/checkout/core/includes/bootstrap.inc on line 2901

but that doesn't appear (from what I can figure out) to be anything that calls or is related to the drupal_get_filename function which is being modified.

Any suggestions welcome (not done much of this so still getting to grips with testbot)

tsphethean’s picture

Status: Needs work » Needs review
Issue tags: -Performance, -Needs backport to D7, -needs profiling
tsphethean’s picture

Ah - error in the variable name I was saving in the cache set following the file scan.

This now passes the failing test in my local environment now so resubmitting.

tsphethean’s picture

Status: Needs work » Needs review
tsphethean’s picture

Definitely confused by these test failures. All upgrade path tests pass on my local site. Does anyone else get any failures from this patch?

tsphethean’s picture

Status: Needs work » Needs review
FileSize
7.24 KB

Updated patch - passes tests locally.

disasm’s picture

corbacho’s picture

andypost’s picture

Spleshka’s picture

I think that every time when admin submits module list, Drupal should check all enabled modules and clean up orphaned file names from {system} table.

mikeytown2’s picture

Issue tags: -Needs backport to D7

The patch for D7 works great, the D8 patch has issues. Thus I'm changing the tags to show this.

Spleshka’s picture

D7 patch looks good to me too.

mgifford’s picture

Status: Needs work » Needs review
mgifford’s picture

If you use http://drupal.org/project/clean_missing_modules then this shouldn't be a problem right?

The patch in #80 is for D7 or D8? Just the Version is for D8, but people are talking about applying this to D7.

@tsphethean any direction on where this should be applied? It can't be for both D7 & D8.

tsphethean’s picture

Sorry, yes - #80 is a D8 patch but is pretty old so might need a re-roll.

tsphethean’s picture

Trying a re-roll against latest D8 build.

tsphethean’s picture

Fixing whitespace issue which prevented patch applying.

tsphethean’s picture

Ok think I've made a mess of my local site - rebuilding and will re-roll the patch by hand tomorrow.

tsphethean’s picture

Having a bit of a look at the revised drupal_get_filename in D8 I'm actually having a few second thoughts about my original approach. We're now doing:

$file_list = state()->get('system.' . $type . '.files');

To get a list of all the files in the system, and then building up the array. Not knowing too much about the new state system, is this going to be DB level state in the first instance, so would benefit from the eventual $files array being cached, or is state going to be cached anyway by the state interfaces?

Grateful for pointers... happy to proceed if it seems worthwhile. Still getting to grips with state, config and things :-)

Anonymous’s picture

Anonymous’s picture

Output of dry-run 1081266-58-drupal_get_filename-D7.patch on Drupal 7.18

patch --dry-run < bootstrap.patch
patching file bootstrap.inc
Hunk #1 succeeded at 816 (offset -27 lines).
Hunk #2 succeeded at 867 (offset -27 lines).
patch unexpectedly ends in middle of line
Hunk #3 succeeded at 895 with fuzz 1 (offset -27 lines).

mikeytown2’s picture

Re-roll of #58 against the latest D7 dev

mikeytown2’s picture

Version: 8.x-dev » 7.x-dev
Status: Needs work » Needs review

Going to test #102 on D7 via testbot. Will move issue back to D8 once patch goes green.

mikeytown2’s picture

Version: 7.x-dev » 8.x-dev
Status: Needs review » Needs work

#102 Passed all tests.
Moving back to 8.x and needs work.

Anonymous’s picture

patch --dry-run < bootstrap.patch
patching file bootstrap.inc
Hunk #1 succeeded at 816 (offset 1 line).
Hunk #2 succeeded at 867 (offset 1 line).
patch unexpectedly ends in middle of line
Hunk #3 succeeded at 895 with fuzz 1 (offset 1 line).

After that, used TextWrangler to ´normalize´ line endings, same result.

mikeytown2’s picture

@StinkFoot
I think you need to use -p1 with your patch commands.

Dry run:

git apply --check -p1 drupal-1081266-102-drupal_get_filename-D7.patch
OR
patch --dry-run -p1 < drupal-1081266-102-drupal_get_filename-D7.patch

Apply:

git apply -v -p1 drupal-1081266-102-drupal_get_filename-D7.patch
OR
patch -p1 < drupal-1081266-102-drupal_get_filename-D7.patch
Anonymous’s picture

@Mikeytown2

Thanks for your support!

After reading what the option does, I´ve looked inside the file and - please correct me if I´m getting this wrong - I see that patch will be looking at

a/includes/bootstrap.inc
b/includes/bootstrap.inc

Patch documentation reads:

For all pathnames in the patch file that indicate the names of files to be patched, delete num pathname components from the beginning of each pathname. If the pathname in the patch file is absolute, any leading slashes shall be considered the first component (that is, -p 1 shall remove the leading slashes). Specifying -p 0 shall cause the full pathname to be used. If -p is not specified, only the basename (the final pathname component) shall be used.

So, NUM would be 3.

Omitting -p should result in patch using the final component and since I run patch in the same directory, that should work.

I tried p0 up to p4 and get the same result every time, so... I am not to sure about your suggestion.

But, always eager to learn, so please enlighten me if I don´t know what I am doing:-)

Merry Xmas!

Update:

After reading my own comment, decided to test the procedure by creating the directories and try to apply the patch from their mutual parent.
Failed.

Lukas von Blarer’s picture

I am running the patch in #102 in production since today and I am experiencing an insane improvement. My page load times went from 2200ms down to 500ms! So far I am not facing any problems. Thank you!

PlayfulWolf’s picture

Is this patch already applied on last D7?

mikeytown2’s picture

@PlayfulWolf
Don't think so. https://api.drupal.org/api/drupal/includes!bootstrap.inc/function/drupal...

D7 patch #102 is RTBC, D8 is still needs work.

mikeytown2’s picture

Issue summary: View changes

Created an issue summary.

nbourdial’s picture

Hi,
any idea when the #102 patch will be commited and released to the next D7 release ?
For info, the patch saved 3.5sec for each page load (7sec -> 3.5sec), for a Drupal instance with about 250 modules enabled !

Thanks for the patch anyway !

oriol_e9g’s picture

Only for information, I'm using this code since D6 to remove all missing files from system table.

Why we can't add a similar cleanup process every time we launch update.php?

/**
 * Remove missing modules.
 */
function mymodule_update_6001() {
  $result = db_query("SELECT * FROM {system}");

  $items = array();
  while ($record = db_fetch_object($result)) {
    $record->info = unserialize($record->info);
    $items[$record->name] = $record;
  }

  foreach ($items as $item) {
    if (!file_exists($item->filename)) {
      $ret[] = update_sql("DELETE FROM {system} WHERE filename = '" . $item->filename . "'");
    }
  }

  return $ret;
}
nicholas.alipaz’s picture

Issue summary: View changes

I couldn't read through all of the over 100 posts here, but I thought that I would share how I was able to determin which missing modules were causing this issue on our server.

for i in $( drush sqlq "select filename from system where filename like 'sites/SITEDIRECTORY/%'" ); do if [ ! -f PLATFORMDIRECTORY/$i ]; then echo MISSING: $i;drush sqlq "select status from system where filename = '$i'"; fi; done;

Where PLATFORMDIRECTORY = '/var/www/drupal' and SITEDIRECTORY = 'example.com' or possibly 'all' depending on your setup.

Running that from cli will give you the name of any missing files and the status of the module that is missing. Example output:

MISSING: filename
MISSING: sites/www.example.com/modules/contrib/addressfield_phone/addressfield_phone.module
status
1
MISSING: sites/www.example.com/modules/contrib/taxonomy_menu_trails/taxonomy_menu_trails.module
status
0
MISSING: sites/www.example.com/modules/contrib/views_tree/views_tree.module
status
0

As you can see I found my module causing the issue.

mikeytown2’s picture

Issue tags: +needs forwardport to D8

Adding tag to show that we have a RTBC 7.x patch in #102 but not a 8.x patch.

ChristophWeber’s picture

Applied #102 to 7.19 core, which has 148 enabled modules per drush pm-list, and runs on a well tuned cluster. We do not have any missing modules or files AFAIK.
Patch works well, no issues, but also no real speed improvement.

mikeytown2’s picture

@ChristophWeber
The speed improvement only happens if you have a missing module. Thanks for the info on the 7.x patch though.

marcelovani’s picture

I have a situation where an empty string is passed as parameter $name, which causes some extra loops for no reason.

This is also causing this error message:

The following module is missing from the file system: .

See a proposed fix here https://www.drupal.org/node/2383823#comment-9385897

I applied both patches (issue 9385897 and issue 9386005) and I can see they both work well together.

ps: I am using Drupal 7.34

jeroen.b’s picture

This issue is more relevant than ever, see the related issue.
I'll re-roll and test this patch soon.

mikeytown2’s picture

#102 still applies with an offset of 21 lines and is RTBC from my understanding. In order for this to get into D7 a patch for D8 needs to be created OR it needs to be proven that this is fixed or is no longer relevant in D8.

jeroen.b’s picture

Ah okay, thats good.
Checked in D8 beta 3, still seems relevant. Code got a rewrite but I think it still has the same issue.
New patch attached, created from D8 git head.

jeroen.b’s picture

Hmm, guess watchdog isn't always available at that point. Any suggestions?

mikeytown2’s picture

Status: Needs work » Needs review
FileSize
4.12 KB

http://www.appneta.com/blog/drupal-8-watchdog/

Drupal::logger() is the way of the future; but your patch already uses that.

Drupal::logger() -> LoggerTrait::error() -> DbLog::log() which writes to the watchdog table. Having an error here seems like a separate issue.

The profile being missing has to do with the code in ExtensionInstallStorage::getAllFolders(); it calls drupal_get_profile() which returns NULL and then InstallStorage::getComponentNames
('profile', array(NULL))
is executed resulting in the error.

I've modified your patch in #120 so it won't call getComponentNames on a null array and I've also incorporated the fix for this from #2383823: drupal_get_filename() should check if $name is empty

mikeytown2’s picture

Line 28 is where it went fatal and it's the function definition.
https://api.drupal.org/api/drupal/core!modules!system!src!Tests!Bootstra...
Not sure where to go from here

EDIT:
Running the test locally I get
ResponseText:
( ! ) Fatal error: Call to a member function get() on a non-object in /core/lib/Drupal.php on line 262
Call Stack
#TimeMemoryFunctionLocation
10.0010150168{main}( )..\index.php:0
20.16301262768Drupal\Core\DrupalKernel->handle( )..\index.php:24
30.33404883904Stack\StackedHttpKernel->handle( )..\DrupalKernel.php:579
40.33404884008Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle( )..\StackedHttpKernel.php:23
50.33404884048Drupal\Core\StackMiddleware\PageCache->handle( )..\ReverseProxyMiddleware.php:58
60.37005466664Drupal\Core\StackMiddleware\KernelPreHandle->handle( )..\PageCache.php:52
70.42908183680Symfony\Component\HttpKernel\HttpKernel->handle( )..\KernelPreHandle.php:53
80.42908183928Symfony\Component\HttpKernel\HttpKernel->handleRaw( )..\HttpKernel.php:66
90.636011414640call_user_func_array
( )..\HttpKernel.php:145
100.636011414864Drupal\system\Controller\BatchController->batchPage( )..\HttpKernel.php:145
110.637011472440_batch_page( )..\BatchController.php:60
120.649012191896_batch_do( )..\batch.inc:83
130.649012192056_batch_process( )..\batch.inc:101
140.653012232128call_user_func_array
( )..\batch.inc:256
150.653012232264_simpletest_batch_operation( )..\batch.inc:256
161.880113557656Drupal\simpletest\TestBase->run( )..\simpletest.module:320
173.962217933560Drupal\system\Tests\Bootstrap\GetFilenameUnitTest->testDrupalGetFilename( )..\TestBase.php:869
183.972217980328drupal_get_filename( )..\GetFilenameUnitTest.php:39
193.972217980616Drupal::cache( )..\bootstrap.inc:270

Will work on this some more.

mikeytown2’s picture

OK story is the testing framework sets https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A... to NULL. This causes all calls to the Drupal class to fail it seems; thus I feel like I'm missing something. I've edited the code to work around this so the tests pass.

https://api.drupal.org/api/drupal/core!tests!Drupal!Tests!UnitTestCase.p...

jeroen.b’s picture

Looks like a weird workaround. Are you sure it's the testing framework that sets it to NULL?
Maybe it's being called at a point where the Drupal class isn't initialized yet. I did not look that deep into D8 yet though so would have to do some more searching.

mikeytown2’s picture

Not 100% sure but according to the docs that is what it does

Sets a new global container.
Parameters

\Symfony\Component\DependencyInjection\ContainerInterface $container: A new container instance to replace the current. NULL may be passed by testing frameworks to ensure that the global state of a previous environment does not leak into a test.

mikeytown2’s picture

Brought this up in IRC and the workaround I created for this seems to be ok.

"The idea with that test is to ensure that drupal_get_filename() works in the bootstrap process in which the container is not there for a purpose." - dawehner

I'll document this so people know why we're checking types.

mikeytown2’s picture

Added this comment to the code
In early stages of the bootstrap process the container may not be set inside of the Drupal class.

Anonymous’s picture

passing NULL/FALSE/'' to method_exists is safe, so this:

+      if (!empty($cache_object) && is_object($cache_object) && method_exists($cache_object, 'get')) {

can be shortened to:

+      if (method_exists($cache_object, 'get')) {

this check:

+    if (empty($bad)) {

will be true over and over again there's no data in $cache_object->get('drupal_get_filename:bad', TRUE);, which doesn't seem to fit the intention of the static. better to initialize the static as $bad as NULL, and check for is_null.

marcelovani’s picture

I can't see any problem in doing extra checks. Is that about having extra bytes in the code? making the execution slower?
We should seriously focus on getting this fix applied rather than debating what is better.
BTW, nothing can be worse than the current implementation.

jeroen.b’s picture

I think @beejeebus is right though. Better fix it properly now.
You don't want to execute an SQL query (for a cache lookup) every time you hit that piece of code.

And extra checks can really make your code more unreadable, so why add extra checks when you don't need them?

jeroen.b’s picture

And let's fix that mistake in the 7.x patch too while we're at it.

jeroen.b’s picture

Version: 8.0.x-dev » 7.x-dev
jeroen.b’s picture

Version: 7.x-dev » 8.0.x-dev

D7 patch passed, so back to D8

jeroen.b’s picture

Priority: Normal » Major

Changed to major as this issue keeps popping up for me, for example when you delete a module without uninstalling it (which could happen quite often when you use makefiles/profiles).

I also think more people are having this issue without them knowing it.

mikeytown2’s picture

Both patches (#137, #138) look good. +1 from me. Also agree that this is an under reported issue.

askibinski’s picture

Yes, this can really cripple your servers with high CPU loads.
Not only with deleted modules , but also if you have an unpatched version of imagecache_actions which contains a typo which triggers this bug.
#2166715: Typo causes extra query every page load

marcelovani’s picture

Status: Needs review » Reviewed & tested by the community

Lets get this committed, shall we?

jeroen.b’s picture

Hmm, how we get this working for D7 and D8 at the same time. Seems impossible...

marcelovani’s picture

Once they apply the patch on D8 they will change the issue to be backported to D7

marcelovani’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
5.39 KB

Re-submitting the patch

swentel’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

Not a fan of the extra call of cache_get so early. The changes in Drupal.php to return NULL funky and shouldn't really happen there.

I'm also leaning more to the idea of cleaning up when installing/uninstalling modules, see comment #86.

Also, needs tests.

Berdir’s picture

What about adding a hook_requirements() check that looks for missing modules and gives you a link that you can click that will drop that module from the system?

That is also a good place to place a bold red warning that you should never ever just remove a module without uninstalling it first. We're putting a huge amount of efforts into D8 to make uninstallation save and not leaving behind configuration and data and not being able to uninstall when a module is still used, just removing a module breaks all that. What if that module for example provides a bundle or a field type that is still used?

oriol_e9g’s picture

+1000
See #112

I also think that it's better remove invalid entries than workaround.

joseph.olstad’s picture

jeroen.b's patch from comment # 137 and #138 makes sense to me

The following comments between #151, 152, 153 talk about Removing module schema information without having run a proper uninstall.

mikeytown2’s picture

Status: Needs work » Needs review
FileSize
3.75 KB

@swentel
I Didn't see Drupal::hasService(); will use that instead of the funky NULL as Drupal::hasService() is used elsewhere in drupal_get_filename(). Also moved the cache to right before we do a filesystem scan; prevents a cache get for the vast majority of calls. Thanks for the critique!

joseph.olstad is correct; removing module schema information without having run a proper uninstall is why this logs the missing module; so an admin can properly deal with it. Since the code is missing there is nothing drupal can should auto do.

Agree with Berdir; adding an additional check in hook_requirements for missing modules sounds like a good idea. I do think we need both solutions; hook_requirements & avoid re-scanning module directory. If we implement an additional check using hook_requirements, we could probably get rid of the logging inside of drupal_get_filename().

mikeytown2’s picture

Status: Needs work » Needs review
FileSize
3.76 KB

new patch with less php syntax errors™

mikeytown2’s picture

Thinking I'll have to install D8 if this fails again...

mikeytown2’s picture

Title: Avoid re-scanning module directory when multiple modules are missing » Avoid re-scanning module directory when a module is missing
Status: Needs work » Needs review
jeroen.b’s picture

I just tested this against HEAD, \Drupal::hasService('cache') and \Drupal::hasService('logger') don't return TRUE, so the cache never gets created.

How would we create tests for this?
AFAIK \Drupal::hasService('cache') returns FALSE in the testsuite so we have to find some other way to test this.

Regarding #152, I think we should do that in a separate issue.

jeroen.b’s picture

Ah, it had to be \Drupal::hasService('cache.bootstrap') and \Drupal::hasService('logger.factory')

New patch attached which fixes that. Just tested locally and now I get cache and a watchdog message.

joelpittet’s picture

Regarding #137 D7 version @jeroen.b

After applying that patch I got this error:

Exception: theme() may not be called until all modules are loaded. in theme() (line 1017 of /Users/joel/Sites/mysite/html/includes/theme.inc).

And on drush rr following that message:

WD system: The following module is missing from the file system: .

Second drush rr went through ok, but didn't resolve the first on page exception.

Omega 4 is the theme being used.

jeroen.b’s picture

@joelpittet
And you are sure those messages are related to this patch?

das-peter’s picture

@joelpittet Any chance you updated rules recently? Because: #2324587: Rules might be triggered too early in the bootstrap This can trigger the theme() exception as far as I remember.

joelpittet’s picture

Yes it was related to this patch it was the only one applied at the time, accept it was d7 patch in #138

@das-peter I just woke up but totally, I do plan on applying that when I get in. Thanks:)

joelpittet’s picture

Didn't have my coffee, re-read #165, I'll try them together and report back:)

joelpittet’s picture

@das-peter That rules patch in #165 didn't resolve the issues mentioned in #163 but that rules patch didn't cause any new problems, so far.

@jeroen.b #138 issues I mentioned in #163 are quite repeatable.

Directly after applying the patch, run registry rebuild.

The registry has been rebuilt via registry_rebuild (A).   [success]
http://site.dev|severity=alert|type=system|ip=127.0.0.1|uri=http://site.dev/|referer=|uid=0|link=|message=The following module is missing from the file system: .
WD system: The following module is missing from the file system: .   [error]
All caches have been cleared with drush_registry_rebuild_cc_all.    [success]
jeroen.b’s picture

@joelpittet
Can you pastebin the output of drush pm-list?
I can't reproduce, even when I use Omega.

I do think I have seen this behaviour before. I think it was when I did a drupal_get_path on a module that wasn't enabled/installed and also wasn't found on the filesystem. Nervermind that, that makes no sense.

joelpittet’s picture

Totally here, sorry it's long: https://gist.github.com/d7e1da8b7113e0f313cb

jeroen.b’s picture

Woah, that's too much to debug for me. Can you try the attached patch?
It adds an backtrace to the watchdog message because I think this is in some contrib module.

joelpittet’s picture

Issue summary: View changes
FileSize
198.21 KB

@jeroen.b thanks for that it gave me an idea. The backtrace is super huge too, so weeding through that wouldn't be fun, I can still post it if you prefer, but I put a breakpoint there to see the context.

The $name is empty, but the $matched_name was addressfield.

Would that have an effect on this patch?

joelpittet’s picture

Digging through the callstack and two other modules were mentioned workbench_moderation and linked_field, though I have my money on workbench_moderation being the trouble maker... it's an invaluable module but has to fight harder than most to do what it does.

jeroen.b’s picture

Nah, addressfield just was the last value in the array $matches, so the last module it was able to find.

And seeing the menu_rebuild, now I remember where I have seen it before.
It's when you have a menu_item with a wrong "file path" value. Can you gist the backtrace?

joelpittet’s picture

Here's the backtrace: (formatted with pre in the patch)
https://gist.github.com/bf0ee1fe8feb398647ad

joelpittet’s picture

Oh elysia cron comes into the call stack too as the $parent in _menu_router_build()
$item['file path'] = drupal_get_path('module', $parent['module']);'

admin/config/system/cron

joelpittet’s picture

joelpittet’s picture

The module name is sometimes empty, I patched the elysia_cron one but the next one is with commerce commerce_tax_ui_tax_rate_title.

I'm trying to decide if this is something we can/should fix in this patch, or should I just use this and file patches on all those modules (daunting argh)

mikeytown2’s picture

Just a heads up #1158322-199: Add backtrace to all errors is handy for error backtracing. In this case, the linked issue wouldn't have helped; but I do find the linked issue to be super helpful.

joelpittet’s picture

FileSize
225.58 KB

The tax one doesn't have an issue opened up for it and it's a bit different but very similar.

The 'commerce_tax_ui' builds the hook_menu with
'file' => 'includes/commerce_tax_ui.admin.inc', but without a module key again. But in the screenshot the path has already been filled out... :S

jeroen.b’s picture

I can't reproduce that with the commerce_tax_ui.
Maybe there is some weird hook_menu_alter somewhere?

Anyway, I don't think we should fix these things in the patch. The stuff we find with this patch is already broken, only difference is that we can see what's broken now.
It's up to the maintainers now to fix their modules.

joelpittet’s picture

@jeroen.b very well could be, I'm a bit worried about the error produced in #137. Though I can't figure out why this patch would cause that at all... sorry for holding this up.

Grateful though that this indirectly solved elysia_cron issue.

jeroen.b’s picture

@joelpittet
Don't worry about holding this up, this issue has been here for a few years ;)
About the error in #137. Are you talking about the error you reported in #163?

Exception: theme() may not be called until all modules are loaded. in theme() (line 1017 of /Users/joel/Sites/mysite/html/includes/theme.inc).

This one?

I could not reproduce this. Not with a clean Drupal 7 HEAD, Drupal 7.34, Drupal 7.34 with Omega and Commerce.
Can you figure out what module (indirectly) calls theme()?

joelpittet’s picture

After repairing elysia cron I've not seen this error since but I'm keeping my eye on it, the patch is on my staging environment till I deploy the next round of changes.

jeroen.b’s picture

I heard you guys want tests.
Here's a D8 patch with a testDrupalGetFilename() assert. I had to change the $bad records to a drupal_static to reach it from the test.
We can't use the bootstrap drupal_get_filename:bad cache for tests as the cache isn't available in the tests.

I'll look at tests for the D7 version later.

jeroen.b’s picture

Issue tags: -needs profiling, -needs forwardport to D8

I think these tags aren't relevant anymore

jeroen.b’s picture

Here is a D7 patch with tests.

mikeytown2’s picture

Version: 8.0.x-dev » 7.x-dev

changing to 7.x for testing

jeroen.b’s picture

Issue tags: -Needs tests

All green. Let's get this committed?

swentel’s picture

Version: 7.x-dev » 8.0.x-dev
Assigned: Unassigned » swentel
Status: Needs review » Needs work

Well, first of all, this should go in D8 first. Secondly, see comment #150 and #151. Doing this kind of optimization at run-time really isn't the proper way to fix this bug. To be clear: it is a bug (sometimes extremely annoying), I've actually hit it a couple of times as well, but I disagree that we should fix this at run time. There are better, cleaner ways (order is my preference)

  1. hook_requirements() with a link to clean up
  2. running update.php
  3. when submitting the modules page
  4. maybe, maybe, maybe when clearing full caches, although that's maybe over the top

I'm in favor of number 1 because we then also have a UI that informs the user which module(s) are actually missing. All the other variants, which may trigger watchog, are less obvious and are easily missed.

I'm in London next week for drupalcamp london, I'll try to write up a patch during those days.

jeroen.b’s picture

I'm all for really fixing this, but sometimes that just not possible by just cleaning the registry.
Sometimes other modules call drupal_get_path manually with wrong arguments, or with disabled modules, I have seen a couple of cases.

Sometimes you can't properly uninstall a module, we deploy all sites with drush make, if we remove the module from the makefile, the record is still in the registry.

My plan was to get this patch committed and then create a new issue where we add the hook_requirements functionality. If you want to include that in this patch that's fine with me, but please also use the existing patches.
Currently if something goes wrong in drupal_get_filename, you have absolutely no idea.

mikeytown2’s picture

Assigned: swentel » Unassigned
joseph.olstad’s picture

I agree with jeroen.b , there's going to be situations where admins of large scale sites managed by committee that can't just decide by themselves if or when to cleanup something and determine if it was something left by a drush make or if it was a module that was removed from the filesystem by mistake. The best approach is his solution because it fixes the performance problem and also is noisy enough so that admins will be able to see what is going on and let them take a reasonable course of action without a huge performance penalty.

Prior to this patch not only are admins left in the dark but there's also a huge performance penalty. Lets get this committed. We've been running patch #102 in production for a while and I just reviewed patch 189 and it looks good. So for those working on D8, please follow the advice of jeroen.b because not only do we want to see D8 improve but we also are running production D7 sites that pay for our work on D8.

heyyo’s picture

I'm using the patch from #189, I'm not sure what's wrong but it reports me this error:
"The following module is missing from the file system: mothership"
Mothership is not a module, but a theme, and this theme is present in sites/all/themes.
Mothership is not enabled(but was enabled before), I had the version 2.x before, but now I'm using 3.x.
Not sure how to fix that ?

SocialNicheGuru’s picture

@heyyo, do a "grep -in mothership" in your module and themes directory.
There is probably a call to a file in that module somewhere or a misspelling.

an issue where something like this occurred to me:
https://www.drupal.org/node/2428105

here is another:
https://www.drupal.org/node/2375741

heyyo’s picture

Thanks a lot @SocialNicheGuru it was indeed the problem with this file mothership/mothership/panelsstyles/motherstyle/motherstyle.inc
Including 3 times :

'path'    => drupal_get_path('module', 'mothership') .'/panelsstyles/motherstyle'
joseph.olstad’s picture

The nice patch from by jeroen.b from comment #186 needed to be refactored after D8 core commit #40cc6312 for issue #2090115 by alexpott, xjm: Don't install a module when its default configuration has unmet dependencies.

Attached is a refactored patch for D8 accordingly.

joseph.olstad’s picture

Status: Needs work » Needs review

D8 patch #202 passed all tests march 26, 2015 22:00 0400+gmt
D7 patch #189 passed all tests in February 2015
D6 not yet backported, lets commit the D8 and D7 patch first.

David_Rothstein’s picture

Adding a cache to this kind of low-level function looks odd to me (others above have said the same thing)... Also there is no code that ever clears the cache (except on a sitewide cache clear)? Can't it get stale?

+    if (\Drupal::hasService('logger.factory')) {
+      \Drupal::logger('system')->error('The following @type is missing from the file system: @name', array('@type' => $type, '@name' => $name));
+    }

Maybe a simple trigger_error() is better here? That should get it into the logs anyway, plus onto the screen on dev sites (a key part of this, so developers can find mistakes in their code that lead to this). This is sort of getting into the territory of #2380361: Latest tweaks to bootstrap.inc can make Drupal sluggish when there is code that tries to load a file that doesn't exist but the two issues are closely related. I tend to think warning about this problem is realtime is better than trying to fix it with a cache in realtime.

joseph.olstad’s picture

When developers are removing things from the filesystem they're usually:
A) running a drush make script
B) performing filesystem operations perhaps from a script
C) changing in or out modules, in which case a full cache clear is run.
D) upgrading a Drupal site from one distribution to another
E) upgrading a Drupal distribution from one version to another (that may be missing files seen in previous versions and lacks appropriate hook_updates)
F) upgrading Drupal core and forgot to restore a folder such as sites/all/modules/contrib
G) any combination of the above combined with updatedb

Also there is no code that ever clears the cache

operations C, D, E, F, G will trigger a site-wide cache clear, so the cache is not stale.

Adding a cache to this kind of low-level function looks odd to me

and so does re-scanning module directory when a module is missing

Maybe a simple trigger_error() is better here

sure it would be nice what about the performance issue? Example: webform_conditionals from webforms 3.x was swallowed up by webforms 4.x , but if you run a pm_uninstall on webform_conditionals 3.x are you sure that you're not going to remove tables that are now used by webforms 4.x? So now you have a performance issue while your production site which needs the new features from this new module that swallowed up another module but the schema table says it's still installed but you've removed it from the filesystem because the webforms readme.txt told you to do so. No one paid attention to this before until they realised their site was slower than it used to be so they went on to find some performance fixes and applied this patch. Now they're aware of multiple other problems that they were not aware of, but no problem, this patch fixes the performance issue without having to immediately deal with the mess undernieth that you will need a committee to sit together and point the finger and look at test results and build behat tests for and wait for approval to make the necessary changes, meanwhile the clients are frustrated that your site is slow except with this patch thankfully we get both INFORMED and RESTORED PERFORMANCE.

Meanwhile, much of the drupal community sits in ignorance wondering how entropy is creeping in and slowing down their sites, not knowing how to resolve it, they start thinking that the best solution is to re-install and start from scratch and hope that the performance problem doesn't come back. Or worse yet, they say, hey, lets install Joomla instead.

Why don't we just commit the patches, illuminate the drupal community and put the smiles back on the developers faces and the confidence back in their ability to administer sites, we get performance back and we also get informed of what is going on! Otherwise we're completely oblivious as to what is happening and why our sites become so damn slow over time. Finding this patch was a stroke of good luck. Thanks to all those that created it and pushed it forward.

RTBC + please

mikeytown2’s picture

100% agree with joseph.olstad #205.

Will also say caching a bad list makes sense due to typo's when calling drupal_get_path(), see #199. Also see this issue where the bad module name was ''. Having your site be slow while you have no idea where to look sucks #2407129: Missing module<empty> on menu rebuild because of bad admin/config/system/cron override. These 2 issues point out that with the mothership theme or the Elysia Cron module installed and this patch not enabled results in a much slower Drupal install for those using the code; which is around 20k sites (counting the d7 installs).

I agree that in an ideal world we wouldn't need a cache here; but we're on the web where bad things happen all the time (typos) and bureaucracy can tie your hands.

joseph.olstad’s picture

A while ago I created a drush tool for detection and auto cleanup of phantom modules based on comment #112 however even with that, my tool only cleans out phantom modules and does not yet clean out phantom themes. With that said, I have run it on one production site but not on another. The patch that everyone here has worked hard on over the past 4 years correctly informs us and gives us the luxury of delaying such a cleanup without a performance penalty. There may be some scenarios where a cleanup has to be delayed for long periods of time. I think we've already outlined the importance of this but just to show you how much this issue is important it's why I am sharing this.

joseph.olstad’s picture

A small drip for Dries and D8, a nice warm douche for all of Drupal kind!

David_Rothstein’s picture

When developers are removing things from the filesystem they're usually ... trigger[ing] a site-wide cache clear, so the cache is not stale.

True, I guess the cache only goes stale if a missing module in the filesystem is added back, and most people probably do expect to have to clear the site's caches when doing something as big as adding a new module (although it's not always technically required now).

Keep in mind that for the trigger_error() solution, it would not just be that - the idea would be to combine it with something like #195 to fix the cases that actually can be fixed. (I'd add "run it on cron" as a possible option to the list of places in that comment where the fix could be done, by the way.)

jeroen.b’s picture

Replacing the watchdog with trigger_error() is fine with me. I'll see if I can update the patch soon.

I also agree with joseph.olstad here. Why don't we RTBC this and figure out #195 in another issue?
As in my opinion idea 1 (which I think is the way to go) from #195 requires a new discussion and it only addresses a small part (dirty system table) of what this patch addresses.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

I am moving this back to RTBC, I think the cache is okay, because it can only get stale if you add a new module or move a module and then the module_implements cache is as much a problem as this cache.

stefan.r’s picture

Is the D7 patch in #189 in sync with the D8 patch, and equally RTBC, so both can be committed at the same time? Or does the D7 patch need work still?

jeroen.b’s picture

D7 and D8 patches are in sync, they contain the same fixes and tests.

Fabianx’s picture

#212 However 8.0.x needs to be committed first and then the D7 patch is discussed.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/includes/bootstrap.inc
@@ -208,10 +208,23 @@ function config_get_config_directory($type = CONFIG_ACTIVE_DIRECTORY) {
+  // We use drupal static for the bad records so we can test it.
...
+    $drupal_static_fast['bad'] = &drupal_static(__FUNCTION__ . ':bad');

bad is a really odd name. How about missing or something more descriptive.

Also the issue summary needs a update since it says that we're only going to statically cache this but no we're putting it in cache_bootstrap too. Which like others before me I'm incredibly dubious of. If you move a module by mistake and break your site, it ends up in the cache, you quickly fix your mistake and then it is still not fixed until you obliterate all the caches. I'd prefer to be more conservative here and just implement the static cache.

jeroen.b’s picture

Yes, in that one example, the cache might be a bit of a struggle. However, in most other examples the cache will come in very handy as this function might be very heavy on every page load.

When I would do something like moving a module by mistake and then place it back, my first idea would be to drush rr and drush cc all...

joseph.olstad’s picture

Jeroen.b makes another valid point in #216 that re-iterates what I already described in #205. We have a high performance solution here that is very practical in realworld use, many have been using it in production (at least the D7 version) and is urgently needed for those that are unaware that they need it (because everyone running Drupal should have this fix). I'm not sure why we keep going back to needs work. 4 years of work, it's more than enough time. IMHO, there aren't enough valid reasons to redesign at this point something that is practical in the realworld as this is and guarantees that performance will not be affected by bone-head operations. Meanwhile admins will be aware of what is going on. All of this has been stated many times.

*everybody* using Drupal needed this 4 years ago when this issue was openned (or even 8 years ago when people didn't know the problem existed but they should have had this fix) and people still need this today and they'll need it tomorrow, those that don't have it should have it.

alexpott’s picture

Caching a cache miss - which is what we are doing is generally a really bad idea. That said perhaps it is an acceptable trade-off if we ignore or clear this cache when we list modules or themes. Or rebuild it on cron as suggested by @David_Rothstein in #209

And bad is still a bad name :)

Fabianx’s picture

At least in Drupal 7 we can use CACHE_TEMPORARY for clearing on cron and a low ttl (24 hours?) for Drupal 8 instead of CACHE_PERMANENT.

In addition to that clearing this when visiting the admin/modules page sounds good to me.

We used to rebuild block caches since ages when visiting the blocks page, so that makes sense.

#218

Caching cache-misses is strange, right, but not caching cache misses is kind of an anti pattern too.

Usually:

$cached = cache_get('key');

if ($cached) {
  return $cached->data;
}

// rebuild $data
$data = complex_operation();

cache_set('key', $data);

return $data;

So every cache_get() should usually be followed by a cache_set, which includes setting empty values or FALSE.

That said, we can't store the information that the module is not there in the original modules files cache?

#217: Keep pushing with good focus (and address feedback) and it will get in - one day :). Most issues that have someone dedicated to them found their way into core eventually.

Issues like this stall only for years, because they are sitting waiting for review or feedback or ... And usually that is lack of "resources" rather than lack of interest.

joseph.olstad’s picture

Alexpott, you proposed two options: "ignore or clear this cache when we list modules or themes"
this seems like a reasonable option (to ignore the cache for that case, clearing it is really not necessary as Jereon.b and I have previously explained)

However : "Or rebuild it on cron as suggested by @David_Rothstein in #209"
I am less favourable of this option because cron is already very resource intensive and doesn't need more load.

with that said, I like even better the "acceptable tradeoff" as you stated that the current patch gives. Slam dunk this.

stefan.r’s picture

Status: Needs work » Needs review
FileSize
5.64 KB
6.83 KB

I concur with joseph.olstad about the importance of fixing this ASAP, and while I agree this is not an ideal implementation I quite like both suggestions in #219 as acceptable workarounds.

This path renames bad to missing and invalidates the missing file cache on cron runs as well as when visiting module/theme list pages.

stefan.r’s picture

Actually this patch doesn't clear the static cache so whoever visits the site right after a cron run may still be hit with a slowdown, correct?

I assume that if we have missing files, clearing up the static cache as well as the Drupal cache in the middle of a cron run will mean that we rebuild it further down that same cron run as there's likely new calls to drupal_get_filename()? Or should we clear the caches of the module/theme lists for that as well?).

I realize that load going up during cron runs is a concern, but that's what we can do a hook_requirements() thing for in a follow-up issue, and besides, I think it still beats giving significant load slowdowns to unlucky users visiting right after a cache clear :)

stefan.r’s picture

stefan.r’s picture

So I assume this patch would also fix this performance regression listed in the 7.33 release notes?

There is a performance regression after updating to Drupal 7.33 on sites that have code which tries to load a file that doesn't exist - this can cause a file system scan every time the code is called. Finding and fixing the offending code is the best solution. See #2380361: Latest tweaks to bootstrap.inc can make Drupal sluggish when there is code that tries to load a file that doesn't exist for further details.

stefan.r’s picture

Created followup issue over at #2464601: Check for missing modules in hook_requirements so we can do both the caching fix as well as a hook_requirements fix as suggested by many in this issue.

Just wondering if checking the system table in D7 and checking system.module.files / system.theme.data in D8 is enough, or if these missing files can come from elsewhere as well? (in which case we may want to get a list of these files by checking the missing files cache)

joseph.olstad’s picture

ok, so Stefan.r played it safe and went with both recommendations by Alexpott. Good job.

Alexpott, please review this and commit it to D8 so we can agree on a backport to D7 and commit that too!

stefan.r’s picture

@joseph.olstad I had missed your comment over in #220.

I am fine with updating the patch so it merely ignores the cache on admin theme/module pages instead of invalidating it, if we can get a committer to sign off on that :)

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

As {system} is more a registry than a cache I am okay with the patch to catch misses, lets see what Alex says.

Invalidation is better than ignoring, else you wonder why the module page is correct, but your system still does not see the new module, which is worse DX. Invalidation on cron is also not costly.

#227: Please do not tell core committers what to do or not do. There is a lot of voluntary work in there as well and it borderline almost violates our code-of-conduct (not really, but please treat core committers as other fellow co-workers).

I know that the core process can be slow at times, but all recommendations by core committers are done for the whole good of the whole Drupal ecosystem!

I have seen both in Drupal as well as in client work, lines of code that have been 'very simple change', it is so obvious, etc.

And some of them have made quite big problems later. Trust me in the end following the process and getting through good peer review will make Drupal for all of us better in the end.

joseph.olstad’s picture

@Fabianx , invalidation has a performance cost, so it's not necessarily better than ignoring. Stefan.r sees the point in #227. See reasons outlined by Jereon.b in #216 and myself in #205.

I'm fine either way, prefer ignoring the cache to invalidation as I stated previously (because invalidation defeats the purpose of a cache and again for the same reasons already explained). But close enough.

jeroen.b’s picture

#225, yes, I think it's the same issue, maybe it should be marked as duplicate?
Also, what about the trigger_error() idea from #204? trigger_error() will make the error be catched by Drupal's error handler which is more advanced than just a watchdog call.

Rest of the patch in #223 looks fine to me.

stefan.r’s picture

Status: Reviewed & tested by the community » Needs work

Yes @David_Rothstein's suggestion makes sense, let me do a patch.

stefan.r’s picture

Status: Needs work » Needs review
FileSize
684 bytes
6.96 KB

This adds the trigger_error().

stefan.r’s picture

OK so this test fails because we do things like this in the tests:

GetFilenameUnitTest.php:

    // Searching for an item that does not exist returns NULL.
    $this->assertNull(drupal_get_filename('module', uniqid("", TRUE)), 'Searching for an item that does not exist returns NULL.');

AttachedAssetsTest.php

  $build['#attached']['library'][] = 'unknown/unknown';
    $assets = AttachedAssets::createFromRenderArray($build);

    $this->assertIdentical([], $this->assetResolver->getJsAssets($assets, FALSE)[0], 'Unknown library was not added to the page.');
Fabianx’s picture

  1. +++ b/core/includes/bootstrap.inc
    @@ -251,13 +264,24 @@ function drupal_get_filename($type, $name, $filename = NULL) {
    +      if (is_null($missing)) {
    

    We use !isset() instead of is_null() in core.

    See http://3v4l.org/11S72 - it works.

  2. +++ b/core/includes/bootstrap.inc
    @@ -251,13 +264,24 @@ function drupal_get_filename($type, $name, $filename = NULL) {
    +        if (\Drupal::hasService('cache.bootstrap')) {
    +          $cache = \Drupal::cache('bootstrap')->get('drupal_get_filename:missing', TRUE);
    +          if ($cache && $cache->data) {
    

    We don't want to use , TRUE here as we don't deal with invalid data.

  3. +++ b/core/includes/bootstrap.inc
    @@ -265,6 +289,16 @@ function drupal_get_filename($type, $name, $filename = NULL) {
    +      \Drupal::cache('bootstrap')->set('drupal_get_filename:missing', $missing, REQUEST_TIME);
    

    Third parameter should be omitted.

    Currently we cache this for < 1s, hence the need to use TRUE above ...

    Edit:

    Use REQUEST_TIME + 24 * 60 * 60

    to store this for 24 hours.

    It will then automatically be cleaned up by the next cron run.

  4. +++ b/core/modules/system/src/Controller/SystemController.php
    @@ -186,6 +186,12 @@ public function systemAdminMenuBlockPage() {
    +    //  Clean up the bootstrap "missing files" cache when listing themes.
    +    if (\Drupal::hasService('cache.bootstrap')) {
    +      \Drupal::cache('bootstrap')->invalidate('drupal_get_filename:missing');
    +      drupal_static_reset('drupal_get_filename:missing');
    +    }
    

    Should use ->delete() and no need to check for the service.

    I think we want to inject the cache.bootstrap service instead here, but that we can do once the rest of the patch is sound.

  5. +++ b/core/modules/system/src/Form/ModulesListForm.php
    @@ -174,6 +174,12 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +    //  Clean up the "missing files" cache when listing modules.
    +    if (\Drupal::hasService('cache.bootstrap')) {
    +      \Drupal::cache('bootstrap')->invalidate('drupal_get_filename:missing');
    +      drupal_static_reset('drupal_get_filename:missing');
    +    }
    

    Should use ->delete(), and this can rely on the cache.bootstrap being available already.

  6. +++ b/core/modules/system/system.module
    @@ -1201,6 +1201,12 @@ function system_cron() {
    +  if (\Drupal::hasService('cache.bootstrap')) {
    +    \Drupal::cache('bootstrap')->invalidate('drupal_get_filename:missing');
    +    drupal_static_reset('drupal_get_filename:missing');
    

    Should use ->delete() instead of ->invalidate().

    And yes, re-calculating system files once after each cron run is acceptable.

    That said I think we should let garbage collection do the job for us.

Here is a more extensive review.

Fabianx’s picture

#235: That is a unit test, which should probably be moved to its own method with @expectedException annotation present:

https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writ...

stefan.r’s picture

Probably expectedExceptionMessage would be better as if I give the exception type to expectedException it will work like the "@" operator and hide all errors which would hurt the test. There's also 6 test fails and not all of those are PHPUnit tests...

For the other ones maybe we could temporarily set a different error handler which ignores "file not found" warnings?

E_USER_NOTICE still triggers exceptions on the testbot right?

Berdir’s picture

Those are kernel tests. phpunit tests could not be calling functions.

stefan.r’s picture

So currently I'm leaning toward setting set_error_handler() / restore_error_handler() before and after the problematic statements and filtering out file not found warnings...

 set_error_handler(function($severity, $message, $file, $line) {
      // Skip error handling if this is a "file not found" error.
      if (!(error_reporting() & $severity) || strstr($message, 'is missing from the file system:')) {
        return;
      }
      throw new ErrorException($message, 0, $severity, $file, $line);
    }


    // Look for missing file

    restore_error_handler();
stefan.r’s picture

Status: Needs work » Needs review
FileSize
9.84 KB
7.14 KB
stefan.r’s picture

Status: Needs work » Needs review
FileSize
11.8 KB
3.56 KB
stefan.r’s picture

FileSize
11.88 KB

Missed a comma.

stefan.r’s picture

Status: Needs work » Needs review
stefan.r’s picture

Status: Needs work » Needs review
FileSize
11.91 KB
3.56 KB
stefan.r’s picture

Status: Needs work » Needs review

Queueing this for re-testing this to make sure this wasn't a fluke as I haven't seen the current test fail in MigrateFileTest with this patch before.

Also just to summarize for reviewers, in terms of changes to the last reviewed patch, this patch:

  • Fixes failing tests, where we remove nonexistent modules or set a custom error handler.
  • Cleans up code as per @Fabianx suggestions.
  • Sets an expiration of 24 hours on the "missing files" cache as suggested by @Fabianx. This means the cache ceases to explicitly clear during every cron run. Instead, it relies on garbage collection, as the cache will be invalidated once it's expired.

@Fabianx just to double check that this invalidation won't break anything, why did you ask to delete instead of invalidate the cache on the admin/theme pages? Won't an invalid cache mean just that when we try to do a get() the cache won't return anything (as $allow_invalid is now set to FALSE), and the cache will just be reset next time we try to do a cache_set(), or during garbage collection in cron?

Lastly, I may be wrong but I think ceasing to explicitly clean up the cache on every cron run also means that the cache will only be cleared once it's been fully built and all missing files have been found. This as any time a new missing file is found the expiration date is set further into the future. In the edge case of any code referring to a nonexisting file with a random file name, that could mean the cache never gets cleaned up. Is that something we can live with? If not, maybe we could set a fixed expiration date of 24 hours that doesn't get bumped further into the future every time the cache is updated?

stefan.r’s picture

FileSize
9.05 KB

OK this is green again.

joseph.olstad’s picture

Here's my test plan for D8:

1) install fresh with the patch
2) create a dummy module, enable it
3) remove the dummy module from the filesystem
4) check for expected results (error message)
5) if possible, performance profile

repeat same tests as above without patch

stefan.r’s picture

FileSize
11.94 KB

So I discussed this with @Fabianx on IRC and apparently the concerns in #250 won't pose a problem. The last point would just need feedback from a core committer.

Just a tiny tweak to the last patch in the attachment:

+++ b/core/modules/system/src/Tests/Entity/EntitySchemaTest.php

@@ -28,7 +28,7 @@ class EntitySchemaTest extends EntityUnitTestBase  {
    *
    * @var array
    */
-  public static $modules = array('menu_link');
+  public static $modules = array('menu_link_content');
 

I'll just delete that altogether, looks like no extra modules are needed in that test anyway.

arosboro’s picture

Patch #189 helped me identify a missing module. Profiling reports a significant decrease in function calls and a savings of 2,000,000 microseconds. (Yes I'm testing a very slow site, but this patch made an improvement none the less)

joseph.olstad’s picture

My dev box is running off of a failover drive (like a human on one kidney) so I'll delay my D8 tests until I get the replacement drive up. I did some preliminary D8 tests but was right in the middle of this when I discovered a hardware problem. Keep up the good work Stefan.r .

jeroen.b’s picture

Just tested this on a fresh install from 8.0.x-dev with the patch from #254 and steps from #253, worked great.
Got the following message in WD:
<em class="placeholder">User warning</em>: The following module is missing from the file system: dummy in <em class="placeholder">drupal_get_filename()</em> (line <em class="placeholder">300</em> of <em class="placeholder">/home/vhosts/jeroen/drupal/d8-patch/core/includes/bootstrap.inc</em>).

stefan.r’s picture

FileSize
9.3 KB

Interdiff with last RTBC'ed patch for reviewers

Wim Leers’s picture

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

Only nitpicks. I think it'd be better if e.g. Fabianx reviews this. Assigning.

  1. +++ b/core/includes/bootstrap.inc
    @@ -208,10 +208,23 @@ function config_get_config_directory($type = CONFIG_ACTIVE_DIRECTORY) {
    +  // We use drupal static for the missing records so we can test it.
    

    s/drupal static/drupal_static()/

  2. +++ b/core/includes/bootstrap.inc
    @@ -208,10 +208,23 @@ function config_get_config_directory($type = CONFIG_ACTIVE_DIRECTORY) {
    +  // Drupal static fast pattern is used as this function may be called often.
    

    s/Drupal static fast/The drupal_static_fast()/

  3. +++ b/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php
    @@ -70,7 +70,26 @@ function testDrupalGetFilename() {
    +    // Generate a non-existing module name.
    +    $non_existing_module = uniqid("", TRUE);
    

    Creative :D

  4. +++ b/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php
    @@ -70,7 +70,26 @@ function testDrupalGetFilename() {
    +    // Get the missing records static from drupal_get_filename.
    ...
    +    $this->assertTrue($missing['module'][$non_existing_module], 'Searching for an item that does not exist creates a static record in drupal_get_filename.');
    

    s/drupal_get_filename/drupal_get_filename()/

  5. +++ b/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php
    @@ -70,7 +70,26 @@ function testDrupalGetFilename() {
    +    // Searching for an item that does not exist creates a static record in drupal_get_filename.
    

    80 cols, and same remark as above.

Wim Leers’s picture

Issue tags: +D8 Accelerate Dev Days
stefan.r’s picture

Status: Needs work » Needs review
FileSize
1.51 KB
11.95 KB
Fabianx’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/system/src/Tests/Common/AttachedAssetsTest.php
    @@ -74,10 +74,20 @@ function testDefault() {
    +    // Set a custom error handler so we can ignore the file not found error.
    +    set_error_handler(function($severity, $message, $file, $line) {
    ...
         $build['#attached']['library'][] = 'unknown/unknown';
         $assets = AttachedAssets::createFromRenderArray($build);
    ...
    +    // Restore the original error handler.
    +    restore_error_handler();
    

    This should be using an existing module, but a non-existing library.

    That would be a better test anyway and the error_handler handling should be removed here.

  2. +++ b/core/modules/system/src/Tests/Entity/EntitySchemaTest.php
    @@ -24,13 +24,6 @@ class EntitySchemaTest extends EntityUnitTestBase  {
    -   */
    -  public static $modules = array('menu_link');
    -
    

    It has been a while, so pardon if I re-ask this question:

    Why do we need to remove that line again?

  3. +++ b/core/modules/taxonomy/src/Tests/TermKernelTest.php
    @@ -23,7 +23,7 @@ class TermKernelTest extends KernelTestBase {
    -  public static $modules = array( 'filter', 'taxonomy', 'taxonomy_term', 'text', 'user' );
    +  public static $modules = array( 'filter', 'taxonomy', 'text', 'user' );
    

    Why do we remove that?

    Does that module not exist?

  4. +++ b/core/modules/views/src/Tests/Plugin/RowEntityTest.php
    @@ -24,7 +24,7 @@ class RowEntityTest extends ViewUnitTestBase {
    -  public static $modules = ['taxonomy', 'text', 'filter', 'field', 'entity', 'system', 'node', 'user'];
    +  public static $modules = ['taxonomy', 'text', 'filter', 'field', 'system', 'node', 'user'];
    

    Why do we remove that?

    Does that module not exist?

CNW for #1, the rest looks good. I assume core used some non-existing modules in tests. Is that right?

stefan.r’s picture

Status: Needs work » Needs review

re #2, #3, #4: those modules don't exist, so they trigger errors.

stefan.r’s picture

FileSize
1.16 KB
11.5 KB
stefan.r’s picture

Issue summary: View changes

Updated issue summary and added beta evaluation

stefan.r’s picture

Assigned: Fabianx » Unassigned
Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Setting to RTBC, this looks great to me now and really needs more feedback by a core committer now.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/includes/bootstrap.inc
@@ -208,10 +208,23 @@ function config_get_config_directory($type = CONFIG_ACTIVE_DIRECTORY) {
+  // Return NULL right away if $type or $name is empty.
+  if (empty($type) || empty($name)) {
+    return NULL;
+  }

Should we be triggering an error here if this occurs. It strikes me that this is a bug too and that we'd want to report it.

stefan.r’s picture

I actually don't know that we need to be checking for that there, in any case it seems outside of the scope of this patch.

@joseph.olstad added it in #202 citing #2090115: Don't install a module when its default configuration has unmet dependencies as the reason but I don't really see how that would be passing empty values for those. Maybe he can clarify?

stefan.r’s picture

FileSize
551 bytes
11.16 KB

Removing this for now, unless anyone can give any examples where $type/$name would actually be empty. Will ping @joseph.olstad for feedback.

The check would probably be better dealt with in a follow-up issue, along with fixing this at the source instead if there's actually any cases of core sending odd values for those.

stefan.r’s picture

Ah perfect, seems it already triggers an error anyway, as we have a bug in ExtensionInstallStorage::getAllFolders(), where we look for a file even if drupal_get_profile() == NULL:

      if ($this->includeProfile) {
        // The install profile can override module default configuration. We do
        // this by replacing the config file path from the module/theme with the
        // install profile version if there are any duplicates.
        $profile_folders = $this->getComponentNames('profile', array(drupal_get_profile()));
        $this->folders = $profile_folders + $this->folders;
      }
stefan.r’s picture

Status: Needs work » Needs review
FileSize
12.17 KB
907 bytes
stefan.r’s picture

Status: Needs work » Needs review
FileSize
363 bytes
12.13 KB
stefan.r’s picture

Status: Needs work » Needs review
FileSize
1.23 KB
13.47 KB

The piece of code in the interdiff doesn't really makes sense to me, why do we pass the type "module" if we're looking for a profile?

stefan.r’s picture

FileSize
1.48 KB
13.77 KB

Adding comment.

stefan.r’s picture

Status: Needs work » Needs review
FileSize
2.91 KB
13.77 KB

In any case I don't think #281 is pertinent to this issue.

Opening #2476055: ConfigInstaller searches for profile overrides in module paths for that, let's stick to test failures here :)

stefan.r’s picture

Status: Needs work » Needs review
FileSize
1.24 KB
15.23 KB
stefan.r’s picture

Status: Needs work » Needs review
FileSize
4.06 KB
15.11 KB

That took a few tries, let's see if this is green now :)

Interdiff is with the patch that was RTBC'ed in #266

joseph.olstad’s picture

@stefan.r
regarding comments #274, #273 and #272 and #202

I saw you made the spoken changes in #274, looks good to me. My refactoring in #202 kept a regression from #186 because of the flux upstream it was difficult to figure out what part of 186 should remain and what part should be refactored from core commit #40cc6312 for issue #2090115. I imagine that there's been other changes to the d8 dev branch since. Looks like that code snippet spoken of in comment #272 and #273 came in during patch #186 by jeroen.b #124 (see next comment) which may have actually come from another older core commit.

I see #289 passed testing, great work Stefan.r! Stefan.r I know you've done already a lot of work on this but can you please summarize the current status of #289 so everyone else can focus on what is the current status for the D8 patch? I've got a few ideas for test plans but just want to double confirm so that we're all on the same page.

jeroen.b’s picture

Indeed great work @stefan.r!
I think the code snippet from #272 was added in #124, not in #186.

stefan.r’s picture

The patch in 266 was RTBC and the concern was this NULL check, which I am fine with removing. It exposed a few test fails, which have been fixed in the latest patch.

As far as I'm concerned this is ready. Someone just needs to review the interdiff in #289 to make sure the test fail fixes are correct.

jeroen.b’s picture

I think it's good that the empty check was removed.
Returning null on an empty $name or $type is rather a workaround than a bugfix and such errors should be fixed at the source. (like you did)

Can't really tell if your fixes from the interdiff are correct so someone else should look at that.

stefan.r’s picture

Thanks for pointing out that the NULL check came from #124. That confirms it was just about a couple of test fails and should be safe to remove at this point.

@joseph.olstad I wouldn't worry too much about profiling this, the performance penalty is obvious at this point, the current patch doesn't differ functionality-wise from the previous RTBC patch (which had been profiled already).

alexpott’s picture

+++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php
@@ -157,17 +157,23 @@ public function installDefaultConfig($type, $name) {
   public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) {
+    $profile = $this->drupalGetProfile();
     if (!$storage) {
       // Search the install profile's optional configuration too.
       $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE);
       // The extension install storage ensures that overrides are used.
       $profile_storage = NULL;
     }
-    else {
+    elseif (isset($profile)) {
       // Creates a profile storage to search for overrides.
-      $profile_install_path = $this->drupalGetPath('module', $this->drupalGetProfile()) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
+      $profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
       $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
     }
+    else {
+      // Profile has not been set yet. For example during the first steps of the
+      // installer or during unit tests.
+      $profile_storage = NULL;
+    }

These changes surprise me - which tests fail for this?

stefan.r’s picture

@alexpott: removing the NULL check causes 799 exceptions altogether now that we have the trigger_error warning: https://qa.drupal.org/pifr/test/1031513

The piece of code you isolated (ConfigInstaller::installOptionalConfig()), makes about all of the tests listed in this other test run fail (other than the QuickEdit and CKEditor tests, which have separate fixes):
https://qa.drupal.org/pifr/test/1031653

stefan.r’s picture

Just to reiterate, we just need someone to review the interdiff in #289 in order to move this issue forward now :)

It's just a few test fixes to make sure we don't do unnecessary calls to drupal_get_filename() if we haven't set a theme/profile yet.

jeroen.b’s picture

Status: Needs review » Reviewed & tested by the community

Reviewed the fixes in the interdiff, looks good to me.
As the rest of the patch was RTBC before... RTBC for me.

catch’s picture

Status: Reviewed & tested by the community » Needs review

Hmm like David in #204 I'd also have just gone for the trigger_error() here. This is adding complexity for what is a very rare case, and the trigger_error() should mean the problem is found and fixed quickly.

jeroen.b’s picture

Status: Needs review » Reviewed & tested by the community

But we did go for the trigger_error():

trigger_error(SafeMarkup::format('The following @type is missing from the file system: @name', array('@type' => $type, '@name' => $name)), E_USER_WARNING);

catch’s picture

Status: Reviewed & tested by the community » Needs review

Yes, I'd have only done that.

stefan.r’s picture

@catch in #204 David Rothstein was suggesting to do the trigger_error on top of the caching, not just to "replace" it. (seems we may have misread, see @alexpott's comment in #218)

In D7 a missing/moved module is not such a rare case at all (hence the 92 followers on this issue). A trigger_error() by itself will mean the problem is found/fixed quickly on dev environments, but not so much on all the production environments out there, hence why we need the caching.

stefan.r’s picture

Just to add to this, I've actually run into this issue on at least 4 different Drupal installs. And version 7.34 may have introduced this issue in further installs per #2380361: Latest tweaks to bootstrap.inc can make Drupal sluggish when there is code that tries to load a file that doesn't exist. I'd be fine with a simple trigger_error() if we didn't do a filesystem scan when a module is missing, but this is causing a serious performance hit where I strongly doubt a simple trigger_error will make even half of sites with missing files clean up their stuff.

I think around #206 we had a similar argument about how in the real world typo's happen, bureaucracy exists, not everyone watches their logs, and not all sites are being actively developed anymore?

joseph.olstad’s picture

@stefan.r ya, my organisation has invested several million dollars into Drupal (three years straight of well paid Drupal consultants, consulting firms, several well paid full time staff dedicated to Drupal, infrastructure, management, support) and every single one of our sites was afflicted by "Drupal sluggish when there is code that tries to load a file that doesn't exist. ". We have sites that have relatively high loads and we need to squeeze every bit of performance we can out of Drupal. Had this performance fix already been in core we might have been able to get many millions more of business for Drupal. Thanks to Mikeytown2s' work we were able to bring in a lot of performance enhancements in the form of drupal core patches , it'd be nice to see this stuff already in core because performance becomes very important once your site scales up and you have higher loads and more and more complexity. Complexity is something we cannot avoid in this case. Either way it's going to be complicated, either we maintain core patches or maintain core it'self. We greatly appreciate your hard work stefan.r (and jereon.b and others here as well as mikeytown2 ), you've helped us with contrib modules, and now the great work here, keep up the great work. It is appreciated.

catch’s picture

Status: Needs review » Needs work

The fact there are existing production sites running 7.x where this is a problem and it silently does the scanning is a decent reason to do the caching in 7.x - on the assumption that some sites get a core update but no other monitoring.

However there are no such sites for 8.x, and I can't see a situation where you'd go from a working site to this issue, and the trigger_error() wouldn't be sufficient warning (unless you're randomly deleting folders on prod). So please upload an 8.x patch with just the trigger_error(), the backport can have the additional safeguards (assuming David's fine with that).

Fabianx’s picture

I talked with catch about this.

The outcome was:

For D7 especially the caching makes sense, it does make less sense for D8 as there are no production sites out there, yet.

If you delete a module from production that is still used you usually have other problems workflow, etc. wise than that the path is no longer found. Also there is no longer the option to disable a module in Drupal 8, but only to uninstall it, which you should definitely do, which also removes the entry.

And hopefully adding new modules either goes via a dev cycle so you see the errors and also in Drupal 8 that function should be way less used as it is all moving to classes.

It is great that the patch has found so many bugs in core though, which shows the usefulness of the trigger_error.

Therefore compromise:

- trigger_error for D8
- Caching + trigger_error for D7 (as we never warned before so its good to handle gracefully)

#304: In our profiling wetkit and the used stack (bean) etc. also had been performance bottle necks in itself and many issues. So the reason why you need many core performance patches is partially also because you use a rather 'inefficient' (or partially overly complex) setup to start. Also while mikeytown2's work is very very great, it is also important to note that some of it is still a little risky and e.g. for drupal.org the opposite approach (cache synchronization) instead of async-ness has been used.

Please also remember that we are a community, it is free software and we work on consensus approach. The best way to ensure getting an issue into core quickly is to work _with_ the core committers, not against them.

They usually have good reasons for their decisions and the best of the project in mind.

jeroen.b’s picture

I don't think anyone is working against the core committers here. We just didn't understand why we would leave caching out.
I see catch's point now and leaving the caching out only in D8 seems like a very good consensus to me.

Fabianx’s picture

Thought some more and I think instead of caching misses we should start for Drupal 8 to avoid the auto-healing in the first place. That also solves the performance issue as it would no longer scan the system as fallback, but just fail.

Already before we had the consensus in here once that if system misbehaves you just clear caches.

The auto-healing was meant to find moved modules, etc. without having to clear caches.

However as we have seen that approach is flawed and with class loader in Drupal 8 and caches on that front it is needed to clear caches afterwards or call /core/rebuild.php.

Note also:

The auto-healing never updates the actual cache, so in the case of a fallback system scan we do this fallback system scan again and again (without a trigger_error) until the cache is cleared finally.

So as we now finally found out this whole approach is flawed my new take (for Drupal 8) is:

- Remove the automatic system scan
- Use trigger_error instead always.

Be done with all cases.

Fabianx’s picture

Title: Avoid re-scanning module directory when a module is missing » Avoid re-scanning module directory when a filename or a module is missing

Clarified title, this is not just about missing or moved modules.

catch’s picture

Yes +1 on removing the automatic scan altogether for 8.x, that would only be useful in the case that discovery hasn't run at all yet - and those cases we could fix individually rather than relying on multiple layers of fallback.

stefan.r’s picture

Yes this is a great solution, thanks so much Fabianx for staying on top of this.

The whole reason we need the caching magic is we have the file scan magic, happy to get rid of that for D8 :)

stefan.r’s picture

Status: Needs work » Needs review
FileSize
13.3 KB
2.7 KB

This patch removes the filesystem scan and the caching for D8.

stefan.r’s picture

Note the information returned by the module handler / saved in state may be outdated and the file might have disappeared/moved in the mean time. Are we fine with that or should we check whether the file exists in drupal_get_filename()?

catch’s picture

We use include()/include_once() in most places in 8.x now after #752730: Remove file_exists() during bootstrap so no need for file_exists() - it'll error but shouldn't be fatal.

stefan.r’s picture

FileSize
9.34 KB
3.79 KB
jeroen.b’s picture

Status: Needs work » Needs review
jeroen.b’s picture

Whoops, I noticed https://www.drupal.org/node/2480027 a little too late...

stefan.r’s picture

FileSize
18.96 KB
9.75 KB

This required some refactoring and hackery, patch is not final but some feedback would still be good. Just to summarize the changes:

1. We do separate ExtensionDiscovery scans in ExtensionInstallStorage::getAllFolders(), InstallStorage::getAllFolders() and <code>drupal_check_profile().
2. As we can't know what the location of the system module is before having installed the system module, we hardcode it in install_drupal(). I realize it's ugly but this ought to be a stop-gap solution before we have a proper extension listing service.
3. We do a system_rebuild_module_data before installing the user module in the installer so we know where it's located. This is expensive so maybe we need to use solution (1) or (2) instead here.

stefan.r’s picture

Status: Needs work » Needs review
FileSize
19.25 KB
10.04 KB
stefan.r’s picture

Status: Needs work » Needs review
FileSize
20.81 KB

KernelTestBase::enableModules() has a similar problem.

As a workaround, just to see if I can get the patch green I'll do another ExtensionDiscovery scan there :/

stefan.r’s picture

Still a few more failures to fix here:

Drupal\system\Tests\Bootstrap\GetFilenameUnitTest              3 passes   4 fails   4 exceptions
Drupal\config\Tests\ConfigDependencyTest                      79 passes             2 exceptions

Fatal error: Call to a member function getPath() on a non-object in /var/lib/drupaltestbot/sites/default/files/checkout/core/modules/simpletest/src/KernelTestBase.php on line 497
FATAL Drupal\system\Tests\Extension\ModuleHandlerTest: test runner returned a non-zero error code (255).

Drupal\system\Tests\Extension\ThemeHandlerTest                84 passes   4 fails   2 exceptions
Drupal\system\Tests\File\RemoteFileScanDirectoryTest          18 passes  17 fails  16 exceptions
Drupal\system\Tests\File\ScanDirectoryTest                    18 passes  17 fails  16 exceptions
Drupal\system\Tests\Installer\InstallerLanguageTest            1 passes   3 fails   1 exceptions

Fatal error: Call to undefined function Drupal\system\Tests\Path\system_schema() in /var/lib/drupaltestbot/sites/default/files/checkout/core/modules/system/src/Tests/Path/UrlAliasFixtures.php on line 84
FATAL Drupal\system\Tests\Path\AliasTest: test runner returned a non-zero error code (255).
stefan.r’s picture

Status: Needs work » Needs review
FileSize
26.92 KB
6.04 KB
stefan.r’s picture

FileSize
22.99 KB
27.79 KB

This should be green now. Would be good to have some feedback on the attached interdiff with #312, and ideas on what to do with the GetFilenameUnitTest test now that we don't have the filescan anymore.

stefan.r’s picture

Status: Needs work » Needs review
stefan.r’s picture

Now that drupal_get_filename doesn't have the file scan anymore we'll need to change the testing strategy in the unit test for that function a bit (GetFileNameUnitTest). Just to describe the current logic:

  1. The path for the "core extension" is hardcoded
  2. Profiles are falsely exposed as modules
  3. We look in the static cache for any previously set values. These may have been primed elsewhere in core (ie. at the bottom of system_list())
  4. If nothing found and it's a module, get the path from ModuleHandler
  5. If still unknown, get from list in state prepared by system_rebuild_module_data and ThemeHandler::rebuildThemeData().
  6. Do a file scan (which this patch removes).

As to where values are saved:

  • The files state data for modules as well as the current profile is saved in system_rebuild_module_data()
  • The files state data for themes is saved in ThemHandler::rebuildThemeData()
  • The ModuleHandler list is set up by DrupalKernel from the the container.modules parameter.

In the current implementation, we can't scan for theme engines anymore as there's no listing available for them, so we can either build a way to retrieve a cached list for them, we do a file scan for or we drop support for them in drupal_get_filename() (which would be an API break).

The ugliness of this patch in a way exposes the need for an extension listing service, there was talk about this with some core committers on IRC but "this is such a tangled mess that it will confuse developers and may introduce bugs outside of core" and "this might possibly improve performance as we do less unnecessary rebuilds" are not really strong enough arguments here, so it's currently looking like a bit of a long shot to get the extension listing as envisioned by @sun in #2208429: Extension System, Part III: ExtensionList, ModuleExtensionList and ProfileExtensionList into the beta.

stefan.r’s picture

FileSize
32.27 KB
6.09 KB

This addresses the comments in #333 by saving theme engines into state in the theme handler and scanning profiles before modules in _system_module_rebuild_info(), as well as various changes to GetFileNameUnitTest so it can deal with the lack of a file scan.

If this is green this just needs some feedback now from someone familiar with the extension system.

joseph.olstad’s picture

Head of D8 is broken, I couldn't run my test plan even when I did a hard reset to HEAD with no patches.

Modules aren't installing and cannot run a cache-rebuild without an error:

drush cache-rebuild

PHP Fatal error:  Class name must be a valid object or a string in 
drupal/core/modules/field/src/FieldStorageConfigStorage.php on line 160

This isn't a problem with your patch, but a problem with HEAD of D8 dev.
I also tried Beta 10 and it also had similar problems.

*EDIT*
I reinstalled again from D8 head, without patching , but was still unable to install a module according to my test plan. @stefan.r do you have a test module you can share that works with recent commits of D8 ?

I'm testing using the migrate_plus module that was recently updated in April to work with D8 but it's not installing.

drush en migrate_plus
PHP Fatal error: Class 'Drupal\taxonomy\Plugin\Field\FieldType\TaxonomyTermReferenceItem' not found in drupal/core/modules/field/src/FieldStorageConfigStorage.php on line 160

also, I see that the test failed for the patch, what commit were you developing from? it seems the rapid flux of D8 dev is putting a curv ball on development of this patch.
*END EDIT*

**DOUBLE EDIT**

retesting, double checking config

**END DOUBLE EDIT

** triple EDIT **
ok, configuratoin issue resolved, retesting
** end triple EDIT **

stefan.r’s picture

HEAD is fine according to https://qa.drupal.org/8.0.x-status, I think this is a testbot issue so I've queued the patch for re-testing.

joseph.olstad’s picture

ok, I applied your patch after installing HEAD
, should also test installing HEAD with patch (haven't yet)
here's what I noticed:

installed migrate_plus (some module from github)
removed it from the filesystem
ran a cache-rebuild
didn't notice anything

then ran drush updatedb

rm migrate_plus -rf
[joseph@srv1 contrib]$ drush cache-rebuild
Cache rebuild complete.                                                                                                                                                      [ok]
[joseph@srv1 contrib]$ drush updatedb
The following module is missing from the file system: migrate_plus bootstrap.inc:254                                                                                         [warning]
PHP Fatal error:  Call to a member function getPath() on a non-object in drupal/core/lib/Drupal/Core/Config/InstallStorage.php on line 226

Fatal error: Call to a member function getPath() on a non-object in drupal/core/lib/Drupal/Core/Config/InstallStorage.php on line 226
Drush command terminated abnormally due to an unrecoverable error.                                                                                                           [error]
Error: Call to a member function getPath() on a non-object in drupal/core/lib/Drupal/Core/Config/InstallStorage.php, line 226

I expected the message "The following module is missing from the file system: migrate_plus"

This is good

as for the getPath error:

related to these lines changed in InstallStorage.php

-  protected function getComponentFolder($type, $name) {
-    return drupal_get_path($type, $name) . '/' . $this->getCollectionDirectory();
+  protected function getComponentFolder(Extension $extension) {
+    return $extension->getPath() . '/' . $this->getCollectionDirectory();

Here`s where the problem is, however I'll let @stefan.r weigh in on this.

stefan.r’s picture

Yes, that's to be expected if the file is missing. It's an ugly errror that we'll need to fix :)

But the patch is green now, so before digging into details too much ideally I would first like some high level feedback from someone familiar with the extension system as to whether this is the right approach at all.

joseph.olstad’s picture

ok so that is expected behaviour?

what also happens now is the site no longer is functional. Trying to use the UI interface gives this error:

LogicException: A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break. in Drupal\Core\Render\Renderer->doRender() (line 393 of core/lib/Drupal/Core/Render/Renderer.php).

*edit*
I guess paralysing the site is one way of dealing with missing files. Definately a change from D7.*edit*

joseph.olstad’s picture

If we could show an error to the interface (web site error message/warning) saying the same message that comes up in drush then that'd be more helpful.

So example: go to the site and you'd see something like: "error: The following module is missing from the file system: migrate_plus bootstrap.inc:254" then maybe the end user site admin might know what caused it.

I was sort of expecting what D7 would behave but yes things change, D7 was pretty forgiving when it came to moving modules around and having things appear and disappear. I understand however that there may be really good reasons for making changes in D8 and that the D8 core maintainers are busy with that.

stefan.r’s picture

FileSize
32.67 KB
4.29 KB

@joseph.olstad this should fix your issue.

Not saying it will fix your broken site (which may need a cache rebuild or similar), but it should prevent the getPath() error from appearing and breaking your site in the first place :)

joseph.olstad’s picture

ok, patch 344 fixed the broken site problem. I'll do a few more tests maybe in a few days. Got some young sick kids to take care of, signing out.

joseph.olstad’s picture

Status: Needs work » Needs review

lets trigger the test bot

stefan.r’s picture

FileSize
3.76 KB
32.53 KB

nitpicks

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Config/InstallStorage.php
    @@ -196,18 +197,45 @@ public function getComponentNames($type, array $list) {
    +  public function getCoreNames() {
    

    I think we should document why we can't use the extension discovery

  2. +++ b/core/lib/Drupal/Core/Config/InstallStorage.php
    @@ -196,18 +197,45 @@ public function getComponentNames($type, array $list) {
    +      $files = new \GlobIterator(DRUPAL_ROOT . '/' . $directory . '/*' . $extension);
    

    Can't we at least use \Drupal::root() at some point here?

  3. +++ b/core/modules/simpletest/src/KernelTestBase.php
    @@ -483,6 +484,10 @@ protected function installEntitySchema($entity_type_id) {
    +    $listing = new ExtensionDiscovery(DRUPAL_ROOT);
    +    $module_list = $listing->scan('module');
    +    // In ModuleHandlerTest we pass in a profile as if it were a module.
    +    $module_list += $listing->scan('profile');
    
    @@ -492,7 +497,7 @@ protected function enableModules(array $modules) {
    -      $module_handler->addModule($module, drupal_get_path('module', $module));
    +      $module_handler->addModule($module, $module_list[$module]->getPath());
    

    This is kinda odd ... why can't we rely on drupal_get_path() here? Every new introduction of ExtensionDiscovery feels kinda wrong in this patch.

  4. +++ b/core/modules/system/system.module
    @@ -883,14 +883,19 @@ function system_get_info($type, $name = NULL) {
    +    // Prime drupal_get_filename() with the profile info file location.
    +    drupal_get_filename('profile', $profile, $profiles[$profile]->getPathname());
    

    Mh, so we couple those two methods together? Not sure whether this is really the right thing to do regarding stability.

stefan.r’s picture

@dawehner thanks, as to 3 and 4: I agree they are ugly as mentioned in #321/ #325, but we can't rely on drupal_get_path() there. #333 explains why; basically because we don't do a file scan anymore.

So we need to get their path some other way. Do you have any idea what to do there instead? I asked @catch for advice and he suggested priming the drupal_get_filename() cache, which we can only do if we actually know about the location, so for now I only saw a couple of ExtensionDiscoverys as a realistic way out without making too many changes elsewhere. Longer term we'd probably want to get rid of those though...

For instance in 3, as drupal_get_path() only works for the current profile, and in ModuleHandlerTest we pass on another profile to that function, we don't have any way to allow for that without making changes elsewhere.

As to 4, do you have any suggestion as to what to do there instead? The problem is, when we do a module scan we have to know about the profile location as it calls drupal_get_path({profile}) in ExtensionDiscovery::scan()/setProfileDirectoriesFromSettings().

Also keep in mind, everywhere we do workarounds like these now, we used to do a file scan anyway :)

stefan.r’s picture

FileSize
3.98 KB
33.3 KB

This replaces the DRUPAL_ROOT usage in this patch by \Drupal::root() and documents the concern in point number 1 from @dawehner's comment.

As to the different workarounds used, I don't really see any way around those without significant re-work elsewhere -- which I would be all for, but for now we may have to live with this, unless core committers change their minds when they see to what extent we need workarounds when we get rid of the file scan.

I'll try to document the individual instances for every single workaround, but just to summarize for now, the reasoning used was:

a. If we know the physical location of the file, if possible skip the drupal_get_path() call altogether and just hardcode the location. Otherwise, if we can't get around the drupal_get_path() call, just prime the drupal_get_filename() static cache.
b. If the physical location of the file is unknown and system module is available, do a module rebuild.
c. If the physical location of the file is unknown and the system module is not available or we are in the middle of a rebuild, perform an ExtensionDiscovery scan.

stefan.r’s picture

Just to document all the individual changes that the removal of a file scan and the inability to get an extension listing required:

1. drupal_check_profile() @ core/includes/install.inc:
Performs an ExtensionDiscovery as this function is only called in the installer, when the system module is still unavailable and we don't yet know where all the modules are located. This way we prevent a call to drupal_get_filename() in module_load_install which would have needed a file scan.

2. install_begin_request() @ core/includes/install.core.inc:
Before installing the system module, primes the drupal_get_filename() static cache with the location of the system module as we know its exact location.

3. install_begin_request() @ core/includes/install.core.inc:
Before installing the user module, performs a module rebuild as the system module is now available. Before installing the user module, primes the drupal_get_filename() static cache with the location of the user module as we know its exact location.

4. InstallStorage/ExtensionInstallStorage::getAllFolders():
Performs an ExtensionDiscovery scan on themes/modules/profiles instead of relying on a drupal_get_filename() file scan in ExtensionDiscovery::getComponentFolder(), as this method is called during install where the system module is not yet available. This had been marked as a @todo anyway: "// @todo Refactor getComponentNames() to use the extension list directly." Note this also required a small change in TestInstallStorage.

5. system_get_info() @ system.module:
The order of the ExtensionDiscovery scans changes: we have to scan for profiles before being able to scan for modules, as a module scan requires knowing where the active profile is located (which we don't always do here). We save the result of this scan into the drupal_get_filename() static cache so we can use drupal_get_path() in ExtensionDiscovery::setProfileDirectoriesFromSettings()

As to the changes in tests:

- ConfigDependencyTest::testConfigEntityUninstall():
Performs a module rebuild as the system module is now available.

- KernelTestBase::enableModules():
Performs an ExtensionDiscovery scan as this function receives a profile that is not the current profile. We don't yet have a way to receive inactive profile information in an uncached way.

- GetFilenameUnitTest::testDrupalGetFilename():
Performs a module rebuild as the system module is available.

- ModuleHandlerTest::setUp():
Performs a module rebuild as the system module is available. Hardcodes the location of the testing and minimal profiles as these are known and shouldn't change.

- ThemeHandlerTest::testThemeInfoAlter():
Performs a module rebuild as we are looking to install a new module while the drupal_get_filename state data is still unavailable.

- ScanDirectoryTest::setUp()/testOptionKey() and InstallerLanguageTest::testInstallerTranslationFiles():
Hardcodes the location of the simpletest files directory as it is already known and shouldn't change.

- UrlAliasFixtures::sampleUrlAliases():
Primes the drupal_get_filename static cache as we are looking to install the system module here and its location is known and shouldn't change.

Tangentially related:

- ThemeHandler::rebuildThemeData():
Now caches theme engine data as well, as otherwise drupal_get_filename has no way to retrieve this information without a file scan.

stefan.r’s picture

FileSize
35.14 KB
35.14 KB

This documents the individual workarounds in code as well.

stefan.r’s picture

FileSize
8.08 KB
catch’s picture

+++ b/core/includes/install.core.inc
@@ -1029,6 +1034,9 @@ function install_base_system(&$install_state) {
+  system_rebuild_module_data();

This could hard-code the user module location too - would save the full rebuild.

stefan.r’s picture

I pinged @catch for feedback and in terms of the overall approach this should be OK as all the workarounds are far from the critical path and there's only 5 of them in the actual code. Most are in tests.

A lot of this will need to be removed as part of the refactoring suggested in #2186491: [meta] D8 Extension System: Discovery/Listing/Info, so just to keep track of this we've settled on adding a @todo in all the affected spots, as opposed to adding a separate method.

I'm including an interdiff with the previously RTBC'ed patch in #289 for easier reviewing.

stefan.r’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
652 bytes
36.41 KB

Updating issue summary and removing a typo that I accidentally introduced in the previous patch.

stefan.r’s picture

FileSize
36.43 KB
806 bytes

As to installing the user module in the installer, let's hardcode its location as well as @catch had suggested in #354.

Previously I had thought we could do the module rebuild there and could skip the other rebuild in the installer that was happening anyway, but this is less of a change in the logic and consistent with what we do with the system module.

stefan.r’s picture

Hmm, this test failure exposes the fact that any ExtensionDiscovery::scan() for a list of modules needs to be able to get the profile path from drupal_get_filename(), which is a problem if we haven't ever run system_module_rebuild().

We've already implemented a workaround for this in this patch in _system_rebuild_module_data(), and @dawehner indicated he didn't much like the coupling with drupal_get_path() that this introduces.

I see three ways out here:

a) Do the same workaround as in _system_rebuild_module_data() in ExtensionInstallStorage::getAllFolders() -- ie. do a profile scan before a module scan and save the location of the profile into the drupal_get_filename() static cache. Again, this introduces the same coupling @dawehner was worried about, but we're hopefully removing all this for #2186491: [meta] D8 Extension System: Discovery/Listing/Info anyway.

b) Keep the module rebuild right after installing the system module. The next module rebuild is in install_profile_modules() -- this data is only statically cached and we are looking for dependencies (which are not cached anywhere), so as this happens in another request we make the installer some 200 milliseconds slower :/

c) Refactor ExtensionDiscovery::scan() to lose the drupal_get_filename dependency -- maybe doing a recursive call to itself to figure out the location of the profile directory. This is a speed decrease on every module scan though, so probably the worst option out of the three. However it would remove the need for the workaround in _system_rebuild_module_data(), which could just remain as is.

As a) introduces the smallest performance hit, likely that's the "least worst" option?

stefan.r’s picture

Status: Needs work » Needs review
FileSize
36.97 KB
36.97 KB

This is what #359.a) would look like...

stefan.r’s picture

FileSize
2.16 KB
stefan.r’s picture

Just FYI, I am finished working on this and this patch just needs further review now on the newly introduced changes since #289.

I am including with this comment an interdiff with the latest RTBC patch in #289.

Fabianx’s picture

Assigned: Unassigned » catch

Assigning to catch for review

stefan.r’s picture

Thanks, just sharing our little IRC conversation from 10 minutes ago here for everyone's information:

<stefan_r> Fabianx-screen: did you manage to have a look at the changes in the filescan issue yourself? 
...
<Fabianx-screen> stefan_r: I was mostly fine with the changes.
<Fabianx-screen> stefan_r: I think its very verbose, but that just shows how much code did depend on kinda broken behavior.
<Fabianx-screen> stefan_r: That was quite a code smell. :)
<Fabianx-screen> stefan_r: Thanks also for the great interdiff, that was helpful from last RTBC.

Also tiny nits I found on final review, for future reference after @catch reviews:

  1. +++ b/core/includes/install.core.inc
    @@ -1029,6 +1035,11 @@ function install_base_system(&$install_state) {
    +  // Prime the drupal_get_filename() static cache with the user module's
    
    +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
    @@ -134,8 +134,9 @@ public function __construct($root) {
    +    // unless explicit profile directories have been set. Exclude profiles
    +    // as we can't have profiles within profiles.
    

    needs reflowing

  2. +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php
    @@ -80,26 +81,57 @@ public function createCollection($collection) {
    +          // Prime the drupal_get_filename() static cache with the profile info file
    +          // location so we can use drupal_get_path() on the active profile during
    +          // the module scan.
    

    80 cols

  3. +++ b/core/modules/config/src/Tests/ConfigDependencyTest.php
    @@ -184,6 +184,10 @@ public function testConfigEntityUninstall() {
    +    // Perform a module rebuild so we can know where the node module is located.
    +    // and uninstall it.
    

    stray dot

  4. +++ b/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php
    @@ -184,6 +197,11 @@ function testUninstallProfileDependency() {
    +    drupal_get_filename('profile', 'minimal', 'core/profiles/' . $profile . '/' . $profile . '.info.yml');
    
    +++ b/core/modules/system/src/Tests/File/ScanDirectoryTest.php
    @@ -28,7 +28,11 @@ class ScanDirectoryTest extends FileTestBase {
    +    // location from drupal_get_filename() in a cached way.
    

    s/'minimal'/$profile/

catch’s picture

Assigned: catch » Unassigned

Didn't do a thorough review but I think the changes here are OK as well. It's verbose but the trade-off vs supporting this behaviour on all requests is worth it - and the @todos mean we have a decent chance to clean things up when the listing/info issue goes in.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Then lets RTBC this.

stefan.r’s picture

FileSize
36.97 KB
3.13 KB

This adds the spelling corrections I had mentioned earlier

Fabianx’s picture

+++ b/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php
@@ -201,7 +201,7 @@
-    drupal_get_filename('profile', 'minimal', 'core/profiles/' . $profile . '/' . $profile . '.info.yml');
+    drupal_get_filename('profile', $profile, 'core/profiles/' . $profile . '/' . $profile . '.info.yml');

This change was what you wanted?

And the comment above should still speak of minimal?

stefan.r’s picture

Yes, that was merely for code readability. The profile is hardcoded to minimal a line above this all, if it gets changed, people can update the comment.

Tests will be red if this is wrong :)

stefan.r’s picture

Issue summary: View changes
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.0.x, thanks!

  • catch committed b5f7ccc on 8.0.x
    Issue #1081266 by stefan.r, mikeytown2, jeroen.b, tsphethean, mfb,...
David_Rothstein’s picture

Version: 8.0.x-dev » 7.x-dev
Status: Fixed » Patch (to be ported)
Issue tags: +Needs backport to D7

Pretty sure there was an intention to backport at least some part of this (whether the trigger_error or the trigger_error + cache or something else).

jeroen.b’s picture

Drupal 7 patch was already finished (without the trigger_error).
If we are not going to do such an extensive rewrite of drupal_get_filename in D7 we can use that as a start.

stefan.r’s picture

Status: Patch (to be ported) » Needs review
FileSize
5.22 KB
0 bytes

Thanks @jeroen.b for the D7 patch and thanks again @Fabianx and @catch for the helpful reviews.

The trigger_error in the D8 patch made a whole bunch of tests fail, let's see how this one does.

stefan.r’s picture

FileSize
6.57 KB

Status: Needs review » Needs work

The last submitted patch, 375: drupal-1081266-375-avoid-rescanning-missing-module-D7.patch, failed testing.

marcelovani’s picture

Nice work with the D8 patch.
Now, back to D7, when I go to the modules page, I get this error:

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WHERE (cid = 'drupal_get_filename:missing')' at line 1: DELETE FROM {} WHERE (cid = :db_condition_placeholder_0) ; Array ( [:db_condition_placeholder_0] => drupal_get_filename:missing ) in cache_clear_all() (line 167 of /Users/marcelo/vm/vagrant/repos/drupal/includes/cache.inc).

ps: I am testing with DB cache.

It looks like, when clearing the cache, providing a cid, the parameter bin is required. See http://cgit.drupalcode.org/drupal/tree/includes/cache.inc?h=7.x#n150

in this case, you would have to change it to cache_clear_all('drupal_get_filename:missing', 'cache'); in the two functions.

stefan.r’s picture

Status: Needs work » Needs review
FileSize
5.24 KB
2.71 KB

Status: Needs review » Needs work

The last submitted patch, 379: 1081266-379.patch, failed testing.

jeroen.b’s picture

Status: Needs work » Needs review
FileSize
5.24 KB
940 bytes

Fixed the cache_clear_all

Status: Needs review » Needs work

The last submitted patch, 381: interdiff-1081266-379-381.patch, failed testing.

jeroen.b’s picture

Status: Needs work » Needs review
FileSize
940 bytes

Interdiff in a txt now to prevent testing.

The last submitted patch, 381: 1081266-381.patch, failed testing.

jeroen.b’s picture

Trying to fix the fails.

Status: Needs review » Needs work

The last submitted patch, 385: 1081266-385.patch, failed testing.

jeroen.b’s picture

Status: Needs work » Needs review
FileSize
5.52 KB

Whoops, that was stupid.

Status: Needs review » Needs work

The last submitted patch, 387: 1081266-387.patch, failed testing.

jeroen.b’s picture

Status: Needs work » Needs review
FileSize
5.44 KB
3.14 KB

I think this is a little cleaner approach without cache or statics in the error handler.
Should have no fails. I wonder where all the "The following module is missing from the file system: default." are coming from though.

Also included an interdiff with #379.

Status: Needs review » Needs work

The last submitted patch, 389: 1081266-389.patch, failed testing.

jeroen.b’s picture

Status: Needs work » Needs review
FileSize
5.35 KB

Rewrote one comment.
Fixed one of our own tests.

The "default" module updates are all in upgrade tests. For some reason there is a "default" record in the system table for those upgrade tests.
The "simpletest_missing_module" is from a test from simpletest to actually test for a missing module.
Any suggestions to solve that?

Status: Needs review » Needs work

The last submitted patch, 391: 1081266-391.patch, failed testing.

marcelovani’s picture

I don't see the "default" but I do see this
"The following module is missing from the file system: ."

marcelovani’s picture

That is related to $name being passed as empty by other contrib/core modules
We already spoke about this here https://www.drupal.org/node/2383823

jeroen.b’s picture

Can you try to figure out what module is doing that and open an issue for that project?
Current status of the patch is that it only fails in tests because:

  1. There is 1 test that actually wants a module to be missing
  2. For some reason the upgrade db seeds contain a "default" module in the system table

Point 1 I'm not really sure how to fix that without removing the test.
Point 2 I'm not really confident that I know why the db seed contains a "default" module.

Also attached a patch in which it's easier to find the causing module for the trigger_error because it adds an debug_backtrace to the trigger_error.

jeroen.b’s picture

Dicovered something, this seems really wrong, this is from the system table in the upgrade DB seed:

[filename] =&gt; profiles/default/default.profile
        [name] =&gt; default
        [type] =&gt; module
        [owner] =&gt; 
        [status] =&gt; 0
        [bootstrap] =&gt; 0
        [schema_version] =&gt; 0
        [weight] =&gt; 1000
        [info] =&gt;
    a:6:{s:12:"dependencies";a:0:{}s:11:"description";s:0:"";s:7:"package";s:5:"Other";s:7:"version";N;s:3:"php";s:5:"5.2.4";s:5:"files";a:0:{}}
jeroen.b’s picture

Status: Needs work » Needs review
FileSize
5.86 KB
473 bytes

I think I have a fix for the missing "default" module. In Drupal 7 it has been renamed to "standard".
This gets updated in a hook_update_n() of the system module but obviously this code runs before running the updates.

Status: Needs review » Needs work

The last submitted patch, 397: 1081266-397.patch, failed testing.

jeroen.b’s picture

Status: Needs work » Needs review
FileSize
865 bytes
5.91 KB

This should fix all the upgrade path tests. Only one thats left now are the lookups for simpletest_missing_module.

Status: Needs review » Needs work

The last submitted patch, 399: 1081266-399.patch, failed testing.

jeroen.b’s picture

Indeed, only the simpletest_missing_module left now. It's caused by modules/simpletest/simpletest.test:

/**
 * Test required modules for tests.
 */
class SimpleTestMissingDependentModuleUnitTest extends DrupalUnitTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Testing dependent module test',
      'description' => 'This test should not load since it requires a module that is not found.',
      'group' => 'SimpleTest',
      'dependencies' => array('simpletest_missing_module'),
    );
  }

  /**
   * Ensure that this test will not be loaded despite its dependency.
   */
  function testFail() {
    $this->fail(t('Running test with missing required module.'));
  }
}
joshtaylor’s picture

Running just this, and no contrib modules, trying to do a test run on any core module gives me the following:

PHP Warning:  The following module is missing from the file system: standard in core/includes/bootstrap.inc on line 254
PHP Stack trace:
PHP   1. {main}() core/scripts/run-tests.sh:0
PHP   2. Drupal\Core\DrupalKernel->prepareLegacyRequest() core/scripts/run-tests.sh:39
PHP   3. Drupal\Core\DrupalKernel->preHandle() core/lib/Drupal/Core/DrupalKernel.php:611
PHP   4. Drupal\Core\Config\ConfigFactory->get() core/lib/Drupal/Core/DrupalKernel.php:510
PHP   5. Drupal\Core\Config\ConfigFactory->doGet() core/lib/Drupal/Core/Config/ConfigFactory.php:94
PHP   6. Drupal\Core\Config\ConfigFactory->doLoadMultiple() core/lib/Drupal/Core/Config/ConfigFactory.php:109
PHP   7. Drupal\Core\Config\FileStorage->readMultiple() core/lib/Drupal/Core/Config/ConfigFactory.php:166
PHP   8. Drupal\Core\Config\FileStorage->read() core/lib/Drupal/Core/Config/FileStorage.php:118
PHP   9. Drupal\Core\Config\InstallStorage->exists() core/lib/Drupal/Core/Config/FileStorage.php:96
PHP  10. Drupal\Core\Config\InstallStorage->getAllFolders() core/lib/Drupal/Core/Config/InstallStorage.php:103
PHP  11. Drupal\Core\Extension\ExtensionDiscovery->scan() core/lib/Drupal/Core/Config/InstallStorage.php:173
PHP  12. Drupal\Core\Extension\ExtensionDiscovery->setProfileDirectoriesFromSettings() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:140
PHP  13. drupal_get_path() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:219
PHP  14. drupal_get_filename() core/includes/bootstrap.inc:277
PHP  15. trigger_error() core/includes/bootstrap.inc:254

Warning: The following module is missing from the file system: standard in core/includes/bootstrap.inc on line 254

Call Stack:
    0.0012     443952   1. {main}() core/scripts/run-tests.sh:0
    0.0087    2021584   2. Drupal\Core\DrupalKernel->prepareLegacyRequest() core/scripts/run-tests.sh:39
    0.3772   14023512   3. Drupal\Core\DrupalKernel->preHandle() core/lib/Drupal/Core/DrupalKernel.php:611
    0.3863   15337176   4. Drupal\Core\Config\ConfigFactory->get() core/lib/Drupal/Core/DrupalKernel.php:510
    0.3863   15337352   5. Drupal\Core\Config\ConfigFactory->doGet() core/lib/Drupal/Core/Config/ConfigFactory.php:94
    0.3863   15337976   6. Drupal\Core\Config\ConfigFactory->doLoadMultiple() core/lib/Drupal/Core/Config/ConfigFactory.php:109
    0.3863   15338512   7. Drupal\Core\Config\FileStorage->readMultiple() core/lib/Drupal/Core/Config/ConfigFactory.php:166
    0.3863   15339080   8. Drupal\Core\Config\FileStorage->read() core/lib/Drupal/Core/Config/FileStorage.php:118
    0.3863   15339120   9. Drupal\Core\Config\InstallStorage->exists() core/lib/Drupal/Core/Config/FileStorage.php:96
    0.3864   15339392  10. Drupal\Core\Config\InstallStorage->getAllFolders() core/lib/Drupal/Core/Config/InstallStorage.php:103
    0.3973   15364360  11. Drupal\Core\Extension\ExtensionDiscovery->scan() core/lib/Drupal/Core/Config/InstallStorage.php:173
    0.3973   15364544  12. Drupal\Core\Extension\ExtensionDiscovery->setProfileDirectoriesFromSettings() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:140
    0.3973   15364728  13. drupal_get_path() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:219
    0.3973   15364728  14. drupal_get_filename() core/includes/bootstrap.inc:277
    0.3983   15483184  15. trigger_error() core/includes/bootstrap.inc:254

PHP Warning:  strpos(): Empty needle in core/lib/Drupal/Core/Extension/ExtensionDiscovery.php on line 270
PHP Stack trace:
PHP   1. {main}() core/scripts/run-tests.sh:0
PHP   2. Drupal\Core\DrupalKernel->prepareLegacyRequest() core/scripts/run-tests.sh:39
PHP   3. Drupal\Core\DrupalKernel->preHandle() core/lib/Drupal/Core/DrupalKernel.php:611
PHP   4. Drupal\Core\Config\ConfigFactory->get() core/lib/Drupal/Core/DrupalKernel.php:510
PHP   5. Drupal\Core\Config\ConfigFactory->doGet() core/lib/Drupal/Core/Config/ConfigFactory.php:94
PHP   6. Drupal\Core\Config\ConfigFactory->doLoadMultiple() core/lib/Drupal/Core/Config/ConfigFactory.php:109
PHP   7. Drupal\Core\Config\FileStorage->readMultiple() core/lib/Drupal/Core/Config/ConfigFactory.php:166
PHP   8. Drupal\Core\Config\FileStorage->read() core/lib/Drupal/Core/Config/FileStorage.php:118
PHP   9. Drupal\Core\Config\InstallStorage->exists() core/lib/Drupal/Core/Config/FileStorage.php:96
PHP  10. Drupal\Core\Config\InstallStorage->getAllFolders() core/lib/Drupal/Core/Config/InstallStorage.php:103
PHP  11. Drupal\Core\Extension\ExtensionDiscovery->scan() core/lib/Drupal/Core/Config/InstallStorage.php:173
PHP  12. Drupal\Core\Extension\ExtensionDiscovery->filterByProfileDirectories() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:190
PHP  13. array_filter() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:277
PHP  14. Drupal\Core\Extension\ExtensionDiscovery->Drupal\Core\Extension\{closure}() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:277
PHP  15. strpos() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:270

Warning: strpos(): Empty needle in core/lib/Drupal/Core/Extension/ExtensionDiscovery.php on line 270

Call Stack:
    0.0012     443952   1. {main}() core/scripts/run-tests.sh:0
    0.0087    2021584   2. Drupal\Core\DrupalKernel->prepareLegacyRequest() core/scripts/run-tests.sh:39
    0.3772   14023512   3. Drupal\Core\DrupalKernel->preHandle() core/lib/Drupal/Core/DrupalKernel.php:611
    0.3863   15337176   4. Drupal\Core\Config\ConfigFactory->get() core/lib/Drupal/Core/DrupalKernel.php:510
    0.3863   15337352   5. Drupal\Core\Config\ConfigFactory->doGet() core/lib/Drupal/Core/Config/ConfigFactory.php:94
    0.3863   15337976   6. Drupal\Core\Config\ConfigFactory->doLoadMultiple() core/lib/Drupal/Core/Config/ConfigFactory.php:109
    0.3863   15338512   7. Drupal\Core\Config\FileStorage->readMultiple() core/lib/Drupal/Core/Config/ConfigFactory.php:166
    0.3863   15339080   8. Drupal\Core\Config\FileStorage->read() core/lib/Drupal/Core/Config/FileStorage.php:118
    0.3863   15339120   9. Drupal\Core\Config\InstallStorage->exists() core/lib/Drupal/Core/Config/FileStorage.php:96
    0.3864   15339392  10. Drupal\Core\Config\InstallStorage->getAllFolders() core/lib/Drupal/Core/Config/InstallStorage.php:103
    0.3973   15364360  11. Drupal\Core\Extension\ExtensionDiscovery->scan() core/lib/Drupal/Core/Config/InstallStorage.php:173
    0.3987   15494016  12. Drupal\Core\Extension\ExtensionDiscovery->filterByProfileDirectories() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:190
    0.3987   15494424  13. array_filter() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:277
    0.3988   15504440  14. Drupal\Core\Extension\ExtensionDiscovery->Drupal\Core\Extension\{closure}() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:277
    0.3988   15504720  15. strpos() core/lib/Drupal/Core/Extension/ExtensionDiscovery.php:270

I'm on Ubuntu, I also get this on Travis with PHP 5.4/5.5/5.6 - https://travis-ci.org/joshuataylor/commerce/builds/61874609

jeroen.b’s picture

@joshtaylor D8 has already been committed, maybe it's better to open a new issue for that.

I went for the same method as the bootstrap tests. All tests should be green now. Not sure if it's the way to go though.

jeroen.b’s picture

Status: Needs work » Needs review
jeroen.b’s picture

For reference, interdiff with #373.

joshtaylor’s picture

Created a followup issue https://www.drupal.org/node/2486083

jeroen.b’s picture

Some minor tweaks and found a mistake with restoring the error handler.

jeroen.b’s picture

Updated interdiff with #373.

fgm’s picture

Status: Needs review » Needs work

New side-effect of this patch: run-scripts is broken for some configurations #2486083: The following module is missing from the file system: standard

fgm’s picture

Version: 7.x-dev » 8.0.x-dev
jeroen.b’s picture

Why did you put it back to 8.0.x-dev?
Can you please keep patches/discussions about the new errors in the new issue?

jeroen.b’s picture

Status: Needs work » Needs review

Other issue has a patch and is RTBC, so let's keep this at 7.x-dev.

jeroen.b’s picture

Version: 8.0.x-dev » 7.x-dev
stefan.r’s picture

Status: Needs review » Needs work

Well @catch and @Fabianx had already mentioned the caching+trigger_error could work for D7, and #407 is essentially equal to the previously RTBC'ed D8 patch in #289. It's green too, so I guess we're close here?

  1. +++ b/includes/update.inc
    @@ -795,6 +795,12 @@ function update_fix_d7_requirements() {
    +  // Default profile has been renamed to Standard in D7.
    +  if ($profile == 'default') {
    +    $profile = 'standard';
    +    variable_set('install_profile', $profile);
    +  }
    

    This could use better documentation -- why are we changing the profile from default to standard. Are there any possible secondary effects here?

  2. +++ b/modules/simpletest/simpletest.module
    @@ -370,11 +370,27 @@ function simpletest_test_get_all() {
    +            // Searching for an item that does not exist triggers an PHP error.
    +            // Set a custom error handler so we can ignore the file not found error.
    

    80 cols

  3. +++ b/modules/simpletest/simpletest.module
    @@ -370,11 +370,27 @@ function simpletest_test_get_all() {
    +            set_error_handler(function($error_level, $message, $filename, $line, $context) {
    
    +++ b/modules/simpletest/simpletest.module
    @@ -370,11 +370,27 @@ function simpletest_test_get_all() {
    +                // Restore the original error handler.
    +                restore_error_handler();
                     continue 2;
                   }
                 }
    +
    +            // Restore the original error handler.
    +            restore_error_handler();
    

    Maybe this should merely wrap the drupal_get_filename call, I don't see that we need to put it around the other statements and having restore_error_handler there twice looks confusing.

jeroen.b’s picture

Status: Needs work » Needs review
FileSize
2.56 KB
7.36 KB

Addresses your comments.
I think the context of update_fix_d7_install_profile() and the "@see system_update_7049()" comment are enough now, what do you think?

mikeytown2’s picture

#415 looks good! Hopefully not to many people have the error_level set to 1 or 2 on a production site with a missing module.

jeroen.b’s picture

Yeah this should definitely be mentioned in the release notes. Possibly with a link to a documentation page with the following structure:

<h2>How to fix "The following module is missing from the file system"?</h2>


<h3>You removed the module without uninstalling</h3>

<h4>Restore the module and actually uninstall it (recommended if possible)</h4>
<h4>With an update function (recommended)</h4>
<h4>Manually with SQL (in drush sql-cli or phpmyadmin or mysqladmin)</h4>

<h3>You moved the module inside your Drupal installation</h3>

<h4>With an update function (recommended)</h4>
<h4>Manually with SQL (in drush sql-cli or phpmyadmin or mysqladmin)</h4>

Anyone feels like writing it up? If not I'll see when I have some spare time.

stefan.r’s picture

Jeroen.b yes those changes in the latest patch look good. Can we link to this documentation page in the error message as well?

jeroen.b’s picture

Created a documentation page on https://www.drupal.org/node/2487215.
Here's a new patch with a mention of the documentation page in the warning.

stefan.r’s picture

  1. +++ b/includes/update.inc
    @@ -795,6 +795,14 @@ function update_fix_d7_requirements() {
     function update_fix_d7_install_profile() {
    ...
    +  // Default profile has been renamed to Standard in D7.
    

    'Default' profile has been renamed to 'Standard' in Drupal 7.

  2. +++ b/includes/bootstrap.inc
    @@ -906,6 +923,16 @@ function drupal_get_filename($type, $name, $filename = NULL) {
    +      // Add the missing file to a temporary cache and throw an alert. This cache
    

    80 cols

  3. +++ b/modules/simpletest/tests/bootstrap.test
    @@ -410,6 +410,35 @@ class BootstrapGetFilenameTestCase extends DrupalUnitTestCase {
    +    // Get the bad records static from drupal_get_filename.
    

    s/bad/missing/
    s/drupal_get_filename/drupal_get_filename()/

  4. +++ b/modules/simpletest/tests/bootstrap.test
    @@ -410,6 +410,35 @@ class BootstrapGetFilenameTestCase extends DrupalUnitTestCase {
    +    // Searching for an item that does not exist creates a static record in drupal_get_filename.
    

    80 cols
    s/drupal_get_filename/drupal_get_filename()/

Status: Needs review » Needs work

The last submitted patch, 419: 1081266-419.patch, failed testing.

Status: Needs work » Needs review

stefan.r queued 419: 1081266-419.patch for re-testing.

stefan.r’s picture

jeroen.b’s picture

Updated the comments.

stefan.r’s picture

Issue tags: -Needs manual testing

Manually tested this.

1. The warning now says:

"User warning: The following module is missing from the file system: crumbs. For more information, see the documentation page. in drupal_get_filename() (line 934 of /Applications/MAMP/htdocs/drupal7/includes/bootstrap.inc)."

That last part isn't very informative (..in drupal_get_filename()...), and the transition from "page" to "in" seems odd, ideally we'd be pointing to wherever drupal_get_path()/drupal_get_filename() was called from, though not sure we can without debug_backtrace() magic. It will also show once for every drupal_get_filename() call, maybe we should only show each type/file combination once per request?

2. We may want to remove a file from the "missing" cache if it actually reappears. Ie. we could do the cache_get if the static is not set, unset the item in the static if it reappears and do a cache_set if ($missing !== $cache->data)

3. I'm a bit worried about the 1 day cache lifetime. I think it should be unlimited as otherwise things may be broken for a day and then suddenly work again, which could lead to a lot of head-scratching :)

Maybe the worry was that if we don't expire the cache, we mark the wrong items as missing, but that is not actually a problem, we still check in the old location with a file_exists() call before we check the missing file cache. So temporarily removing a file and then putting it back would actually be fine. It's just he moving that's problematic. That would only work after 1 day if we keep the expiry. Only fixing this upon visiting the modules/themes overview pages or upon a full cache clear (as we do now) might be the better way to go?

jeroen.b’s picture

1. Yeah, but even with some backtrace magic you won't be able to pull it off. Let's say that module_load_include called it, then the message still won't help you any further because it's just another function that's called a million times. If people want to find the calling function, I think it's best if they add the debug_backtrace manually. I can't really justify any line of debug_backtrace in code.

I think it's better to always show the PHP warning. I think developers will ignore it when we don't ("it's just one time after the cache clear... whatever"), especially if we change the cache time to unlimited.

Any idea how we can make the PHP warning better?

2. Agreed. But that might be unwanted because we have to do an unnecessary cache_get.

3. Agreed.

David_Rothstein’s picture

Status: Needs review » Needs work
  1. @@ -837,6 +837,14 @@ function drupal_get_filename($type, $name, $filename = NULL) {
       // drupal_static().
       static $files = array(), $dirs = array();
     
    +  // We use drupal_static() for the missing records so we can test it.
    +  // drupal_static_fast() is used as this function may be called often.
    +  static $drupal_static_fast;
    +  if (!isset($drupal_static_fast)) {
    +    $drupal_static_fast['missing'] = &drupal_static('drupal_get_filename:missing');
    +  }
    +  $missing = &$drupal_static_fast['missing'];
    

    Any reason not to move this code down to where it's actually going to be used (i.e., the filesystem-scanning part of the function)? Then we don't call it at all in most cases, and a regular drupal_static rather than drupal_static_fast would be fine.

  2. +        $cache = cache_get('drupal_get_filename:missing');
    

    I suspect we need some protections around this, since this could run during the early bootstrap (and looking at how other calls from inside this function are handled, they have special protections). For example, consider what happens if cache_get() is undefined, or if it throws an exception because the database isn't available yet.

  3. +    trigger_error(t('The following @type is missing from the file system: @name. For more information, see <a href="@documentation">the documentation page</a>.', array('@type' => $type, '@name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING);
    

    This probably shouldn't be translated here, because we want the admin to see it in their language (not necessarily the visitor's language) when viewing the watchdog logs - i.e. just like how the watchdog() function works.

    I guess we have no way to do that with trigger_error(), so maybe we could just not translate it at all for now and leave that for a followup issue.

  4. The documentation page itself looks like a good start and a good idea, although "You removed the module without uninstalling"... perhaps it should say something more like "You removed the module without disabling and uninstalling it"? (Although it's generally recommended to uninstall the module in addition to disabling it, I do not think this bug can be triggered unless you remove a module from the filesystem while it's still enabled, right?)
  5. +              set_error_handler(function($error_level, $message, $filename, $line, $context) {
    ....
    +    set_error_handler(function($error_level, $message, $filename, $line, $context) use (&$get_filename_test_triggered_error) {
    

    These won't work in PHP 5.2.

  6. +    // Generate a non-existing module name.
    +    $non_existing_module = uniqid("", TRUE);
    

    Is that even guaranteed to return a legal module name? Maybe it doesn't matter for this test, but I think $this->randomName() is a more standard way to do that.

  7.  function system_themes_page() {
    +  // Clean up the bootstrap "missing files" cache when listing themes.
    +  drupal_static_reset('drupal_get_filename:missing');
    +  cache_clear_all('drupal_get_filename:missing', 'cache');
    +
       // Get current list of themes.
       $themes = system_rebuild_theme_data();
       uasort($themes, 'system_sort_modules_by_info_name');
    @@ -788,6 +792,10 @@ function _system_is_incompatible(&$incompatible, $files, $file) {
      * @see system_modules_submit()
      */
     function system_modules($form, $form_state = array()) {
    +  // Clean up the bootstrap "missing modules" cache when listing modules.
    +  drupal_static_reset('drupal_get_filename:missing');
    +  cache_clear_all('drupal_get_filename:missing', 'cache');
    +
       // Get current list of modules.
       $files = system_rebuild_module_data();
    

    Is there a reason not to do this inside system_rebuild_theme_data() and system_rebuild_module_data() instead?

  8. I think I agree with #426.3 (that this should be a permanent cache). Introducing a cache here at all is a behavior change that we'll have to document either way (for anyone who might be using drupal_get_filename() in some kind of very dynamic situation) but having it magically expire after 1 day doesn't help that situation anyway.
  9. Speaking of the above, has anyone tested this patch with those kinds of unusual situations (I'm thinking of things like drush pm-update, or other Drush commands that change things in the file system while interacting with Drupal)?
stefan.r’s picture

1. Moving both the static and the database cache initialization into the if block that has the file scan in it (and making it a regular static) makes sense.

2. @jeroen.b see the protections around db_query() for an example :)

3. may have to remain untranslated

As to 7, the worry there is Drupal runs system_rebuild_theme_data and system_rebuild_module_data more often than necessary, so we wouldn't have as much of a performance improvement. On the other hand, it does seem safer...

stefan.r’s picture

#427:

1. Yes, I guess we may have to do without that then. As to when to show the warning, I meant once per module per request, not once after a cache clear. So it would still appear on every page, except we wouldn't see 10 warnings for the same module on top of every page.

As to the warning, I guess it's not a problem in the message itself. More so in the error handler, which shouldn't show the "in bootstrap.inc line xyz" part, and should translate the message. Maybe we can make a missing_file_error_handler()?

2. We can live with the cache_get() on every request if it only occurs if we have modules that would require a file scan as mentioned by @David_Rothstein

jeroen.b’s picture

I don't really have any time left this week to work on this, so if anyone feels like picking it up :)
@David_Rothstein, feel free to make some changes to the documentation.

stefan.r’s picture

Maybe in drush this should be merely a status message? Would it affect execution of a command if it's a user warning? (as it is right now)

Maybe we can have @moshe take a look as to whether this would cause unnecessary warnings?

stefan.r’s picture

Should this patch also be updated to account for when we're moving modules around (ie. from sites/all to sites/all/contrib? I think this is automatically fixed upon module rebuild anyway, but previously I think we were allowed to move modules to a different location than what was listed in the system table, at which point the file scan would find the new location. In such a case it's arguable whether the module is actually "missing"

Fabianx’s picture

Yes, moving modules is a problem, because sometimes you cannot even reliably clear caches (however that problem is pre-existing for other things). Can we add a test for that?

stefan.r’s picture

@Fabianx sounds good, however not sure how to deal with moved modules. If we cache the new locations of any moved files that's adding an extra layer of complexity.

Maybe we should just disallow that, and make people do a cache clear after moving a file instead?

stefan.r’s picture

Status: Needs work » Needs review
FileSize
8.12 KB
7.34 KB

Current patch is untested and will still break with moved files, but should at least be a step in the right direction as it addresses some of the concerns above.

stefan.r’s picture

FileSize
8.24 KB
9.13 KB
EvanSchisler’s picture

When I tried to use this patch(from comment #437), it patched with no issues. However, after clearing my caches I received these notices and warnings(none of these were raised prior to the patch):

User warning: The following module is missing from the file system: imagcache_actions. For more information, see the documentation page. in drupal_get_filename() (line 942 of /home/acro/accounts/frame2/frame2/wwwroot/includes/bootstrap.inc).
User warning: The following module is missing from the file system: imagcache_actions. For more information, see the documentation page. in drupal_get_filename() (line 942 of /home/acro/accounts/frame2/frame2/wwwroot/includes/bootstrap.inc).
Notice: Undefined index: commerce_discount_product_category_has_specified_terms in commerce_discount_product_category_rules_condition_info() (line 28 of /home/acro/accounts/frame2/frame2/wwwroot/sites/all/modules/commerce_discount_product_category/commerce_discount_product_category.rules.inc).
Notice: Undefined index: commerce_order_compare_order_amount in inline_conditions_rules_condition_info() (line 37 of /home/acro/accounts/frame2/frame2/wwwroot/sites/all/modules/inline_conditions/inline_conditions.rules.inc).
Notice: Undefined index: commerce_order_has_owner in inline_conditions_rules_condition_info() (line 59 of /home/acro/accounts/frame2/frame2/wwwroot/sites/all/modules/inline_conditions/inline_conditions.rules.inc).
Notice: Undefined index: commerce_order_contains_products in inline_conditions_rules_condition_info() (line 81 of /home/acro/accounts/frame2/frame2/wwwroot/sites/all/modules/inline_conditions/inline_conditions.rules.inc).
Notice: Undefined index: commerce_order_has_specific_quantity_products in inline_conditions_rules_condition_info() (line 115 of /home/acro/accounts/frame2/frame2/wwwroot/sites/all/modules/inline_conditions/inline_conditions.rules.inc).
Notice: Undefined index: commerce_product_contains_products in inline_conditions_rules_condition_info() (line 139 of /home/acro/accounts/frame2/frame2/wwwroot/sites/all/modules/inline_conditions/inline_conditions.rules.inc).
Notice: Undefined index: commerce_product_has_specified_terms in inline_conditions_rules_condition_info() (line 163 of /home/acro/accounts/frame2/frame2/wwwroot/sites/all/modules/inline_conditions/inline_conditions.rules.inc).

jeroen.b’s picture

@EvanSchisler
That is a bug in the Imagecache Actions module, they already fixed it.
Please update the Imagecache Actions module.

See: #2166715: Typo causes extra query every page load

This is actually a good sign, because this patch finds such bugs.

stefan.r’s picture

Any further ideas about what to do when a module is moved?

I'm thinking if the data diverges from whatever we had in cache, we should just mark the module as missing and refuse to load it until caches are cleared. It's a bit of a BC break but not a very severe one, I guess the file scan was intended mostly just in case we didn't have any cached data yet -- but if we do have (faulty) data, we should just wait for that to be rebuilt instead.

jeroen.b’s picture

Yeah, agreed. We can also extend the warning message if the record was in the cache.
Maybe something like: "If you just moved this module, try to clear the cache"?

Fabianx’s picture

There is three cases of which we cover two now:

1) Something refers to a file that can never be found (typo, etc.). => trigger_error and cache in missing to avoid further file system scans.
2) Something refers to a module that was removed from the file system since => trigger_error and cache in missing to avoid further file system scans.
3) Something refers to a module that was moved => We do a file system scan every time.

Now case 3) is interesting as while it seems to "self-heal" it does so at the cost of run-time performance.

E.g. if you move a module, it will self-heal for those cases of rename using drupal_get_filename() (but e.g. registry files won't be found), but it won't be fully healed until the module system table is rebuild.

There is 2 possibilities for us here:

a) Ignore and hope caches are cleared once in a while fully => what we have now
b) Use missing not only for negative entries, but also as a 2nd level cache to cache what we found as missing

In that case $missing[$name] => FALSE is "not found" and $missing[$name] => $filename would be "found elsewhere".

In that case a file system scan would only be done when even our missing cache would be outdated.

That likely would be the perfect solution, but not sure if its in scope or not.

( There is a reason however why we don't just write-through the database in such cases as the cache is possibly database agnostic and e.g. in a memcache or redis server. )

stefan.r’s picture

I think something like b) would be the better way to go here, I'm afraid there would be even more edge cases with odd behavior with what we have now.

stefan.r’s picture

FileSize
11.59 KB
4.04 KB

Maybe something like this could work? (please do not use in production yet as I haven't tested this yet, patch is just to illustrate)

stefan.r’s picture

I had forgotten to implement reading of the cached file scan result in that previous patch. This patch still should not be used in production until this is further tested/reviewed...

The last submitted patch, 444: 1081266-444.patch, failed testing.

stefan.r’s picture

Looking at the whole patch, it needs a bit of further work to deal with the case where the cache_get('drupal_get_filename:missing') fails/is unavailable in early bootstrap. Just resetting the missing static during the database bootstrap doesn't feel like the right solution.

There's also a couple of trailing spaces.

Status: Needs review » Needs work

The last submitted patch, 445: 1081266-445.patch, failed testing.

stefan.r’s picture

Status: Needs work » Needs review
FileSize
12.27 KB
3.06 KB
5.7 KB
Fabianx’s picture

Looks really great!

Fabianx’s picture

Issue summary: View changes

Added myself to proposed commit message

stefan.r’s picture

FileSize
15.44 KB
9.34 KB

Well-deserved, thanks again for the helpful reviews :)

Adding some documentation

stefan.r’s picture

FileSize
15.79 KB
2.23 KB

And this removes the cached "missing" record if a missing file reappeaars.

Fabianx’s picture

+++ b/includes/bootstrap.inc
@@ -986,7 +989,13 @@
+    if (isset($missing[$type][$name]) && $missing[$type][$name] === TRUE) {

Arguably its an edge case: If the module gets moved back, we don't remove it from missing cache?

stefan.r’s picture

stefan.r’s picture

FileSize
786 bytes
15.89 KB

Maybe this would work?

stefan.r’s picture

+++ b/includes/bootstrap.inc
@@ -897,15 +963,80 @@ function drupal_get_filename($type, $name, $filename = NULL) {
+          trigger_error(format_string('The following @type has moved on the file system: @name. Clearing caches may help fix this. For more information, see <a href="@documentation">the documentation page</a>.', array('@type' => $type, '@name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING);

Can we think of better verbiage here? It should probably explain the data in the system table is outdated and that that is what a cache clear might fix.

stefan.r’s picture

Just wondering about #428.8, ie. the behavior changes posed by the current patch...

a) If we remove a file altogether, we will cache that it's missing and cease to do any further file scans until a module/theme rebuild happens (such as during cache clears and when visiting admin pages). This means that once a file has been marked as missing, we can't put it back in another location anymore but we can put it back in the location logged in the system table, at which point everything will be OK again and the file will be removed from the missing cache.

b) If we move a file from wherever it was supposed to be, we will cache the result of the file scan and use that instead of a file scan from then on. If the file then moves again from its new location, we will do a new file scan, and cache the result of that. However, if we move a file back to where it was supposed to be, everything is OK again and we remove the record from the missing cache.

In the case of moving a file, the "missing" caching is only relevant as long as we haven't done a rebuild. The rebuild should log the new location into the system table.

So outside of whatever is happening in drupal_get_filename(), the only limitation/behavior change that this patch introduces is that now we can't first delete a file, and then put it back in another location (as it has been marked as missing already, so we don't do a file scan). Reactivating the file would require either putting it back in its original location first, or performing a cache clear.

Anything I'm missing here?

David_Rothstein’s picture

This is getting complicated. If a file moves, and we know what the old location and the new location are, why do we need to put that in a cache? Shouldn't we really just do something like this:

  db_update('system')
    ->fields(array('filename' => $new_filename)
    ->condition('filename', $old_filename)
    ->execute();
  system_list_reset();

Not sure if we'd want to do that inside drupal_get_filename(), but even if not we could do it at the end of the page request (like the patch is currently doing for the cache).

Maybe in drush this should be merely a status message? Would it affect execution of a command if it's a user warning? (as it is right now)

Interesting question. That plus some other reasons makes me wonder if we should go back to using watchdog() after all (rather than trigger_error()). When I originally suggested trigger_error() it was based on a couple things:

  1. That we'd only be adding this warning (no cache), i.e. that it would indicate a major ongoing performance problem with your site. But the consensus here is now to add the cache too, so this warning no longer indicates such a major problem.
  2. I was thinking mostly about #2380361: Latest tweaks to bootstrap.inc can make Drupal sluggish when there is code that tries to load a file that doesn't exist (problems in code that we want to warn a developer about in realtime). While that's still true, the fact that it's also common to get into this situation because of something the site owner did means that a regular watchdog() call might be OK.

@David_Rothstein, feel free to make some changes to the documentation.

OK, I did https://www.drupal.org/node/2487215/revisions/view/8451805/8483001 to address the disabled/uninstalled issue.

stefan.r’s picture

I agree we want to simplify the current patch as much as possible, so updating the system table at the end of the request with the moved modules that had been statically cached could potentially work, although it could cause a bit of a slowdown on subsequent requests and be more of a BC break than just doing the caching: the data in the system can now change on any regular request as opposed to just after a rebuild. The other worry is we may not catch all the moved files so this may turn out to be spread out over several requests. @Fabianx what do you think?

As to just doing a watchdog() call, the problem is it's not annoying enough for people to clean up their stuff and people can easily miss/ignore it. A moved module may not be a big deal, but a missing one is. And even with the cache, the first few requests after a missing module cache clear are still going to be very slow as we re-do the file scans.

Ideally we want to nag administrators and developers as much as possible. Maybe instead of a trigger_error() we should do a drupal_set_message() (in addition to a watchdog() call)?

Fabianx’s picture

I think the cache is actually pretty nice for such reasons.

Yes we might want to change the code to be simpler, but writing the system table with partially new information is likely not the right choice. And doing dynamic rebuilds - like menu_get_item() vs. menu_rebuild() vs. menu_rebuild_needed() has a ton of own problems ...

I still think a cache layer is a good thing to take care of what we would have done anyway (falling back).

It just means we cache the result of the fallback file system scan - which should be fine.

Maybe moving the file_system_scan to its own function would make this all clearer, and allow clearer wrapping of it?

--

drupal_set_message() for something like this is a no-go -- end users see it and that is problematic ...

stefan.r’s picture

Thinking this over a bit further I think you may be right here as far as the moved file caching is concerned.

I agree the drupal_get_filename() function is getting way to long, taking out the file system scan part would be a good start at making it clearer.

As to the status message, I was thinking we wrap the drupal_set_message() in an if statement so only admins and developers see it, but we don't really have a way of determining that do we? So it'd really have to be trigger_errror() if we want to annoy a bit more than just a watchdog message?

stefan.r’s picture

Just to clarify, other than potentially disrupting Drush, are there any further concerns with a trigger_error()? Maybe we could live with a Drupal message to users with permission 'administer site configuration' only?

David_Rothstein’s picture

I don't fully understand what's wrong with updating the system table when we know it's out of date. Essentially, that already is our cache of where known files are in the filesystem (and it already has other caches on top of it), right? Which is what makes me a little uneasy about adding a whole separate cache for the case of a module/theme that has moved somewhere else.

But perhaps moving this to a separate function as suggested above would make it cleaner and less complex.

As to the status message, I was thinking we wrap the drupal_set_message() in an if statement so only admins and developers see it, but we don't really have a way of determining that do we?

Could probably do something to check the 'error_level' variable, like error_displayable() I suppose - but yeah, it's not ideal.

Just to clarify, other than potentially disrupting Drush, are there any further concerns with a trigger_error()?

Not really - a minor concern about the lack of translatability (mentioned earlier) but it's not too bad.

And a general concern that we're going to see lots of support requests from people who suddenly see this message displaying on the screen on their production sites. They shouldn't have set their production site up to display errors to the screen in the first place, but the reality is, I bet there's a lot of overlap between sites that don't direct error messages to the logs on production and sites that delete modules from the filesystem without disabling them :)

stefan.r’s picture

Hmm the last concern seems like the more serious one. A substantial amount of installs probably doesn't have that configured right. And I guess the same concern would still apply if we say that error_level must be ERROR_REPORTING_DISPLAY_ALL (even if we recommend people not to):

  $form['error_level'] = array(
    '#type' => 'radios',
    '#title' => t('Error messages to display'),
    '#default_value' => variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL),
    '#options' => array(
      ERROR_REPORTING_HIDE => t('None'),
      ERROR_REPORTING_DISPLAY_SOME => t('Errors and warnings'),
      ERROR_REPORTING_DISPLAY_ALL => t('All messages'),
    ),
    '#description' => t('It is recommended that sites running on production environments do not display any errors.'),
  );

On the other hand, the error message is a disturbance but a minor one; as soon as the missing module is dealt with, the error disappears. And in the link in the error message we can also have a "Why do end users see this error message?" heading where we point people to wherever they can set the error_level to "Errors and warnings" or "None".

I'll let others have some input on this as I can see the arguments both for only displaying this to logged in administrators (to prevent breaking a number of misconfigured sites), but also for making it a user level notice/warning and potentially breaking a few sites, but pushing a much larger number of sites to fix their stuff

stefan.r’s picture

As to fixing the system table in real time, I'll let Fabianx chime in but my main concern was that it felt like a substantially more invasive change than just caching the file scan result.

And yes, in a way it's a cache, but it's a pretty important one compared to other caches, and updating it in real time feels like making it easier than it should be to change whatever is in there (which should be leading, and not what may be -temporarily- happening on the file system), or to only do a partial update when several files have moved. Having to wait for a rebuild means less things can go wrong in the mean time, ie. when one person passes a database dump to someone else with almost the same files installed, except this one module is duplicated in two locations on one system but only in one location on the other.

stefan.r’s picture

Which makes me think of another edge case that would be a change from current behavior, both in the case of caching the result of the file scan and in the case of changing the system record directly: a module being installed in more than one place, ie. in sites/all as well as in the profile folder.

For instance:

  1. We move a file from its original location (listed in the system table) to somewhere else, let's say from sites/all/modules to profiles/some_distribution/contrib/modules
  2. The file scan/updated system table will cache the new location
  3. We install a slightly different version of this file in another location that would have had prevalence in the file scan (profiles/some_distribution/modules?), and now have two versions of this module on our file system.

...and I could see something like this happening when people symlink a distribution for different sites but want to change a module for one site only.

Result without the patch: Drupal would have used the module in the third location as it would do a file scan every time.

Result with the patch: Drupal will use the module in the second location (as it caches the result of the file scan), and only after a module rebulid, would it go back to using the file in the third location.

Fabianx’s picture

My main concern is that the system_rebuild_module_data() would rebuild the data for the full module at once (and also take care of registry, etc. potentially if done via drush cc all), while this partial update of system table would only do for the accessed files. The system table is then out of sync.

It would be a different story if we rebuild the system table for this whole module / directory. (if that is possible)

==

Error messages:

Yes, trigger_error is potentially problematic, but given a good message and a warning in the release notes, that should be okay.

==

One module at two locations at once:

- The behavior has always been undefined, therefore we can change the behavior without problems.

stefan.r’s picture

And scheduling a complete rebuild for the next request, such as variable_set('system_rebuild_needed', TRUE) (as opposed to just system_list_reset()) is also going to be problematic I guess, the rebuild can take quite a while...

David_Rothstein’s picture

There is only one row per module in the system table, so I still don't fully understand most of that concern. For each module, either we know the correct location or we don't. (And if the concern is about related modules, e.g. multiple submodules in the same package, note that in practice all enabled modules will be touched in the same page request anyway, at least for uncached page requests.)

Having to wait for a rebuild means less things can go wrong in the mean time, ie. when one person passes a database dump to someone else with almost the same files installed, except this one module is duplicated in two locations on one system but only in one location on the other.

OK, yeah, I can buy that we'd like to avoid any problems with that.

stefan.r’s picture

...which reminds me, we should be checking if a module/theme has moved in system_list() (where the drupal_get_filename() cache gets primed after a module_load_all() under normal circumstances).

We may also need to look what to do for theme_engine files, I don't think those locations are stored in the system table?

I'm willing to investigate if updating the system table directly is feasible if @Fabianx thinks it's fine after @David's reminder that all enabled modules are loaded on every request. But there's not a single edge case where (moved) disabled modules/profiles could be a problem? (I think we already loads disabled themes on every request?) And as far as core is concerned I believe the system table should only hold module and theme files, can it hold anything else?

stefan.r’s picture

Also, moving a file may still leave the menu_router include_file data and the registry data inconsistent, but that would have happened before the patch as well, right?

Fabianx’s picture

Yes, if there was a chance to fix that, that would be great though :-D, but the problem is that then we need a lot of locking to ensure we don't get new races, while with the current cache layer the worst that can happen is that we do another file scan ...

stefan.r’s picture

Discussed the system table updating at the end of a request on IRC with @catch and @Fabianx and I think I'm going to explore taking out the file scan and caching its result a bit further first, maybe we can make that simpler after all...

stefan.r’s picture

FileSize
21.68 KB
19.9 KB
Fabianx’s picture

Overall looks much better now, will have a closer look again tomorrow - hopefully.

stefan.r’s picture

Thanks for having a look. Just a few questions to look into when you do:

  1. +++ b/includes/bootstrap.inc
    @@ -850,192 +850,222 @@
    +function _drupal_get_filename_fallback($type, $name) {
    

    Maybe we can we think of a better name for this.

  2. +++ b/includes/bootstrap.inc
    @@ -850,192 +850,222 @@
    +      $files[$type][$matched_name] = $file->uri;
    

    It's not too confusing that this is called the same as the leading static in drupal_get_filename()?

  3. +++ b/includes/module.inc
    @@ -121,6 +121,7 @@ function module_list($refresh = FALSE, $bootstrap_refresh = FALSE, $sort = FALSE
    +  $system_filepaths = &drupal_static('system_filepaths');
    

    Are we fine with the way this is named?

stefan.r’s picture

FileSize
19.09 KB
4.38 KB

Some nitpicks, and moved the cache clearing to system_list_reset()

stefan.r’s picture

FileSize
9.83 KB
19.78 KB

Updated some comments

stefan.r’s picture

FileSize
19.73 KB
2.03 KB
joseph.olstad’s picture

Tested patch 480 481.

Applied patch.
installed test contrib module
removed test contrib module from the filesystem

clear cache, no notices

drush updatedb
WD system: The following module is missing from the file system: test. [error]
No database updates required [success]

then did some basic performance profile of a cache clear.

Missing module from filesystem test took 43 seconds to perform the cache clear.

Restored test module to the filesystem.

Performed another cache clear.
Test configuration took exactly 43 seconds to perform the cache clear, same as when files were missing.

From these rudimentary tests I can confirm that patch 480 481 is working as expected.

Thanks

RTBC+

stefan.r’s picture

Thanks for the manual testing. So Drush ought to be fine then, I guess it's only during a "drush make" where an error may be problematic? (and a user-level PHP warning may still be fine)

I also noticed this triggers a missing file warning on every request, but only triggers a moved file error once, so we should probably update the patch to warn on every request with moved files as well.

I think the ability to move modules may actually be an unintended consequence of the file scan, because even if the drupal_get_filename()/drupal_get_path() call works correctly for a moved module, the registry / menu router records still remain broken.

stefan.r’s picture

FileSize
19.92 KB
3.32 KB

This should show the "moved file" warning every request.

joseph.olstad’s picture

Hi stefan.r 481 was sufficient for my tastebuds (warning when running updatedb, no problem with drush make, I believe that drush make confusion may have been refering to some earlier discussion about how some distributions of drupal might forget to pm-uninstall between versions while removing a feature or custom or contrib module), hence the need for this issue to get a final patch to commit and into core.

joseph.olstad’s picture

(same message as above)

David_Rothstein’s picture

Only skimmed it, but the new approach looks cleaner so far!

We probably need to test this with the Update Manager also. In Drupal 8, this is causing an issue there because the Update Manager code was relying on drupal_get_path() to check whether a module/theme exists in the filesystem or not (see #2042447-133: Install a module user interface does not install modules (or themes)). I think Drupal 7 might have a similar issue...

joseph.olstad’s picture

Ok, I re-ran a similar testplan outlined in comment #481 however this time instead of removing only one test module I removed 4 features/modules from the filesystem thinking that I might notice bigger performance differences (however I only sampled a half dozen times or so.

Results: between 38 and 47 seconds for a cache clear. The higher number came when the patch was rolled back(reversed). I ran more cache-clears afterwards and could not get the higher number again.

Visual tests were done (no perceptible performance differences between tests with and without patch) - xhprof would come in handy about now.

The patch otherwise working as expected, the only thing I could say is that the drush message includes HTML markup that isn't pretty in the drush command line mode.

Here's what the output looks like:

PHP warning:   The following <em class="placeholder">module</em> is missing from the file system: <em class="placeholder">rules</em>. In order to fix this, put the file back in its        [warning]
original location or uninstall the module. For more information, see <a href="https://www.drupal.org/node/2487215">the documentation page</a>. bootstrap.inc:1000
The following <em class="placeholder">module</em> is missing from the file system: <em class="placeholder">rules</em>. In order to fix this, put the file back in its        [warning]
original location or uninstall the module. For more information, see <a href="https://www.drupal.org/node/2487215">the documentation page</a>. bootstrap.inc:1000
The following module is missing from the file system: rules. In order to fix this, put the file back in its original location or uninstall the module. For more information, [error]
see the documentation page. in _drupal_get_filename_fallback() (line 1000 of /var/www/html/d/includes/bootstrap.inc).
The following module is missing from the file system: rules. In order to fix this, put the file back in its original location or uninstall the module. For more information, [error]
see the documentation page. in _drupal_get_filename_fallback() (line 1000 of /var/www/html/d/includes/bootstrap.inc).

Also, for some reason it looks like the same/similar notice shows up twice.

It'd be nice to pull out xhprof and run benchmarks with and without the patch 483, and compare that to patch 102 (which we're currently using in production thanks). This would give us a better idea of where we're at in terms of performance and have some sort of benchmark set up.

My personal server is one of the better places to test this because it's load doesn't fluctuate very much and it's on a physical box with predictable performance/availability.

For now, aside from the markup on the text, I don't have any concerns other than wanting a full xhprof performance diagnosis.

The text output in 483 differs from 481 in the html tagging of the drush text output added to 483.

We've come so far, almost to the finish-line, please help!

stefan.r’s picture

@David_Rothstein regarding the updater, D7 does also have a ModuleUpdater::getInstallDirectory() and ModuleUpdater::isInstalled() that do a drupal_get_path(), but I'm not sure those should be triggering a (partial) cache clear. As opposed to D8 (where we dispose of the file scan), in the current D7 patch we merely cache the result of the file scan, which as far as the update manager is concerned we should probably stick with, for consistency. So I gues the only thing we want to test is that it doesn't trigger any "missing file" errors where it shouldn't?

@joseph.olstad I am not sure that comparing with #102 will help us further along here -- both patches skip the file scan so the performance should be the same.

The user warning will show up once for every call to drupal_get_filename(). Indeed as mentioned earlier we should probably make that be once every request instead.

As to the drush message, what do you suggest we do about it? Maybe a separate issue (in Drush or in Drupal core) where we make the error handler show cleaner messages on CLI?

Fabianx’s picture

#488: The HTML message thingy is out-of-scope here, but just doing it once per request should be fine.

stefan.r’s picture

Yes, that was what I was suggesting as well, i.e. keep the HTML in the message, and solve the CLI HTML filtering in another issue. It could even be fixed in Drush itself.

stefan.r’s picture

FileSize
3.08 KB
20.94 KB

Update manager seems fine.

I don't think there's actually a case where it would throw an unnecessary error as it doesn't allow for installing previously non-existing modules anyway. I don't think it needs to be (partially) clearing any caches either -- if the rest of drupal considers the module to be in a certain place, the update manager can assume that too. And if it's missing, it won't be trying to update it anyway. wrong, I forgot about the installer

Added a "one error per request" function.

stefan.r’s picture

FileSize
1.03 KB
20.94 KB

Removing a trailing space

Fabianx’s picture

Status: Needs review » Needs work
+++ b/includes/bootstrap.inc
@@ -992,8 +992,35 @@
+  static $errors_created = array();
+  if (empty($errors_shown[$type][$name][$error_type])) {
...
+    $errors_created[$type][$name][$error_type] = TRUE;

$errors_shown vs. $errors_created won't fly ...

David_Rothstein’s picture

@David_Rothstein regarding the updater.... So I guess the only thing we want to test is that it doesn't trigger any "missing file" errors where it shouldn't?

Right, that's what I meant. Thanks for checking. I'm surprised it doesn't trigger an error actually - the code in the Update Manager that does this looks very very similar in Drupal 7 and Drupal 8 so I would expect that in both cases it calls drupal_get_path() on a nonexistent module and therefore triggers the error message.

stefan.r’s picture

@David_Rothstein well maybe I was doing this wrong then, perhaps a silly question but at what point would that be calling a drupal_get_path() on a nonexistent module?

The thing I tested was updating an existing module - should that call a drupal_get_path() on a nonexistent module at any point?

stefan.r’s picture

Status: Needs work » Needs review
FileSize
887 bytes
20.94 KB
stefan.r’s picture

Ah, I had overlooked admin/modules/install -- let me test that.

stefan.r’s picture

FileSize
5.9 KB
22.65 KB

Indeed that was triggering an unnecessary error, here's a try at fixing that.

stefan.r’s picture

FileSize
22.64 KB
1.43 KB

And we can actually keep casting to boolean as (bool) NULL === FALSE

stefan.r’s picture

Anyone willing to review/test this? :)

dagmar’s picture

I ran a quick search and it seems the pattern cache_get('_cache_identifier') is not used anywhere in core.

Maybe you could use a more descriptive value for that cache ID. For reference here is the list of the current values for the cache_bootstrap table:

  • bootstrap_modules
  • system_list
  • module_implements
  • hook_info
  • variables
  • lookup_cache
stefan.r’s picture

A lot of those use the function name though. This one just happens to begin with an underscore.

How about we rename it to file_scans?

mikeytown2’s picture

Just gave #499 a spin and it seems the cache it not written under all circumstances; even if drupal was fully bootstraped.

Put this in a php file like test.php at the root level; note that REQUEST_TIME_FLOAT requires php 5.4

define('DRUPAL_ROOT', getcwd());
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
$runtime = round(microtime(TRUE) - $_SERVER['REQUEST_TIME_FLOAT'], 5);
echo "Drupal bootstrap took $runtime seconds\n<br>";

Run it a couple of times to get baseline on how long drupal takes. Remove a module and run test.php again a couple of times. Visit a normal drupal page like /admin, then hit test.php and should get faster.

Why doesn't test.php get faster after the 2nd run, why do I need to run menu_execute_active_handler() for this to work; drupal_deliver_html_page()? What about Drupal acting as a backend for something else?

Making sure we're OK with this or if we want to look over what we have currently; maybe use a shutdown function? Then again looking at module_implements_write_cache() makes me think this is in the correct place...

stefan.r’s picture

because the cache is written to in drupal_page_footer()

Fabianx’s picture

#503: Yes, the benchmark needs to include a call to drupal_page_footer(), that is normal and needs to be used for bootstrap testing, also for module_implements testing ...

mikeytown2’s picture

RTBC from me

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Lets do it!

pwaterz’s picture

First off, thanks so much for all the hard work on this! I have a question and I can't quite make out based on the book of comments here. Will this patch prevent modules from being disabled if the module goes missing? Like if I put the module back in place, will it re enable it self?

marcelovani’s picture

Nothing to be worried about pwaterz, it will check again anytime the cache is cleared

fgm’s picture

Status: Reviewed & tested by the community » Needs work

Funny, I just happened to notice this issue yesterday diagnosing a slow D7 customer site : I had not seen a site with removed/moved modules for years.

  1. +++ b/includes/bootstrap.inc
    @@ -847,68 +854,269 @@ function drupal_get_filename($type, $name, $filename = NULL) {
    -  // Verify that we have an active database connection, before querying
    

    I think the patch may be ignoring one case: it assumes the DB can only be unavailable during an install of it the DB can not be reached. However, I suspect that with $conf['page_cache_without_database'] = TRUE, combined with $conf['page_cache_invoke_hooks'] = TRUE (it is usually set to FALSE in that case), this might be triggered to look for hook implementations after a non-DB cache flush. This probably needs to be tested to be sure the case is covered, but even if it works, the comment leading to the exception would then need to be completed by this case.

  2. +++ b/includes/bootstrap.inc
    @@ -847,68 +854,269 @@ function drupal_get_filename($type, $name, $filename = NULL) {
    +      catch (Exception $e) {
    

    Since this patch adds an exception, the phpdoc should be updated with a @throws.

stefan.r’s picture

+++ b/includes/bootstrap.inc
@@ -847,68 +854,269 @@ function drupal_get_filename($type, $name, $filename = NULL) {
+ catch (Exception $e) {

I don't think #2 is needed? drupal_get_filename doesn't throw an exception does it?

stefan.r’s picture

Status: Needs work » Needs review
FileSize
22.8 KB
762 bytes
fgm’s picture

@stefan.r : my bad, you're right. If the patch keeps this pokemon catch, any exception will not surface, so there is nothing to add.

I cannot but wonder if it is really sane to catch all the exceptions which can be raised by db_query, though. Because, although this is not documented, db_query can throw exceptions, at least \DatabaseConnectionNotDefinedException and DatabaseDriverNotSpecifiedException from \Database::openConnection and \PDOException from the \DatabaseConnection::query() method, and catching \Exception is normally seen as bad practice.

stefan.r’s picture

Well yes... it's not pretty but in practice I can't see this doing any damage. Considering the same catch is in the current function as well and we merely leave it unchanged, getting that exactly right would be out of scope for this patch.

Let's re-RTBC if you agree with the updated comment in #512?

fgm’s picture

Status: Needs review » Reviewed & tested by the community

Indeed.

David_Rothstein’s picture

Status: Reviewed & tested by the community » Needs work

The patch is complicated but well-written and well-documented. I'd like to get it in, but I tested it and it didn't seem to fully work:

  1. When I tried moving a module in the filesystem, the code still does a filesystem scan on every page request, and also results in a PHP notice: Undefined index: module in _drupal_get_filename_fallback() (line 985.
  2. When I tried calling drupal_get_path() on a module that exists in the filesystem but is not yet in the {system} table, the code does a filesystem scan on every page request, and never displays the message.

I think some of this might be due to the 'system_filepaths' static variable, the logic of which I found hard to follow and I'm not sure it's working correctly.

Do we even need that variable and the logic surrounding it? I know we've been through a few iterations here and there are a lot of edge cases we're trying to handle, but I'm trying to remember if there's a reason we couldn't use simpler logic. For example, leave the main part of drupal_get_filename() alone, but at the end, before performing a filesystem scan:

  1. Do a cache_get() + merge into the static variable (only once per page request).
  2. Check the static cache. If there's a record of the item in the cache, use it if it's either (a) FALSE (missing module) or (b) a location for which file_exists() returns TRUE.
  3. Otherwise, do the filesystem scan. If the module is missing, store FALSE in the static cache. If the module is found (i.e. a moved or newly-appeared module), store its location in the static cache. In both cases, set #write_cache to trigger a cache_set() at the end of the page request.
  4. Trigger errors if requested, "missing" if the result is FALSE or "moved" (actually a bit misnamed) otherwise. Note that we should probably skip triggering the "moved" error in the case where the db_query() on the {system} table was unable to run (e.g., database down/installer situation).

Would anything be missing if we simplified it like the above?

Some other things:

  1. I think we should have a change notice for this issue, to cover things like:
    -    return (bool) drupal_get_path('theme', $this->name);
    +    return (bool) drupal_get_filename('theme', $this->name, NULL, FALSE);
    

    and:

    -              if (!drupal_get_filename('module', $module)) {
    +              // Pass FALSE as fourth argument so no error gets created for
    +              // the missing file.
    +              $found_module = drupal_get_filename('module', $module, NULL, FALSE);
    

    I'm still a little wary of those changes, but the patch seems to catch every case in core that needs it, and I couldn't find any in contrib after grepping through a few popular modules. Plus the worst that happens is a trigger_error() anyway, so it should be OK.

  2. The documentation page at https://www.drupal.org/node/2487215 looks like it needs a lot of updating (lots of @todos). It could also be simplified - I'm not sure we ever need to document writing an update function, and for the "moved module" case shouldn't a cache clear always be the recommendation?

    We probably also need a section on "the module never existed in the first place" (e.g. where this error appears due to a bug in a contrib module). Most of the first paragraph could move to this section; also, the reference to module_exists() in the first paragraph doesn't look right - I don't think this issue is necessarily limited to enabled modules.

  3. + * @param boolean $create_errors
    + *   Whether to create an error when a file is missing / moved. Defaults to
    ....
    + * @param boolean $create_errors
    + *   Whether to create an error when a file is missing or moved.
    

    I think this would be better named $trigger_error (and documented as "whether to trigger an error"). Also I think it should be @param bool rather than @param boolean?

  4. +      trigger_error(format_string('The following %type has moved on the file system: %name. In order to fix this, clear caches or put the file back in its original location. For more information, see <a href="@documentation">the documentation page</a>.', array('%type' => $type, '%name' => $name, '@documentation' => 'https://www.drupal.org/node/2487215')), E_USER_WARNING);
    

    "moved within the file system" might be better wording here. Although based on the above discussion, it could be better to say "has moved or has recently appeared in the file system"?

  5. +  // @see system_update_7049()
    

    Should be a complete sentence since this is an inline code comment (not PHPDoc), e.g. "See system_update_7049()."

stefan.r’s picture

Aren't 1 and 2 the intended behavior (other than the notice)? Let me have a look.

stefan.r’s picture

Status: Needs work » Needs review
FileSize
5.92 KB
22.91 KB

I don't know that we need to be catering to use case #2 (drupal_get_filename on a module not yet in the system table) - we hadn't thought of it earlier but it sounds like enough of a edge case for it to be out of scope here... we can keep the file scan there I think.

As to the notice, that seems very odd. How can I reproduce this? Merely installing a module and moving it doesn't work. Are you calling drupal_get_filename() before system_list() had been called? (in module_load_all()). That's where the static is being built...

As to the proposal of getting rid of the system static entirely, I'm not too sure. I remember discussing this with Fabianx/catch(?) and I think it's better to keep it. The problem is we want to know if something has moved relative to the path in the system table which we consider "leading" (see earlier comments). Removing that feels like too drastic of a change.

Uploading a new patch that incorporates the feedback. It seems the file scan for moved modules wasn't being cached properly indeed :)

stefan.r’s picture

stefan.r’s picture

Actually I misread the line number... 985 was not about the system static but about the $file_scans static. That should now be fixed with the latest change:

+++ b/includes/bootstrap.inc
@@ -982,14 +982,14 @@
-      if ($file_scans[$type][$name] != $filename_from_file_scan) {
+      if (!isset($file_scans[$type][$name]) || isset($file_scans[$type][$name]) && $file_scans[$type][$name] != $filename_from_file_scan) {

This tells us to write to persistent cache if it's a new "moved" record as well (i.e. when $file_scans[$type][$name] is not yet set)

I did manage to reproduce the notice with the old patch (on a bootstrap module - not sure if that mattered), but with the new patch this seems to work.

David_Rothstein’s picture

My testing was with a non-bootstrap module (I used the Diff module). Here are the steps I took:

  1. Install Drupal core with the patch applied and debugging added to _drupal_get_filename_perform_file_scan() to track when it's called.
  2. Install the module, clear caches, and visit the homepage.
  3. Move the module (e.g. from sites/all/modules/contrib to sites/all/modules) and reload the homepage a few times.

The issue was reproducible with #512 but I can confirm that #518 fixes it.

As to the proposal of getting rid of the system static entirely, I'm not too sure. I remember discussing this with Fabianx/catch(?) and I think it's better to keep it. The problem is we want to know if something has moved relative to the path in the system table which we consider "leading" (see earlier comments)

I saw the comments, but I guess it's just not clear to me how the system static relates to them. With the code already in drupal_get_filename() before this patch, we know that the only time we run a file scan is when the info from the {system} table is unavailable, missing, or out-of-date. Given that we already do that, I'm not sure what else we get from the static variable. Can anyone think of a (manual) test scenario where the static is expected to be necessary?

David_Rothstein’s picture

The change record and documentation page are looking good. I made a few more updates to them now, and I think they are probably set.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Btw. another module leading to a file scan on each drupal_load(): flickrapi (fixed in their -dev versions already), but the module_load_include with a typo really puzzled us at first ...

So good to see this moving forward fine.

stefan.r’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
11.85 KB
23.3 KB

Implementing the suggestion in #521 to see what it looks like.

Not sure it's a simplification but I don't see any big problem with it either.

Status: Needs review » Needs work

The last submitted patch, 524: 1081266-524.patch, failed testing.

David_Rothstein’s picture

Hm, I thought it might be simpler than that actually. I will try to take a look at this later and see.

I guess another part that isn't clear to me is why all the code at the bottom of drupal_get_filename() that modifies &drupal_static('_drupal_get_filename_fallback') is necessary? It seemed to me like that logic is (or at least could be) handled by _drupal_get_filename_fallback() itself.

stefan.r’s picture

Status: Needs work » Needs review
FileSize
23.61 KB
0 bytes

Let's see if this fixes the test fail, the trigger_error() message was being suppressed because the system table didn't exist.

As to the code at the bottom of drupal_get_filename(), it's necessary just to keep the cached list up-to-date and remove entries that used to be marked as moved or missing which have reappeared at their original locations.

If the modules reappear, a call to drupal_get_filename('reappeared_module') won't go through _drupal_get_filename_fallback(). This was the case in the previous version of the patch as well FWIW.

@David_Rothstein expressed a concern about the logic in the current code being too different to the previously RTBC'ed code... could anyone give the changes a close review and see if this should actually be a worry?

Status: Needs review » Needs work

The last submitted patch, 527: 1081266-527.patch, failed testing.

stefan.r’s picture

Hmm looking at these failures I wonder if we should go back to the system table check solution that had been RTBCed 4 months ago. It's not that complicated really - it just caches the result of the 2 system table checks we do in core.

Not sure catering to the "newly introduced file" use case is worth the effort and additional risk here... as that would be fixed by a cache clear anyway and is not as common.

David_Rothstein’s picture

Sorry for the delay. I'm going to take another look at this tomorrow.

David_Rothstein’s picture

Status: Needs work » Needs review
FileSize
28.54 KB
34.18 KB

OK, I tried reworking the patch and think I may have succeeded in simplifying it. All the logic is now self-contained underneath _drupal_get_filename_fallback() and reasonably straightforward, and all the various cases should be handled.

The patch is bigger than the last one, but only because I expanded the tests also. (I'm posting the interdiff too, but the patch itself is probably easier to review.)

As to the code at the bottom of drupal_get_filename(), it's necessary just to keep the cached list up-to-date and remove entries that used to be marked as moved or missing which have reappeared at their original locations.

If the modules reappear, a call to drupal_get_filename('reappeared_module') won't go through _drupal_get_filename_fallback().

But if _drupal_get_filename_fallback() doesn't get called, why do we care about its cache being up to date? It doesn't have to be (and can't be) an accurate record of exactly what's in the filesystem - it's just a cache of what we found the last time we scanned the filesystem.

The only case I can think of where this would make a difference would be if you deleted a module, then restored it to its original location, then moved it to a new location (all without a system_list_reset() anywhere in between). But the earlier patches don't actually deal with that either (I tested this with #527) since they don't do a cache_get() at the bottom of drupal_get_filename() (and presumably shouldn't, for performance reasons). The cache doesn't actually get updated in those cases either, right?

It also seems fine not to deal with that. I think you all came to a similar conclusion earlier - we need to support putting a module back in its original location (so the site can recover after something goes wrong) and we need to support moving a module too, but we shouldn't have to support any more complicated scenarios because it's reasonable to assume you need to visit admin/modules or clear caches after making big changes to the filesystem.

David_Rothstein’s picture

FileSize
28.49 KB
473 bytes

Removing a line of code that was probably a bad idea on my part...

stefan.r’s picture

This seems well thought out, it's quite a bit simpler and has extra test coverage, so +1.

I agree we can live with the cached file scans not being completely up to date - it changes behavior a bit in some edge cases, but those would be solved through a system_rebuild_module_data.

+++ b/includes/bootstrap.inc
@@ -847,64 +854,253 @@ function drupal_get_filename($type, $name, $filename = NULL) {
+function &_drupal_file_scan_cache() {

Is the & needed here?

stefan.r’s picture

Should we also have a prominent reminder in the release notes about turning off error messages on production systems and about this patch potentially causing thousands of entries in the error log?

David_Rothstein’s picture

Thanks for the review! Anyone able to do some more review or testing of this?

+function &_drupal_file_scan_cache() {

Is the & needed here?

I believe so since it's what allows functions that call this to modify the cache. (Same reason that the definitions of drupal_static() and batch_get() have it.)

Should we also have a prominent reminder in the release notes about turning off error messages on production systems and about this patch potentially causing thousands of entries in the error log?

Yeah, good idea. Regarding the thousands of entries in the error log, I wonder if there's a way to make it fewer? But the only obvious way I can think of would be to show the message once after the cache is cleared (i.e. only when the actual file scan is performed) which would perhaps be too infrequent.

jeroen.b’s picture

+1 looking good, also great work on the test coverage.

I wonder if there's a way to make it fewer? But the only obvious way I can think of would be to show the message once after the cache is cleared (i.e. only when the actual file scan is performed) which would perhaps be too infrequent.

I believe that's what we had before. Only adding a log entry when the actual scan is performed.
It makes more sense to do that because mostly that's a point a developer/site builder will see it (on update.php or drush cc all), and afaik, that's the only one that is able to fix such issues.

stefan.r’s picture

Not a fan of only doing it on file scan, we want to incentivize people to fix their stuff, just a single log entry would be easily ignored... and missed on actual page loads on dev environments (which display trigger_error messages in a drupal_set_message)

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community
+++ b/includes/bootstrap.inc
@@ -847,64 +854,253 @@ function drupal_get_filename($type, $name, $filename = NULL) {
+  // Only write to the persistent cache if we are fully bootstrapped.
+  if (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL) {
+    return;
+  }

One question:

Should we use the "all_modules_loaded" condition here as that is what we really care about and not the bootstrap phase to be consistent?


Overall:

+2 to the new patch, RTBC

jeroen.b’s picture

Not a fan of only doing it on file scan, we want to incentivize people to fix their stuff, just a single log entry would be easily ignored... and missed on actual page loads on dev environments (which display trigger_error messages in a drupal_set_message)

I get that, but I wonder if showing it every time is going to help to do that. I'd rather go with a hook_requirements approach if we want to motivate people to fix it.
The current approach works fine for me (show on actual file scan), but maybe that's because I'm a CLI guy and I see the message clearly when I run drush updb.

stefan.r’s picture

@jeroen.b maybe we're actually on the same page -- I agree the current patch is fine. I think it does a trigger_error() on every failed drupal_get_filename call, rather than on every uncached file scan.

I remember we went through this earlier in the thread, the only real disadvantage is that we may be a bit too annoying in people's logs, but that's solved as soon as they fix the problem, and other PHP errors trigger on every request as well... So building some kind of limit in the error handler feels like unnecessary optimization and doing it only on uncached file scans feels like not doing it often enough. A hook_requirements() would be nice, but that can be looked at in #2464601: Check for missing modules in hook_requirements

Checked with @Fabianx as well and looks like we're good with the current patch:

3:19 PM <
> Fabianx-screen: for https://www.drupal.org/node/1081266 do you think it's OK to do the trigger_error() on every request? I don't really like doing it on file scan only
4:14 PM <
> stefan_r: I am okay with that.
David_Rothstein’s picture

One question:

Should we use the "all_modules_loaded" condition here as that is what we really care about and not the bootstrap phase to be consistent?

Actually, do we need to do either? What we're writing to the cache here is just the results of a filesystem scan, so why would it matter?

Instead of that, we could probably just check the #cache_merge_done key and make sure it's set, since that indicates that whatever came from the cache will get written back to the cache, and also ensures that the cache_set() call will succeed (in case this is done extremely early in the bootstrap) since it means cache_get() ran successfully earlier in the same request.

jeroen.b’s picture

@stefan.r, yeah, you're right. On every page request is fine.
Once every real filescan is way too easy to ignore/miss.

stefan.r’s picture

@David_Rothstein I guess now that we don't care about whether modules that are marked as missing/moved actually still are it may be fine to write to it always.

The only problem (which we already have anyway) is that it can be a bit confusing when debugging, and the "unnecessary" caching could lead to us cache a file scan before we want it to be cached in some edge cases (i.e. not finding the new location when a module is in both the profile and in sites/all), but that would be solved through a cache clear anyway.

Wouldn't this possibly be an extra unnecessary cache_set() call though - or the #cache_merge_done check would fix that?

David_Rothstein’s picture

Sorry, just to clarify, I didn't mean #cache_merge_done would be the only check (the #write_cache would still be checked too) so something like this:

function drupal_file_scan_write_cache() {
  // Only write to the persistent cache if requested, and if we know that any
  // data previously in the cache was successfully loaded and merged in by
  // _drupal_file_scan_cache().
  $file_scans = &_drupal_file_scan_cache();
  if (isset($file_scans['#write_cache']) && isset($file_scans['#cache_merge_done'])) {
    unset($file_scans['#write_cache']);
    cache_set('_drupal_file_scan_cache', $file_scans, 'cache_bootstrap');
  }
}
stefan.r’s picture

Yes I understood that much, just haven't given it any though yet - for now I don't see any big problem, so if you think this optimization is an improvement I have no objection.

OTOH if we leave the patch as-is, the only drawback is we don't write to persistent cache in the edge case of finishing the request before full bootstrap, doing a file scan that we currently do anyway, which I could live with as well, we already do a trigger_error() anyway.

David_Rothstein’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
28.57 KB
1013 bytes

I could go either way also and don't have a huge preference. I think checking #cache_merge_done might be slightly nicer since it's more self-contained, so here's what the patch looks like if we change it to that.

Either way, we won't be able to get this patch in for this week's release unfortunately. Too many changes too recently, and I think we'll benefit from getting a bit more review here (and hopefully real world testing). I think we can definitely aim to get it in the next release though, which probably won't be too many months down the road either. Sorry for pushing back a lot on this issue, but since it's a big change that affects a very low-level part of Drupal, I think it needs extra review to make sure we get it right.

Fabianx’s picture

I agree with #546 that it is better to skip this release rather than rush it in. +1 to that.

The interdiff looks fine to me.

joseph.olstad’s picture

The new patch looks good, seems to be quick.

I have only a minor quirp, I'm a CLI guy and like using drush.

Patch #102 makes a nice pretty CLI output as follows:

WD system: The following module is missing from the file system: linkit. [error]

however Patch #546 outputs HTML onto the CLI in drush as follows:

PHP Warning: The following module is missing from the file system: <em class="placeholder">linkit</em>. In order to fix this, put the module back in its original location. For more information, see <a href="https://www.drupal.org/node/2487215">the documentation page</a>. in includes/bootstrap.inc on line 1079

However I'm fine with that as I'd rather have the PHP warning than no warning at all (but prefer the WD system message as in patch 102).

Either way it looks great! Thanks.

stefan.r’s picture

The CLI problem should rather be solved in drush IMO... this is not the only commands that outputs HTML

stefan.r’s picture

Anyone have #546 running on production or care to give this another review?

skadu’s picture

I'm not running #546 on production but just patched a development environment. All seems to be working fine for now. I will keep an eye on it over the next couple of weeks.

Only thing to mention was the patch output:

patching file includes/bootstrap.inc
patching file includes/common.inc
patching file includes/module.inc
patching file includes/update.inc
patching file modules/simpletest/simpletest.module
Hunk #1 succeeded at 374 (offset 3 lines).
patching file modules/simpletest/tests/bootstrap.test
patching file modules/system/system.updater.inc

Not sure if there is the offset because of something on my end or the patch, just wanted to share my findings.

  • catch committed b5f7ccc on 8.1.x
    Issue #1081266 by stefan.r, mikeytown2, jeroen.b, tsphethean, mfb,...
jeroen.b’s picture

I have been using #546 on a few sites for over a month now. No problems occurred.

jeroen.b’s picture

Actually, this generates an error when you want to install a profile.

The following module is missing from the file system: <em class="placeholder">{name-of-profile}</em>. In order to fix this, put the module back in its original location. For more information, see <a       [warning]
href="https://www.drupal.org/node/2487215">the documentation page</a>.

Line 1183 of includes/install.inc adds the profile name to the list of dependencies.
Line 1188 tries to load it as a module, which results in a error by _drupal_get_filename_fallback_trigger_error, as there is no module by the name of the profile.

jeroen.b’s picture

Status: Needs review » Needs work
stefan.r’s picture

@jeroen.b is loading a profile as if it were a module something we should support? I.e. should drupal_get_filename('module', 'profile_name') return the location of the profile?

jeroen.b’s picture

I'm not sure.
AFAIK drupal_get_filename has basic support for profiles by telling it you want a profile:

  if ($type == 'profile') {
    $profile_filename = "profiles/$name/$name.profile";
    $files[$type][$name] = file_exists($profile_filename) ? $profile_filename : FALSE;
  }

However, the profile is not a type "profile" in the system table, it's a "module" (which is kinda weird). So we should probably prime the cache somewhere as we do in D8.
drupal_get_filename('module', 'profile_name', 'profiles/profile_name/profile_name.profile');

stefan.r’s picture

I'm OK with that solution - it'd need a file_exists as well

jeroen.b’s picture

Status: Needs work » Needs review
FileSize
29.12 KB
506 bytes

Here's a new patch. The file_exists is already in the code (4 lines above my addition). If it can't find the profile, the installation doesn't even start.

Just tested it, no error in install.

sylus’s picture

Hmm with the latest patch testing in Travis CI I am getting the following with Drush SI:

WD php: User warning: The following module is missing from the file  [warning]
system: . In order to fix this, put the module back in its original
location. For more information, see the documentation page. in
_drupal_get_filename_fallback_trigger_error() (line 1079 of
/home/travis/build/.../drupal-7/drupal/includes/bootstrap.inc).
stefan.r’s picture

@sylus could you may want to backtrace that - seems it's doing a drupal_get_filename('', 'module')?

@jeroen.b that fix seems fine...

sylus’s picture

Ah your right issue is related to #2383823: drupal_get_filename() should check if $name is empty I removed that patch as thought this patch alone would be enough, sorry for the noise :)

pwaterz’s picture

I'd like to make note of some what related issue that we were facing. At my current job we ran into an issue where we moved our site from dev/stage to production. The production environment sometimes has hicups and also sometimes can place modules in different directories that dev or stage. During some of the build hicups modules were not placed in the proper directories. We had issues where entire sets of modules would be auto disabled by drupal core. After a few hours of digging around I found that is was caused by update.php/drush updb. The update process calls https://api.drupal.org/api/drupal/includes%21update.inc/function/update_... to check 'compatibility' of modules and disables them if they aren't. One issue is that if a module is missing for some reason, like a broken symlink and you run drush updb, the system table gets updated and these modules get disabled. This is really scary for our organization because we run hundreds of drupal sites, and if this was to happen on massive scale, yikes! I think there should be a way to disable this feature in drupal core. Does this go along with this ticket or a new ticket?

stefan.r’s picture

@pwaterz new issue - it's unrelated to this one

stefan.r’s picture

Has anyone tested #546 or #559 yet?

jeroen.b’s picture

I have been using #559 since I posted the patch.
But it's my own patch so would be weird to put it to RTBC.

stefan.r’s picture

Status: Needs review » Reviewed & tested by the community

I gave this another look and I concur with Fabianx in #547... David_Rothstein's changes still look great to me. joseph.olstad, sylus, skadu and jeroen.b have tested this in production, and #559 makes sense, so back to RTBC.

David_Rothstein’s picture

Agreed this look ready. Given the somewhat disruptive nature of this (and the late date compared to tomorrow's release window) I'm going to hold it for one more release; especially if we do #2598382: [policy] Adopt a 6-month feature release schedule for Drupal 7 (similar to Drupal 8) and use pseudo-semantic versioning this would be a great fit for an April 20 release of Drupal 7.50 which would have some other bigger changes also. But either way, rest assured it should be in the next non-security release after this one. Thanks everyone for sticking through a 550+ comment issue :)

I could not quite figure out #559 though. The change looks harmless but I can't figure out how to generate the problem (at least #546 seemed to work fine with the core install profiles).

@jeroen.b, what install profile were you using when you experienced that?

In particular:

Actually, this generates an error when you want to install a profile.
....
Line 1183 of includes/install.inc adds the profile name to the list of dependencies.
Line 1188 tries to load it as a module, which results in a error by _drupal_get_filename_fallback_trigger_error, as there is no module by the name of the profile.

I think for line 1183 you're referring to https://api.drupal.org/api/drupal/includes!install.inc/function/install_... but the code there is very explicit about making sure that the profile does not get listed as a dependency of itself. So it seems like it shouldn't ever try to load the .install file via the code you mentioned. That's why I'm not sure how to trigger this.

jeroen.b’s picture

@David_Rothstein
It's a custom profile. I just tested it but couldn't reproduce it with Drupal 7.41.
Then I remembered that specific profile used the Omega8 version, I was able to reproduce it with that.
I'll take it up with Omega8 when this patch lands. Patch #559 can be ignored.

kenorb’s picture

Shell command to test for missing modules:

while read -r file; do [ -f "$file" ] || echo "$file is missing."; done < <(drush sqlq "SELECT filename FROM system WHERE status = 1")

Fabianx’s picture

There is a problem with this patch (or rather sites that update to it):

It can under bad circumstances together with #2450533: SchemaCache is prone to persistent cache corruption / drupal_get_complete_schema() is not robust against recursion break a site:

From cold caches:

- hook_init() =>
- entity_get_info is called (by e.g. menu_set_custom_theme => get_admin_paths() => entity_translation_admin_paths() => bean => ... )
- That triggers drupal_get_schema()
- That triggers drupal_get_complete_schema()
- That calls field_sql_storage_schema, which checks all fields for data
- This calls drupal_get_filename() to load the .install files

- This leads to watchdog being triggered. (Note: The module is just referenced somewhere, but not even in the system table anymore).

- rules_watchdog triggers a loading of event rules
- That triggers ctools_load_plugins => ... => entity_get_info()
- That triggers drupal_get_complete_schema() again, which returns the now incompletely build schema()
- That leads to errors and entity_get_info() being incompletely populated.

While not the fault of this patch, this condition is only resolvable by putting the original module back (or an empty .install file) as it is very hard to find and prune all data referencing somewhere to a module file.

On the other hand, without this patch a system scan would always be done, which would lead to strange and unexplainable slow down and the warning message gives a dev at least a chance to restore the modules.

However a watchdog call can have side effects during early bootstrap or during cache rebuilding.

So my proposal:

'Cache' the watchdog call in a static variable and execute it during shutdown (drupal_exit()), where we also create the caches for e.g. modules. That reduces / eliminates the side effects of this. There is a slight chance that a site crashes and drupal_exit() is not called. However I think we need to live with that.

The 'bad' cache is still good and solves the performance problems to avoid re-scanning the filesystem.

@David: What do you think?

stefan.r’s picture

So, postpone execution of any and all watchdog calls that happen during early bootstrap to the end of the request?

Should we discuss in a separate issue?

Fabianx’s picture

#572: No, not all watchdog calls, that would indeed be a follow-up. Just this newly introduced one for now.

e.g.


$watchdog_collector = &drupal_static('drupal_watchdog_collector', array());
$watchdog_collector[] = array('Some Message', WATCHDOG_INFO);

// error_log should not call any hooks, but log to a file / null or syslog. That should ensure in case of a crash the message can be found somewhere.
error_log('Warning: Some Message');

function hook_exit() {
  $watchdog_collector = &drupal_static('drupal_watchdog_collector', array());
  if (!empty($watchdog_collector)) {
    foreach ($watchdog_collector as $watchdog_entry) {
      call_user_func_array('watchdog', $watchdog_entry);
    }
  }
}
David_Rothstein’s picture

Hm, I think that makes sense, but the patch is currently using trigger_error (rather than a direct watchdog call) in order to get the message to appear on the screen on development sites... I'm not sure offhand what happens if you try to do a trigger_error that late (after sessions have already been committed).

We could also look at trying to get #2450533: SchemaCache is prone to persistent cache corruption / drupal_get_complete_schema() is not robust against recursion fixed in the same release. (At this point I think we're aiming for May 4 rather than April 20.) But I think you're saying that you see other potential risks with this watchdog call being directly triggered by a low-level function?

It looks like rules_watchdog() is doing some pretty heavy stuff for a low-level hook like hook_watchdog(); ideally it would not do that, but it is what it is :)

Fabianx’s picture

#574: Indeed, my distribution site used an older version of this patch, which is why I was confused.

If we only trigger_error(), we should be okay. I will confirm on my site, but this means the patch can go ahead as planned.

And I agree that rules_watchdog() should at least also check for BOOTSTRAP_FULL or at least their (rules) hook_init() should be moved as last item in the hook chain. I will open an issue.

marcelovani’s picture

I think this is related to https://www.drupal.org/node/2324587#comment-9397273
Can you check if you are using the version of Rules that contains the fix of patch #37?

stefan.r’s picture

So I guess this will be for a May 4 release?

Just a note that we'll need to update the docs at https://www.drupal.org/node/2487215

SocialNicheGuru’s picture

I applied the patch (awesome patch by the way).
I deleted a directory by accident
I got this on my screen
Notice: Undefined variable: trigger_error in drupal_get_filename() (line 888 of /includes/bootstrap.inc).
the line is:

$files[$type][$name] = _drupal_get_filename_fallback($type, $name, $trigger_error, $database_unavailable);

should we pass in 'TRUE' for $trigger_error since the variable is not defined and as no default value?

stefan.r’s picture

#578: I think we already do? You'll want to use the patch in #559 and ensure it is applied correctly, I don't see how you would get an "undefined variable" error like that when the function signature says:

function drupal_get_filename($type, $name, $filename = NULL, $trigger_error = TRUE) {
alexmoreno’s picture

#559 tested (with an Omega subtheme, although don't think it's relevant) and works more than fine.

I was doing some profiling and found an alarming file_scan_directory function being called more than 1.000 times per call. Applying the patch makes this to dissapear, although I'd recommend anyone to dig further and remove any entries or modules disabled but not uninstalled and similar.

IMO this should be merged ASAP, as it is relatively easy to reproduce in a big/medium site with a big team working for some time (you know, one disables a module, someone else enables, ... you get the idea).

stefan.r’s picture

@Fabianx re #575, doesn't trigger_error() cause a watchdog call?

stefan.r’s picture

Status: Reviewed & tested by the community » Needs work

Looking at this with jeroen.b, back to NW for #571

jeroen.b’s picture

Status: Needs work » Needs review
Issue tags: -D8 Accelerate Dev Days +drupaldevdays
FileSize
30 KB

After discussion with @stefan.r we decided to only delay the trigger_errors for the missing/moved errors so we don't have to touch other code base. We are storing the errors in a drupal_static and then doing a trigger_error in _drupal_bootstrap_full. We are not doing it in system_exit because it's too late to do an drupal_set_message so then the developer wouldn't see the error message.

jeroen.b’s picture

FileSize
3.95 KB

Whoops, missed the interdiff.

Status: Needs review » Needs work

The last submitted patch, 583: 1081266-582.patch, failed testing.

jeroen.b’s picture

Ah, yeah, that makes sense.
Because we do the invoke in _drupal_bootstrap_full(), all the missing/moved errors after _drupal_bootstrap_full won't do a trigger_error.

My solution would be to set another drupal_static in _drupal_get_filename_fallback_trigger_error_invoke whether the invocation has already happened.
In _drupal_get_filename_fallback_trigger_error we would check for that static. The logic to do the actual trigger error would be separated in a new function. When the static is TRUE, the trigger_error happens directly, if it is FALSE the trigger_error is delayed until bootstrap_full.

jeroen.b’s picture

Status: Needs work » Needs review
FileSize
31.01 KB
5.33 KB
4.45 KB

Here's a patch that does what I described.

Status: Needs review » Needs work

The last submitted patch, 587: 1081266-587.patch, failed testing.

jeroen.b’s picture

We might need to update the bootstrap test a bit. It looks directly for a triggered error in the custom error handler, but since we delay the messages now it will fail.

stefan.r’s picture

Issue tags: +Drupal 7.50 target
Fabianx’s picture

I like the idea, I am a little worried about the implementation.

For me this should just be:

function trigger_error_safe(string $error_msg , int $error_type = E_USER_NOTICE) {

  if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) {
    trigger_error($error_msg, $error_type);
    return;
  }

  // It is not yet safe to trigger an error, so queue it up.

  $errors = &drupal_static(__FUNCTION__, array());
  $errors[] = array($error_msg, $error_type);
}

function trigger_all_queued_errors() {
  $errors = &drupal_static('trigger_error_queued', array());

  if (empty($errors)) {
    return;
  }
  foreach ($errors as $error_parameters) {
     call_user_func_array('trigger_error', $error_parameters);
  }
}

Possibly can add something to the message as done before, but the implementation should really be a drop-in replacement for trigger_error(), which we call after we have completed the bootstrap_phase().

jeroen.b’s picture

Thanks Fabian. I get your feedback.
However, after some thinking, shouldn't we just make trigger_error "phase safe"?

Fabianx’s picture

#592: trigger_error() is an internal function, so we can't override it.

On the other hand I would not like to make our error handler depend on bootstrap phase.

So lets please do it the way I suggest in #591.

jeroen.b’s picture

That's not what I meant. I think we should fix it inside our own error handler instead of creating a trigger_error_safe() because we might not be the only one possibly calling trigger_error too early.

Fabianx’s picture

#594: And that is an excellent idea, but not feasible to put into this is already large patch.

Lets use _drupal_trigger_error_safe and _drupal_trigger_all_queued_errors as names then we can technically still remove the functionality again or re-use it from our error handler.

Reasoning:

- We add a new trigger_error() call, where there was none before
- Therefore we get new side effects from this trigger_error() call - e.g. my updb dieing
- If we remove the side effects for _just_ this call, we are good to go for the run-time
- This only means trigger_error() is called shortly bootstrap full phase is reached, which is an acceptable stage to trigger an error

- If we introduce queueing of errors in the error handler now, we get new side effects related to that
- This might lead to unexpected behavior on other sites, etc.

Therefore - even though the idea is great - and even though it would be better to fix this at the "core" in the error handler, this is not helping this patch to get into 7.50 any time soon.

Isolation of side effects needs to be our goal to get this patch in - if we want it now.

jeroen.b’s picture

I get your reasoning. I would be fine with _drupal_trigger_error_safe just for the sake of getting it in.
Maybe we should create a follow-up issue for fixing the watchdog call inside trigger_error?

stefan.r’s picture

Yes, sounds like follow-up material :)

Fabianx’s picture

#596: Yes, a follow-up would be great!

jeroen.b’s picture

@Fabianx will you create the new patch or should I?

Fabianx’s picture

#559: Can you do that please? I am pretty busy atm.

David_Rothstein’s picture

The above plan sounds good to me, but I don't understand this part from #591:

  if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) {
    trigger_error($error_msg, $error_type);
    return;
  }

The main scenario we're trying to protect against here (from #571) is triggered in hook_init() (during full bootstrap) so won't the above code fail to protect against it? (As far as I know, drupal_get_bootstrap_phase() returns DRUPAL_BOOTSTRAP_FULL anytime after the full bootstrap phase has begun.)

But if we leave that code out (which would make the function's behavior more consistent/predictable anyway) then it makes sense to me.

I'd also consider calling the new functions _drupal_trigger_error_delayed() and _drupal_trigger_all_delayed_errors() (or similar - the key point being to use the same terminology for each).

jeroen.b’s picture

@David_Rothstein You are right about the naming of the functions, we should change it.
You are also right about drupal_get_bootstrap_phase() returning DRUPAL_BOOTSTRAP_FULL when it started bootstrapping, not when it finished bootstrapping.
That's actually inconsistent with documentation, as it says "The current phase is the one most recently completed by drupal_bootstrap()".

Thing is, we can't just remove these lines:

if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) {
    trigger_error($error_msg, $error_type);
    return;
  }

Because then we will have no trigger_error() when Drupal has been fully bootstrapped as _drupal_trigger_all_queued_errors() will never be executed.

jeroen.b’s picture

Here is the new patch for completeness. Like @David_Rothstein said it will probably not work.
I could also not think of a proper way to test this edge case.

stefan.r’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 603: 1081266-600.patch, failed testing.

David_Rothstein’s picture

Status: Needs work » Needs review
FileSize
29.72 KB
3.24 KB

Ah, I see.

Well it's kind of ugly but it could be possible to do something like the attached (rough code, untested).

It does seem to me like the key point here is not what stage of the bootstrap you're in, but whether or not you're potentially in the middle of an operation that can't be "interrupted". Because calling trigger_error() => watchdog() at any point in time will invoke a hook which runs arbitrary code which could involve something that breaks the operation you're in the middle of. I think that's effectively what we're trying to say here.

Note this patch is built on top of #546 (not #559) since I think we agreed in #568/#569 that the change in #559 wasn't necessary.

jeroen.b’s picture

That would probably work yeah. Any idea how we can create a test for this?

stefan.r’s picture

I think that may be fine, for the test we can check the watchdog table after a call to _drupal_trigger_error_with_delayed_logging() (if needed, from a test module, for example with a drupalGet() to a page that triggers an error)

stefan.r’s picture

This needs the @todo documented and ideally a test

David_Rothstein’s picture

I should be able to look into that a little later tonight.

David_Rothstein’s picture

Here is a new patch with the documentation plus additional tests.

In addition to testing the watchdog() call itself, I think I was able to test the actual scenario with drupal_get_schema() failing. It's a bit convoluted, but still may have been easier than setting up the Rules module in such a way as to trigger this as per #571 and then testing that manually :)

I'm also posting a patch with the new tests combined with the earlier patch from this issue. If this is working correctly we should see failures for some of the new tests in that case.

David_Rothstein’s picture

The interdiff is compared to #606, by the way.

The last submitted patch, 611: 1081266-546-plus-new-tests-should-fail.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 611: 1081266-611.patch, failed testing.

David_Rothstein’s picture

Status: Needs work » Needs review

Retesting - that looks like a random testbot fail on the real patch.

The first patch failed with the expected failure.

stefan.r’s picture

Assigned: Unassigned » Fabianx
Status: Needs review » Reviewed & tested by the community

Interdiff looks great, with some very nice tests!

Fabianx’s picture

Issue tags: +Pending Drupal 7 commit

The new solution to just trigger the watchdog call delayed is great!

Giving credit to a lot of people on this issue (soooo many have tested the patch or given otherwise useful feedback).

Marking for commit.

  • Fabianx committed 206c7c1 on 7.x
    Issue #1081266 by stefan.r, jeroen.b, mikeytown2, David_Rothstein,...
Fabianx’s picture

Assigned: Fabianx » Unassigned
Status: Reviewed & tested by the community » Fixed
Issue tags: -Pending Drupal 7 commit +7.50 release notes

Committed and pushed to 7.x! Thanks all!

Fixed on commit (as git had a warning related to whitespace problems being added):

diff --git a/modules/simpletest/tests/system_test.install b/modules/simpletest/tests/system_test.install
index 18d4af9..c209233 100644
--- a/modules/simpletest/tests/system_test.install
+++ b/modules/simpletest/tests/system_test.install
@@ -18,5 +18,3 @@ function system_test_schema() {
 
   return array();
 }
-
-
David_Rothstein’s picture

Thanks for going through and figuring out all those issue credits - that must have been a lot of work :)

I edited the change notice to make it a bit more relevant for site builders (who might see this PHP warning too), and published it:
https://www.drupal.org/node/2581445/revisions/view/8963663/9848351

I also edited the documentation page to indicate this is now in Drupal 7.50:
https://www.drupal.org/node/2487215/revisions/view/9220114/9848355

We are hopefully finally done with this enormously long issue. Thanks everyone!

DamienMcKenna’s picture

FYI I'm seeing a weird regression in Metatag's tests from this change: #2761829: Getting drupal_trigger_error_with_delayed_logging errors on Drupal 7.x dev (7.50)

MustangGB’s picture

43 "missing" modules :'(

MustangGB’s picture

Serious question now though, I have a "profiles/default/default.profile" entry in my system table, was there ever a hook_update_N() that should have removed this?

joseph.olstad’s picture

@MustangGB , for those missing modules, if you can, put them back into the filesystem temporarily, then run a pm_uninstall either
drush pmu module_was_awol module_was_awol2 module_was_awol3 module_was_awol4 module_was_awol5 module_was_awol6 module_was_awol7 module_was_awolXX module_was_awol40 -y

or, (if all else fails) in the case that you cannot find the module(s) install this cleanup utility (for use with drush) or for some reason the uninstall fails:

download this ZIP of cleanup_tool
extract it to your sites/all/modules folder

USAGE:

drush en cleanup_tool -y;
drush cc drush;
drush cu-check;
drush cu-clean all --update_option=all -y;
drush dis cleanup_tool -y;
drush pml | grep cleanup_tool;

For more details, please see project page

MustangGB’s picture

@joseph.olstad:

Yea don't worry I've already sorted them all out, except for "default".

My question was really regarding people that have upgraded from D5/D6 and whether "default" should have already been removed during the upgrade/migration process, and are there any consequences of it not having been dealt with, or should we just remove it now with a new hook_update_N().

(Nice module though)

stefan.r’s picture

@MustangGB can you open a new issue for the default.profile problem, in case we want to fix this in an update hook?

MustangGB’s picture

charginghawk’s picture

FYI, the D8 fix spawned #2486083: The following module is missing from the file system: standard, an issue which seems to be appearing in 7.50. We may need to port the fix there to D7.

geek-merlin’s picture

Thanks everyone. A 40k patch, finally awesone!

Status: Fixed » Closed (fixed)

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