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

s3 : use cache for listing object #365

Open
tristanbes opened this Issue Jul 25, 2018 · 8 comments

Comments

Projects
None yet
3 participants
@tristanbes

tristanbes commented Jul 25, 2018

Hello,

We run a very large multi-tenant ecommerce site where we have all our client images under Amazon S3.

Each time they use the CKFinder (PHP), it run multiple getObject/listObject commands (which are not free).
Just for this month we have

  • 14,214,783 Requests for PUT/COPYPOST/LIST requests
  • 120,687,341 Requests for GET requests. (of course not all of them are related to CKfinder).

Wouldn't it be possible to plug some caching system (Memcache, Redis...) to avoid spam Amazon s3 service with those request and try to serve the file from the cache (with some cache invalidation system and a button to force reload the list from the provider) ?

Regards,
Tristan.

@zaak

This comment has been minimized.

Member

zaak commented Jul 25, 2018

Hi @tristanbes! Yes, adding caching layer is possible and should be fairly simple. CKFinder PHP connector uses Flysystem abstraction layer under the hood, so there are ready to use adapters that decorate regular adapter with caching layer: https://flysystem.thephpleague.com/docs/advanced/caching/#persistent-caching.

The recommended way to do that in CKFinder connector is by registering a custom backend adapter inside plugin, as follows:

<?php
// ckfinder/plugins/CustomCachingPlugin/CustomCachingPlugin.php

namespace CKSource\CKFinder\Plugin\CustomCachingPlugin;

use CKSource\CKFinder\CKFinder;
use CKSource\CKFinder\Plugin\PluginInterface;
use Aws\S3\S3Client;
use CKSource\CKFinder\Backend\Adapter\AwsS3 as AwsS3Adapter;
use League\Flysystem\Cached\Storage\Predis as PredisStore;

class CustomCachingPlugin implements PluginInterface
{
    public function setContainer(CKFinder $app)
    {
        $backendFactory = $app->getBackendFactory();

        $backendFactory->registerAdapter('cached_s3', function ($backendConfig) use ($backendFactory) {
            // Regular S3 adapter instantiation.
            $clientConfig = [
                'credentials' => [
                    'key'    => $backendConfig['key'],
                    'secret' => $backendConfig['secret']
                ],
                'signature_version' => isset($backendConfig['signature']) ? $backendConfig['signature'] : 'v4',
                'version' => isset($backendConfig['version']) ? $backendConfig['version'] : 'latest'
            ];

            if (isset($backendConfig['region'])) {
                $clientConfig['region'] = $backendConfig['region'];
            }

            $client = new S3Client($clientConfig);

            $filesystemConfig = [
                'visibility' => isset($backendConfig['visibility']) ? $backendConfig['visibility'] : 'private'
            ];

            $prefix = isset($backendConfig['root']) ? trim($backendConfig['root'], '/ ') : null;

            $s3Adapter = new AwsS3Adapter($client, $backendConfig['bucket'], $prefix);

            // Define cache storage to use for S3 adapter
            $predisClient = new Predis\Client(); // Alter this line to configure proper Redis connection
            $predisCacheStore = new PredisStore($predisClient);

            return $backendFactory->createBackend($backendConfig, $s3Adapter, $filesystemConfig, $predisCacheStore);
        });
    }

    public function getDefaultConfig()
    {
        return [];
    }
}

Don't forget to enable the plugin in CKFinder's config.php:

$config['plugins'] = ['CustomCachingPlugin'];

The plugin presented above registers a new adapter type named cached_s3. You can use it just like regular s3 backend adapter, but intead s3 you should use cached_s3 registered in the plugin:

$config['backends'][] = array(
    'name'         => 'S3',
    'adapter'      => 'cached_s3',
    // ...

Please also have a look at following articles in PHP docs:

Feel free to ask if anything is still unclear.

@tristanbes

This comment has been minimized.

tristanbes commented Jul 25, 2018

Ok thanks, I'll put someone on my team on this subject, we'll comeback to you if any questions. Thank you for this detailled answer.

@thiphamyp

This comment has been minimized.

thiphamyp commented Jul 26, 2018

Hello,

The cache works. Thank you!

However what is the procedure for updating the cache without recalling listing s3 (or listContents of Flysystem)?

@zaak

This comment has been minimized.

Member

zaak commented Jul 26, 2018

Hi @thiphamyp.

By default s3 will be checked in case of cache miss. Cache miss occurs if object is not in cache yet, or if cache under given key has expired - you can pass cache lifetime in 3rd parameter of PredisStore: https://github.com/thephpleague/flysystem-cached-adapter/blob/master/src/Storage/Predis.php#L29

I'm not sure if the above answers your question, so here's another approach:
PredisStore implements CacheInterface, which contains a couple methods that allow to tinker with cache directly - save, delete, update data in cache. You can save the reference to created PredisStore object and use it to talk to the cache.

@thiphamyp

This comment has been minimized.

thiphamyp commented Jul 27, 2018

Hi @zaak ,

In fact, version 3.4.2 (PHP) File browser is up to date after deleting, renaming, ... but not after uploading

I found these codes below in CKSource\CKFinder\Filesystem\File\DeleteFile, CopiedFile, RenamedFile, MovedFile but not in UploadedFile.

ex:
$this->getCache()->delete(Path::combine($this->resourceType->getName(), $this->folder, $this->getFilename()));
$this->getCache()->copy(...);
$this->getCache()->move(...));

Would it be possible to add this to UploadedFile?

Thanks

@zaak

This comment has been minimized.

Member

zaak commented Jul 27, 2018

Hi @thiphamyp.

I'm sorry, but I'm not sure if I understand your question. The code you pasted operates on internal CKFinder connector metadata cache (internal image thumbnails etc), it has not much to do with filesystem cache we were discussing earlier.

Could you please explain the issue in more detail?

@thiphamyp

This comment has been minimized.

thiphamyp commented Aug 2, 2018

Hi @zaak ,

The problem is with the cache, the image is not display on list after uploading (FileUpload). The list of image is updated with DeleteFiles, RenameFile, ... using filesystem cache

It seems to me that updateObject of CacheInterface is not be called by putStream of FileUpload that is not existent in CachedAdapter. There are only writeStream and updateStream

Regards,

@zaak

This comment has been minimized.

Member

zaak commented Aug 2, 2018

Hi @thiphamyp.

Thanks for the details, I can reproduce this issue now. This happens because the previous response from S3 gets cached and CKFinder does not receive info about newly uploaded files when calls GetFiles command again, just after the file is uploaded. To fix this, it's enough to flush the cache just before the file is uploaded, like this:

<?php
// ckfinder/plugins/CustomCachingPlugin/CustomCachingPlugin.php

namespace CKSource\CKFinder\Plugin\CustomCachingPlugin;

use CKSource\CKFinder\CKFinder;
use CKSource\CKFinder\Event\BeforeCommandEvent;
use CKSource\CKFinder\Event\CKFinderEvent;
use CKSource\CKFinder\Plugin\PluginInterface;
use Aws\S3\S3Client;
use CKSource\CKFinder\Backend\Adapter\AwsS3 as AwsS3Adapter;
use League\Flysystem\Cached\Storage\Predis as PredisStore;

class CustomCachingPlugin implements PluginInterface
{
    /**
     * @var PredisStore
     */
    private $predisCacheStore = null;

    public function __construct()
    {
        // Define cache storage to use for S3 adapter
        $predisClient = new \Predis\Client('tcp://127.0.0.1');
        $this->predisCacheStore = new PredisStore($predisClient);
    }

    public function setContainer(CKFinder $app)
    {
        $backendFactory = $app->getBackendFactory();

        $backendFactory->registerAdapter('cached_s3', function ($backendConfig) use ($backendFactory) {
            // Regular S3 adapter instantiation.
            $clientConfig = [
                'credentials' => array(
                    'key'    => $backendConfig['key'],
                    'secret' => $backendConfig['secret']
                ),
                'signature_version' => isset($backendConfig['signature']) ? $backendConfig['signature'] : 'v4',
                'version' => isset($backendConfig['version']) ? $backendConfig['version'] : 'latest'
            ];

            if (isset($backendConfig['region'])) {
                $clientConfig['region'] = $backendConfig['region'];
            }

            $client = new S3Client($clientConfig);

            $filesystemConfig = [
                'visibility' => isset($backendConfig['visibility']) ? $backendConfig['visibility'] : 'private'
            ];

            $prefix = isset($backendConfig['root']) ? trim($backendConfig['root'], '/ ') : null;

            $s3Adapter = new AwsS3Adapter($client, $backendConfig['bucket'], $prefix);

            return $backendFactory->createBackend($backendConfig, $s3Adapter, $filesystemConfig, $this->predisCacheStore);
        });

        $app->on(CKFinderEvent::BEFORE_COMMAND_FILE_UPLOAD, function (BeforeCommandEvent $event) {
            $this->predisCacheStore->flush();
        });
    }

    public function getDefaultConfig()
    {
        return [];
    }
}

This is a modified version of the plugin which registers a listener for CKFinderEvent::BEFORE_COMMAND_FILE_UPLOAD event, so its logic will be executed just before the file upload is processed. Please note that in this example a whole cache is flushed - you might want to make this logic a bit more optimized and flush from cache only the current folder path.

I hope this helps!


After some more detailed testing I see that CachedAdapter with PredisStore sometimes indeed don't work like expected. The best would be probably extending the adapter/store and altering the required methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment