Problem/Motivation
Drupal saves its service container in the database by default. In some cases (which?), users may want to save the container to disk instead.
The container cache does no checking for a stale container. If the container exists, and the mtime-hash matching is ok, it is loaded.
Thus, any Drupal event that should lead to a new container must take care to remove the old one, so a new one will be built.
For a multiple webhead site, this requires a) that we put the cached php on a shared disk, or b) that we make sure to provide a custom container loader implementation that checks a central location to determine if the locally cached container is stale.
a) is scary for a lot of reasons, so this issue is about making b) work.
Proposed resolution
Keep track of the container state so that Drupal is aware when a local container file needs to be rebuilt. This patch adds Drupal\Core\PhpStorage\CoordinatedMTimeProtectedFileStorage.
CoordinatedMTimeProtectedFileStorage extends MTimeProtectedFileStorage and does two things:
- overrides MTimeProtectedFileStorage::getFullPath() to incorporate a counter, which is fetched from a global store
- overrides MTimeProtectedFileStorage::deleteAll() to increment the counter
Remaining tasks
TBD
User interface changes
None.
API changes
- Add
Drupal\Core\AtomicCounter\AtomicCounterInterfaceto provide a centralised count interface - Add
Drupal\Core\PhpStorage\CoordinatedMTimeProtectedFileStoragewhich utilises the AtomicCounter to mark containers stale on delete
To enable it, add this to settings.php
$settings['php_storage']['service_container'] = array(
'class' => 'Drupal\Core\PhpStorage\CoordinatedMTimeProtectedFileStorage',
);
| Comment | File | Size | Author |
|---|---|---|---|
| #119 | 2301163-nr-bot.txt | 161 bytes | needs-review-queue-bot |
| #88 | phpstorage-multiple-webheads-2301163-88.patch | 14.48 KB | cameron tod |
| #74 | phpstorage-multiple-webheads-2301163-74.patch | 14.42 KB | jhedstrom |
| #48 | phpstorage-multiple-webheads-2301163-48.patch | 8.34 KB | jhedstrom |
Comments
Comment #1
Anonymous (not verified) commentedComment #2
Anonymous (not verified) commentedpoked at this, tried the dumbest implementation i could think of.
and nope, simple and D8 don't go together.
we can't store any central timestamp/counter in cache or state, and use that to figure out the path to the cached container, because we need to load the container to use cache or state.
this is just another reason why relying on the SF2 container for our DI needs was a really, really bad idea.
so to make this really, really slow, must be cached in php, central to everything blob work on multi-webhead sites we're probably going to have to write the counter/timestamp in to the container itself, load it, then call out to check the central value to see if it's stale.
yay for top-down, architecture astronaut design.
Comment #3
berdirDo we expect that a problem would already occur so early that we wouldn't be able to initialize the container and execute the first event (REQUEST I think)?
If we can say that this is going to work in 99% of the cases, maybe it's enough to just have a request event listener that checks for some flag/state, if that doesn't match, force a container rebuild and reload the page or something like that?
Comment #4
chx commentedWe discussed this with msonnabaum (at least) at ... BADcamp? I think. Simple solution:
> thus, any Drupal event that should lead to a new container
That's a module enable / disable. You are code deploying for that. Deploy a container. Do not allow module enable/disable on multiple webheads. Add a feature to disable the module screen. Or write a module to do it. I have NFI how to do that, in D7 it was a hook_menu_alter with 1 line changing an access callback to FALSE. Or form_alter the submit button out :P
Comment #5
Anonymous (not verified) commentedre. #3 - i don't know? what percentage of WSODs is ok?
chx pointed out that we can use the
Database::getConnection($target)->query($query, $args);without the container, so i'll put something together that does that.Comment #6
chx commentedSo I presume you are patching PhpStorageFactory::get to include a counter in the path.
Since this already uses Settings, may I have a settings override please for the method determining the counter? Thanks! BootstrapConfigStorageFactory::get is an excellent example of what I would like to see.
Comment #7
Anonymous (not verified) commentedok, here's an ugly first cut.
chx - sorry, i didn't do #6. see the comment in the patch.
i guess we could make a CoordinatedMtimeProtectedCounterStorage thingie, but in this first round i just made i possible to specify the db connection target, and the fetch and update queries. not sure if that's enough to make Mongo work, or more generally if that's acceptable, but i guess y'all will tell me if it's not :-)
Comment #8
Anonymous (not verified) commenteddammit wrong patch in #7, please ignore.
Comment #9
Anonymous (not verified) commentedbut wait, it's possible to make this uglier! see interdiff - this adds the necessary initialisation of the counter.
Comment #10
Anonymous (not verified) commentedalso, i think this is a critical. D8 is not really safe on multi-webhead sites, and i'm pretty sure we don't want to ship like that.
not a beta-blocker, because there's no API change.
Comment #12
Anonymous (not verified) commentedComment #13
chx commentedRe #6 , there's already a class override in the factory so we just need to keep that alive.
Edit: http://privatepaste.com/6526b897db
Comment #14
Anonymous (not verified) commentedComment #15
Anonymous (not verified) commentedupdated patch with better docblocks.
i think this is ready to go.
Comment #16
chx commentedI think we are not running INSERT/UPDATE statements? I would put the relevant db_insert / db_update (or just a single db_merge ? ) in methods and then use a variable calling method $callable = $this->update; $callable() and provide ways from settings.php to override that callable so that for eg redis or similar can be used.
Comment #17
Anonymous (not verified) commentedmoved the sql in to methods as suggested in #16, changed the configuration to accept callables for fetching and incrementing the counter.
Comment #19
Anonymous (not verified) commentedwoops, durp durp durpal named the interdiff wrong.
Comment #20
moshe weitzman commentedNeeds work for chx's suggestion to use db_insert/db_update or db_merge
Comment #21
Anonymous (not verified) commentedupdated to use insert and update methods.
Comment #23
Anonymous (not verified) commenteddurp durp without syntax error this time.
Comment #24
sun"Contains \..."
Our current policy is that code in
Drupal\Componentmust not depend on code ofDrupal\Core, so this implementation needs to be moved intoDrupal\Core\PhpStorage(which contains the Drupal-specific factory already)Not sure I can get behind these — wouldn't it be much more natural to simply subclass this implementation and override desired methods?
Non-existing @param.
This won't work in the early installer, in case the settings.php override has been set already, because the key_value table does not exist yet.
We can either adjust the docs in settings.php to explicitly state that this implementation cannot be used for installing Drupal, or we'd have to catch some more database exceptions and silently default the counter to 0.
Comment #25
Anonymous (not verified) commentedneeds work for the points #24.
Comment #26
Anonymous (not verified) commentedComment #27
Anonymous (not verified) commentedthanks for the review sun, new patch fixes #24.1, #24.2 and #24.4.
re. #24.3 - i don't have a strong opinion. there are so many ways to override this. leaving as is, will try to find other people who care to weigh in.
re. #24.5 - tricky. first thought was to just document this, but we want this to 'just' work for those who don't change any defaults. that is, i'd only support not running this code during the installer if we automagically make it the default after install.
will try to catch you in IRC to discuss.
Comment #28
Anonymous (not verified) commentedComment #29
Anonymous (not verified) commentedi don't think this should block release, and interest in it seems fairly low, downgrading.
Comment #30
berdirI've been thinking about a low-budget version of this, that's using a request listener or middleware-thingy to check something after we bootstrapped (and assume that we can get at least that far), then check something there and try rebuild there then.
Comment #31
Anonymous (not verified) commentedberdir - cool, i don't know what that looks like, but happy to trash this patch in favour of something better.
rerolled for 8.0.x in case anyone wants to look at this for Drupalcon.
Comment #32
chx commentedI had a discussion today with David Strauss and we can't find a better way to do this in core -- of course if you have a nice queue system you can do more -- but using the files directory doesn't seem to make sense. files is supposed to be sync'd among webheads, right? So perhaps let's default this driver to the tmp directory -- quite probably not even overrideable and not the Drupal tmp directory because that's not yet available, but sys_get_temp_dir() which is certainly local.
Comment #34
jhedstromGiven that many sites will need to run on multiple webheads, I think this is at least major. I'm working on a reroll of #31 as a starting point for reviving discussion.
Comment #36
jhedstromHeh, no reroll required. I haven't seen that on an 9-month old patch in some time :)
Comment #37
jhedstromThere were some tweaks needed however.
Comment #38
fabianx commentedInstead of individual callables, we want an interface and a default class, then just instantiate the class here and provide it with configuration, too.
This should all be in the default implementation of the MySQLFileStorageCoordinator class.
That also makes it way easier to re-use with a different backend (e.g. just FileStorage).
Comment #39
jhedstromHere's an initial attempt at moving the custom callables to a proper interface (forgive the naming, it feels awkward at the moment).
Comment #40
fabianx commentedUnsure about making it dependent on the database driver by default, I think just falling back to Database is enough - as we always have a database available at this point.
The rest however looks and feels really good.
What about having the interface define the semantics instead of the implementation:
Thanks for your work on that!
@msonnabaum will be very happy :).
Comment #41
jhedstromThis should address the first part. For moving more to the interface, I'm not sure I follow the methods mentioned. I can see moving more logic from
getFullPath(), and perhapsdeleteAll(), but don't see an immediate mapping to the methods listed in #40Comment #42
jhedstromI don't think this is actually incrementing anything.
Comment #43
Crell commentedPlease do NOT rely on the database directly. There are trial implementations of D8 running on 100% MongoDB, and if that gets used in production it's likely that those will all be multi-head. Since we're reading/writing the k/v table already, why not just use the keyvalue service, or state service? Those seems like the appropriate tool (and could be backed by SQL, Redis, Mongo, or whatever the site chooses).
Comment #44
berdirBecause this is about checking if the container is still valid. Which has to happen *before* the container is loaded. No container, no key value service. Just like php storage also isn't using the container/services.
It is pluggable with it's own mechanism.
Comment #45
fabianx commentedTo be fair the K/V class could be used, just would need to inject the database directly.
As far as I know K/V does not depend on any further services we need to inject than the database - we got it running with D7.
In that case just need a EarlyDatabaseKeyValueStore instance and then could just use key/value directly.
The class would still be pluggable, but instead just overwrite __construct to statically inject the Database (or Mongo) or whatever is needed. (or have a ::create() method)
--
That would also get rid of the interface and the incrementCounter / getCounter methods :).
Comment #46
chx commentedYou'd need something like the bootstrap storage in DrupalKernel if you want with pluggability provided by settings.
Comment #47
msonnabaum commentedI'm fine with the idea of a bootstrap storage service, and it defaulting to the db. If we do that, it should be independent of any existing services (kv, state, etc).
I dont think the "site running 100% on mongo" example is worthwhile to cater to, but I can see the use case for a custom configuration service that could handle this.
Comment #48
jhedstromHere's an attempt to add a bootstrap storage service. This issue will need tests once the direction is stable.
Comment #49
msonnabaum commentedHere's a version of #41, but with an incrementCounter method that actually increments the value.
This makes it clear that we actually need more than just a dumb k/v for a bootstrap storage interface, since we'll want the increment to be atomic. It may need to be an interface purely for this task, or maybe a counter service or something.
I'm not sure that there is a way to atomically increment that works on mysql and postgres, but the version I used I think may work, I didnt test it.
It also adds an increment during module install so I could test the basic functionality. Implementation is super ugly, but we dont seem to have any reasonable API for "something changed that requires a rebuild".
Comment #50
chx commentedThat's good standard SQL why it wouldn't? http://www.postgresql.org/docs/8.3/static/sql-update.html
Same page has example queries like
UPDATE weather SET temp_lo = temp_lo+1...UPDATE employees SET sales_count = sales_count + 1Comment #51
chx commentedCore has
so why are we not using this and a raw query instead...? Performance? But this is not on a performance critical path...?
Comment #52
msonnabaum commentedI did not know about expression.
Comment #53
msonnabaum commentedRelated, but not dependent issue I started to provide a more reasonable interface for invalidation: #2493665: Add centralized container invalidation method
Comment #54
Anonymous (not verified) commentedi'll have a go at addressing #51 tonight.
Comment #55
Anonymous (not verified) commentedok, here goes nothing.
Comment #56
jhedstromThe approach of a 'coordinator' class, or a bootstrap storage service still hasn't been fully decided. The patch in #55 is the 'coordinator' approach, and the patch in #48 was an initial attempt at the new bootstrap service.
Since either approach is overridable by contrib that doesn't want to use the database, I don't have a preference one way or the other.
Comment #57
Anonymous (not verified) commentedre. #56 - ack on the different approaches. i don't have a strong feeling either way.
should i create something based off #48 as well? does anyone else want to way in?
Comment #58
msonnabaum commentedI'm not clear on the vision for the "bootstrap storage" version. Replacing the service at the level that we're using PhpStorage seems like the wrong place to me.
An interface with get/set isn't enough to express what's going on here. We need something with an increment method so that a backend can perform the increment atomically.
When I mentioned I was fine with the idea of a bootstrap service, I was thinking we'd have a service that maybe only handled the counter itself.
Please elaborate on the other approach if I got any of that wrong.
Comment #59
fabianx commentedI think if we call it the AtomicCounter component, we are fine with the Coordinator approach, just before when it was just get/set I did not see why we should not use KeyValue directly.
Comment #60
chx commentedIf I were writing this (thanks god i am not) I would add an IncrementableKeyValueStore interface (we planned another such interface at #2161643: Add a KeyValueStore\IterableKeysInterface (allowing to retrieve keys with a given prefix) ) and just have the normal k-v class implement it.
Comment #61
jhedstromAfter talking on IRC with msonnabaum and Fabianx, I'm currently pursuing the following:
AtomicCounterInterface. It shouldn't conceptually be tied to k/v since some backends such as Redis have dedicated operations for countersComment #62
jhedstromUsing the
DatabaseStoragek/v class seems a little messy:but aside from that, this seems to be working. Interdiff is from #55.
Comment #65
Anonymous (not verified) commentedyay, #62 looks good to me.
Comment #66
berdirDidn't review yet, can we maybe use something like this also for a rebuid-lock to help with #2497243: Replace Symfony container with a Drupal one, stored in cache?
Comment #67
Crell commentedCan we specify what an atomic counter is? I'm assuming it's unrelated to the resonant frequency of cesium...
Why can't this just be a merge query, which does the either/or logic for us? Doing the update in a catch block is very slow, because that's presumably the typical case. This would need to hit the DB, get a DB fault, create an exception (non-cheap), and then do a second DB connection. Totally wasteful.
Or, better still:
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Database%...
(Even if we can't use nextId directly, there was a ton of discussion around how to do that performantly that is relevant here.)
Comment #68
jhedstromThis switches the try/catch to use a merge instead of an update (and that is attempted first). We still need to catch the exception and use an insert for the initialization of the row in the db, since merge doesn't appear to work with expressions when a row isn't found. I added a very simple test as well.
Comment #69
jhedstromOne thing this patch doesn't account for is cleaning up the invalidated directories that will be left in place.
Comment #70
Anonymous (not verified) commentedre. #67 and #68 - please don't use merge() if we have to follow it with an insert().
we are doing this because we want to handle multiple processes running this code at the same time. as such, any implementation that runs the insert last is unsafe, because some process will win, and the others will throw an exception.
i'm interested in my merge() fails. jhedstrom - do you think that's a bug in the underlying db code? or is this a case of 'works as designed, don't do that'?
Comment #71
jhedstromThe failure is actually an SQL exception directly from MySQL, so I'd guess it is the underlying implementation, and also backend-specific.
Do folks have thoughts on how we can go about cleaning out the stranded container directories (and twig caches if it is used for that)?
Comment #72
jhedstromThis is the actual error:
Manually doing an insert with an expression on MySQL works fine:
However, I don't think addressing the internal implementations of merge queries should block this issue.
Comment #73
jhedstromThat being said, here's the switch back to using update instead of merge.
Comment #74
jhedstromThis adds a unit test similar to the other phpstorage classes. Note I had to do the goofy test counter class rather than using a proper mock object for testing.
This test can be updated to ensure that leftover container directories are properly removed (eg, simulating invalidation across multiple webheads where only one directory is removed from a single webhead).
We also might want to change
the hard-coded
service_container_counterso this can be used for other storage (eg, Twig templates).Comment #75
berdirIs everyone here aware of #2497243: Replace Symfony container with a Drupal one, stored in cache? AFAIK, the currently proposed solution there would solve that problem on it's own, because container would be stored in a synchronized storage, so this problem would simply no longer exist...
That issue definitely needs testing/feedback, so it would be great if people could review/comment/test the patches there...
Comment #76
jhedstromRe: #75
I think this issue may still be necessary for other shared invalidations (I don't know enough about twig caching offhand to know if it is safe currently to store those locally on separate webheads w/o centralized invalidations).
Comment #77
fabianx commentedTwig should just use a chained fast cache backend with fast == phpstorage 'twig' backend, then it gets cache tag and coordinated invalidation for free. We can also properly invalidate twig templates then (that still is very buggy sometimes ...) and ensure once we have them cached in the DB we just need to put them to disk on other web heads => faster startup times.
=> Overall win (And as I am author of the original twig implementation, this is what I would use _today_ if I were to integrate twig now.)
Edit: Hmm, maybe I would not store the PHP code in consistent backend (PHP Code in DB? What have I been thinking ...), but just using cache tags and invalidate on any write should work and probably the default implementation for PHP Storage Cache backend anyway ...
Comment #78
chx commentedSee #2497243-81: Replace Symfony container with a Drupal one, stored in cache for a writeup + code. I think that'd solve this neatly. Perhaps switch the K-V store to cache?
Comment #79
chx commentedSo, going 5.5 gives us a very small window to actually read the container from cache and yet run it through opcache. To see my code actually does get the container into opcache, add in CacheStorage::load just after the include but before the stream_wrapper_restore
print_r(opcache_get_status(TRUE)['scripts']["phar://$name"]);. It is impossible to test this from CLI since opcache.enable_cli is not enabled and doesn't look runtime changeable either. But, the dump will show you it lives happily in cache and the hit counter grows on every reload. Huzzah.Yes, the patch makes baby jesus cry but please file your complaints at those who hardwired the opcacheable wrappers. As I said: the window is very small.
Regarding cache expire, I checked everything under the sun and they all set to microtime like http://cgit.drupalcode.org/memcache/tree/src/MemcacheBackend.php?h=8.x-2... memcache does and all core backends do. So we are good there.
Although this patch uses the SQL cache backend it's waaaaaay easier than the patches above to just swap in memcached at will.
Comment #81
chx commentedAfter discussing with beejeebus I am separating this out to #2513326: Performance: create a PHP storage backend directly backed by cache .
Comment #82
anavarreShould this issue be postponed until #2513326: Performance: create a PHP storage backend directly backed by cache gets in?
Comment #83
msonnabaum commented@anavarre - No.
Comment #84
chx commentedI am not even sure what this patch solves since it extends mtime storage which uses the files directory which will typically be shared disk and not local disk? it should just work off the base class IMO.
Comment #85
chx commentedRe #84 -- I get it -- if you point the directory to a local dir then it works.
Per https://www.drupal.org/node/2547827#comment-10213679 this is unlikely to be committed for now because core is being changed not to require coordination across webheads. and so I am moving over to 8.1.x-dev .
Comment #87
jhedstromBeen a while since this was touched. Tagging as needing an IS update with what remains to be done.
Comment #88
cameron tod commentedReroll
Comment #89
cameron tod commentedComment #90
catchA lot of this got resolved by moving the container out of PHP, and by #2544932: Twig should not rely on loading php from shared file system.
PhpStorage itself doesn't handle multiple web-heads, but core does now. Dropping priority for now.
Comment #91
cameron tod commentedGiven that core is storing the container in the database by default, do we want to update this settings.php comment to explain that, and why you might want to move the container to file storage?
Comment #92
cameron tod commentedComment #93
cameron tod commentedComment #94
wim leers#90 is right, but there's one big caveat.
#90 is right in that:
BUT! It doesn't yet take Twig template modifications are detected. And rightly so, because it'd mean requesting the Twig template's mtime or reading its contents on every request. That doesn't work. We specifically rely on compiled Twig templates, i.e. PHP classes, to avoid much of the work there.
This means that whenever you modify Twig templates (including when you deploy code), you need to clear Twig's PHP storage directory! This is also what
drupal_flush_all_caches()does:So if you forget to clear Twig's PHP Storage cache on all webheads, you may end up using old (stale) compiled Twig templates! We should document this more clearly.
Comment #95
wim leersComment #96
fabianx commentedI had thought we had included the deployment identifier in the twig hash, but if not we indeed should certainly add it.
Comment #97
dawehnerI had a quick look at it and it doesn't seem to be that we use the deployment identified explicitly for twig nor for all php storage things. For twig we argued that its enough to vary by twig extensions.
Comment #98
wim leersIt's not, unfortunately. When Twig is stored in PhpStorage, and PhpStorage is specific to each webhead, then you need to wipe each web head's PHP storage after each deployment. Because each deployment may modify Twig templates, but if a prior version of the Twig template is already compiled, then the
class_exists()logic in\Twig_Environment::loadTemplate()will see that the template is already compiled, and just use it.Including the deployment identifier in the Twig template class name would definitely fix that.
Varying only by Twig extensions means that only new Twig extensions or updated Twig extensions cause Twig templates to be recompiled. And updating Twig extensions is of course orders of magnitude more rare than updating Twig templates.
Comment #99
dawehnerOH yeah no question, I was just saying what we used to think.
Btw. I'm not actually sure we need our own custom logic to deal with twig extensions. There is
\Twig_Environment::getTemplateClassas well, which seems to deal with it already.One tricky bit is: How do you know as part of your deployment that any twig file changed. Otherwise we don't want to recompile those templates.
Is there anything we can help with?
Comment #100
wim leersWell, that is the downside. We'd always recompile those templates. But only for those sites that actually use deployment identifiers. Would that really be a problem? It's a bigger problem when stale compiled templates are used, which is the problem we have today.
Correctness first, performance second.
The only alternative I see is to provide an official "build step script" for Drupal 8, which allows you to compile all Twig templates ahead of time, commit them, and deploy them, as code. That seems like a much bigger leap.
What do you mean?
Comment #101
moshe weitzman commentedFor that compiles script, see https://github.com/drush-ops/drush/blob/7f59c15d18daff5552f00c2c6ea9c57f.... Would need a little more work for core.
Comment #102
dawehnerWell at least store the timestamps of every template and in people writing build scripts, provide them these timestamps, for example.
Comment #103
fabianx commentedSo in theory we could have cake and eat it, but it is a little tricky implementation and I am not sure the current structure of FileSystemLoader allows it:
We have the timestamp to check if the PHP template matches.
Currently we only do check the timestamp if $this->autoRefresh == TRUE.
However we could check the timestamp if we have a cache miss with the rebuild identifier.
e.g. we store the invalidation key inside of the class and do cache re-validation instead of invalidation.
Tricky, but would take care of the concern.
On the other hand: Correctness and KISS first ...
Comment #104
wim leers@msonnabaum opened #2752961: No reliable method exists for clearing the Twig cache to focus on the most important subset of this issue: the aspect that #94 describes, i.e. Twig.
Comment #115
fgmSince #2752961: No reliable method exists for clearing the Twig cache was fixed one year ago, maybe it's time to revisit this ?
Comment #119
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.