-
-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Symfony's capability to run on read-only out of the box #21
Comments
Thank for you reporting this, that is consistent to what I have seen. I would definitely go the route of keeping the cache that we deployed (without copying/symlinking). I'm not sure I understand what you mean by "it might be a much more elegant idea to patch the system and filesystem cache and not copy anything", but it's late for me ^^ Do you mean that: all cache should stay in |
What I mean is a custom filesystem cache adapter that is aware that the target directory might not be writable and creates an overlay. I have quickly thown this together (untested and probably not the most beautiful): use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
class OverlayFilesystemAdapter extends AbstractAdapter
{
/**
* @var FilesystemAdapter
*/
private $base;
/**
* @var FilesystemAdapter|null
*/
private $overlay = null;
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null)
{
parent::__construct($namespace, $defaultLifetime);
$this->base = new FilesystemAdapter($namespace, $defaultLifetime, $directory, $marshaller);
// if the directory is not writable, create an overlay
if ($directory !== null && !is_writeable($directory)) {
$this->overlay = new FilesystemAdapter($namespace, $defaultLifetime, null, $marshaller);
}
}
protected function doClear(string $namespace)
{
if ($this->overlay !== null) {
throw new \LogicException("Can't clear overlayed filesystem cache.");
}
return $this->base->doClear($namespace);
}
protected function doDelete(array $ids)
{
if ($this->overlay !== null) {
throw new \LogicException("Can't delete overlayed filesystem cache.");
}
return $this->base->doDelete($ids);
}
protected function doFetch(array $ids)
{
$values = $this->base->doFetch($ids);
if ($this->overlay !== null) {
$values = array_replace($values, $this->overlay->doFetch($ids));
}
return $values;
}
protected function doHave(string $id)
{
if ($this->overlay !== null) {
if ($this->overlay->doHave($id)) {
return true;
}
}
return $this->base->doHave($id);
}
protected function doSave(array $values, int $lifetime): array
{
if ($this->overlay !== null) {
$this->overlay->doSave($values, $lifetime);
return; // don't save to the base implementation
}
return $this->base->doSave($values, $lifetime);
}
} This could be configured in place of the normal cache adapter # config/packages/cache.yaml
framework:
cache:
system: cache.adapter.overlay_filesystem # the hypothetical overlay adapter
app: cache.adapter.filesystem # still the default filesystem adapter for max compatibility
directory: '/tmp/pools' # but write app cache into /tmp since there is normally nothing warmed in it |
Just copying the pools folder is what we do right now. In fact we probably have the most elegant implementation possible without overwriting private details or defining another cache adapter. The problem is that the pools directory is hardcoded in the system cache definition based on the That said, we could also recommend defining the system cache as a normal filesystem cache like this: # config/packages/cache.yaml
framework:
cache:
system: cache.adapter.filesystem # instead of the default cache.adapter.system
app: cache.adapter.filesystem # this is the default
directory: '%env(CACHE_POOL_DIR)%' # something that is "%kernel.cache_dir%/pools" during warmup but "/tmp/pools" in the lambda environment
# because the system cache is now using the default filesystem adapter, it is also influenced by the directory option Then we could copy just that one directory during boot. An overlay implementation would remove the need to copy during boot but introduce a slight runtime overhead. Now going back to the current implementation: The nice thing is that it is fairly easy to explain how to setup because there is no config file editing needed and works with or without pre-warming and it has nearly no runtime overhead compared to any overlay implementation besides the longer boot time. So just to summarize my thoughts:
|
I did some more research. The smallest solution i found to have no cache write fail (in my app) while deploying as much cache as possible is this: framework:
cache:
# those 2 options are the default, but it helps understanding
system: cache.adapter.system
app: cache.adapter.filesystem
# make it possible to move the app cache dir out of the "%kernel.cache_dir%/pools"
# important: the "cache.adapter.system" is hardcoded to "%kernel.cache_dir%/pools"
directory: '/tmp/%kernel.environment%_pools' # only influences the filesystem adapter
pools:
# overwrite some system caches which are not properly warmed
# those components will now write to the "cache.app" which is writable
cache.property_info: ~
cache.serializer: ~
cache.validator: ~ I defined the 3 pools which shadow the private implementations of those 3 components because the pools option defines a service with the same name. Those caches will then no longer write into the Again, It relies on the private service definition of those components. It also isn't perfect because i throw away some cache entries of the validator because the cache isn't fully warmed. But it's still interesting to see which components create the problems. Although I wouldn't recommend it, it's pretty damn elegant by being small. |
The more I dig, the more i find that you two (@Nyholm, @mnapoli) were already on it long before me 😉 More information about cache the problematic cache warmersIt seems that the annotation warmer looks into every file in the composer classmap: The validator and the serializer component, on the other hand, does only warm xml or yaml definitions. The property info component has no warmer at all (or i didn't find it). So contributing to symfony would require to build a cache warmer for the property-info component and extending the validator and serializer warmers to check all classes similar to the validator warmer. |
Good to know! That is definitely something that we can document.
Just to be sure I understand, does that mean that these components will not be compatible with a read-only cache, even if it is warmed before deploying? |
@mnapoli Correct. The warmers would need to be improved to work similar to the annotation warmer. But you can move these specific caches into |
OK I think I'm starting to get it, thanks :) At the moment, you identified 3 components that write to the cache at runtime. In the end, there might be more than that. Would it make sense to:
? |
Good idea~
But we can just redefine cache.system as a normal filesystem cache. This doesn't even need to be aware of private implementation details. framework:
cache:
system: cache.adapter.filesystem # this is the important part
app: cache.adapter.filesystem
directory: '/tmp/%kernel.environment%_pools' There is still a lot of pre warmed cache, not just the compiled container. There are also twig templates, translations and doctrine proxies. I just ran that though the same benchmark i just wrote in #18 (comment) with php 7.4 and preload enabled.
So I don't know how much my app uses the pre-warmed cache in that case but i assume the annotation cache is probably the biggest hit. But the hit is smaller than the hit of copying the |
Right!
Yes, I like when it's simple and stable ^^ At least for a first version that we can release and test with. We can still experiment around these ideas, but we'll have a stable baseline that people can start using and that we can compare to. |
Now instead of blacklisting specific caches, i can also whitelist so i know that the annotation cache works fine so... framework:
cache:
# [...]
pools:
cache.annotations:
adapter: cache.adapter.system
Does not seem to make a significant difference in my 1 test at ~ |
I kind of started this library by positing my code which copies cache files.
But #17 (comment) made me check again and realise that symfony (4.4) works without modifications if the cache is properly warmed and configured.
So I investigated.
setup
I use a symfony multipage application which i'm not totally comfortable to share (it is a real project) but its a symfony 4.4 skeleton with doctrine/orm and a user entity in a crud for user as well as some email handling
I changed the following files for logging:
And for completeness I changed the cache directory:
It's important that the directory option only changes the directory of the
cache.adapter.filesystem
(defined here) and not thecache.adapter.system
which is the cache we warm.I also note that I test using symfony 4.4.7 with php 7.3 so not the newest setup but a realistic one.
result
It seems to work ok.
I have a lot of warnings in the log from not being able to write like these:
These warnings appear when a form is accessing an Entity. I found this issue talking about it.
Other than that I can't find any immediate issues. The console works fine too.
So what's are we doing here?
When I started using symfony I could not get my project to work on a read-only filesystem (I don't know why anymore). I used the documentation which had the rewriting to the
/tmp
folder in it but that was too slow for my taste so I started to copying the cache to tmp with some exception wich is the state this library is at right now.Now that I know that symfony almost works out-of-the-box I would probably choose a different path.
One could overwrite/decorate the
cache.adapter.system
service (maybe even in a normal bundle) to include overlay logic and to my current knowledge it would work perfectly.Now there are still advantages to the copy approach here: It works in every case, even when the cache isn't warmed or isn't properly warmed. It even works in dev mode with profiler as long as you didn't deploy the
profiler
folder. Even though you will get problems when multiple lambda instances are running there. The question is if that is important enough.read-only-bundle
which writes tosys_get_temp_dir
.Opinions? Did I overlook something?
The text was updated successfully, but these errors were encountered: