multi-tenant: Driver [redis_tenancy] is not supported.

Hi,

related to Make cache handling better for tenants #309 I get the issue of getting the error Driver [redis_tenancy] is not supported. Currently I setup my own CI/CD process with local docker (base on php:7.1.8-apache), gitlab and production server. This seems to be related to some autogenerated loading of composer or so as it seems nearly to be random when it works and when not.

grafik

  • hyn/multi-tenant version: hyn/multi-tenant 5.3.1
  • laravel version: laravel/framework v5.6.39
  • webserver software and version: apache
  • php version: PHP 7.1.8 (cli) (built: Aug 4 2017 18:55:44) ( NTS )

Composer:

    "extra": {
        "laravel": {
            "dont-discover": [
				"hyn/multi-tenant",
				"spatie/laravel-permission"
            ]
        },

config/app

     /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        ...

	// Needs to be before Hyn\Tenancy: https://github.com/hyn/multi-tenant/issues/309#issuecomment-401509408
	App\Providers\AppServiceProvider::class,
	App\Providers\CacheServiceProvider::class,		
		
        /*
         * Package Service Providers...
         */	   
        Hyn\Tenancy\Providers\TenancyProvider::class,
        Hyn\Tenancy\Providers\WebserverProvider::class,
        Spatie\Permission\PermissionServiceProvider::class,
        ...

        /*
         * Application Service Providers...
         */
        App\Providers\HelperServiceProvider::class,        
        App\Providers\AuthServiceProvider::class,
        App\Providers\ComposerServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,	    
       ...

config/cache.php

return [
    'default' => env('CACHE_DRIVER', 'file'), // in env = redis
    'stores' => [
        ...
        'redis' => [
            'driver' => 'redis_tenancy', //'redis', 'redis_tenancy'
            'connection' => 'default',
        ],
    ],
    'prefix' => env(
        'CACHE_PREFIX',
        str_slug(env('APP_NAME', 'laravel'), '_').'_cache'
    ),
];   

CacheServiceProvider:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Cache\RedisStore;

class CacheServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {	

        // https://github.com/hyn/multi-tenant/issues/309
		Cache::extend('redis_tenancy', function ($app) {
       		if (PHP_SAPI === 'cli') {
	            $uuid = $app['config']['driver'];
	        } else {
	            // ok, this is basically a hack to set the redis cache store
	            // prefix to the UUID of the current website being called
	            $fqdn = $_SERVER['SERVER_NAME'];
            
	            $uuid = DB::table('hostnames')
	                ->select('websites.uuid')
	                ->join('websites', 'hostnames.website_id', '=', 'websites.id')
	                ->where('fqdn', $fqdn)
	                ->value('uuid');
	        }

	        return Cache::repository(new RedisStore(
	            $app['redis'],
	            $uuid,
	            $app['config']['cache.stores.redis.connection']
	        ));
	    });	
    }

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
	
    }
}

Any idea what is going wrong here? I think the boot of CacheServiceProvider is not loaded early enough, but how to change this?

While writing this, I located this: https://stackoverflow.com/a/53263914/1597218 The order in the config/app is not used as expected. In my case the order is the following:

  1. Illuminate
  2. 3rd Party (which may will use Cache as well)
  3. Rest of app/config including CacheServiceProvider, Hyn & App\

"According to the Application::registerConfiguredProviders it’s hardcoded to have everything that starts with 'Illuminate' to go to the starting chunk, all others to the end, and Composer Packages go in the middle. "

Even so I add all missing 3rd Party provider into the app/config manually, the Application::registerConfiguredProviders list the providers twice. So one option would be that this package add this provider to the default settings, as vendor files are loaded before app providers or alternativly adding a customIlluminate namespace.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 24 (2 by maintainers)

Most upvoted comments

Here are my thoughts why the current documentation is not working. At least sometimes. Making it extremely hard to find out the the solution.

I use Laravel 5.8 and hyn/tenancy 5.4.*

Tenant aware cache will not work with file or database driver in general case. At least it will not work if you try to use 'Telescope`. More on this below.

When any package calls cache for the first time in the app lifecycle, the Cache object is instantiated. And the directory/prefix of the cache is passed to the cache object.

E.g. ./vendor/laravel/framework/src/Illuminate/Cache/FileStore.php

    public function __construct(Filesystem $files, $directory)

./vendor/laravel/framework/src/Illuminate/Cache/RedisStore.php

    public function __construct(Redis $redis, $prefix = '', $connection = 'default')

./vendor/laravel/framework/src/Illuminate/Cache/DatabaseStore.php

    public function __construct(ConnectionInterface $connection, $table, $prefix = '')

For all (or at least many) auto-discovered packages Cache is instantiated before the application calls our App\Providers\CacheServiceProvider::boot to get our Cache::extend rules. So Cache::extend is ignored and we stay at the cache root for all tenants, we are not able to namespace cache dynamically.

Anything calling Cache::remeber instantiates the Cache object as it’s configured in config/cache.php and freezes the prefix/path values inside the Cache object.

Here we can try to run our App\Providers\CacheServiceProvider::boot and \Cache::extend() before anything instantiates a Cache object. We may try to disable auto-discover for extensions which instantiate Cache, manually add all needed service providers into config/app.php.

And this may work until you try to run telescope debug tool, which must be run as early as possible to catch all possible activity. But telescope instantiates Cache object before anything else happens, before we change the instantiate rules in App\Providers\CacheServiceProvider.

Fortunately Illuminate\Cache\RedisStore class (but not Illuminate\Cache\FileStore or Illuminate\Cache\DatabaseStore) has setPrefix methods. So we can redefine the prefix any time, after telescope has already instantiated the cache object.

So finally to get cache tetant aware we must:

  • Switch to redis cache driver in .env
CACHE_DRIVER=redis
  • Use @hakanersu approach, create own CacheServiceProvider
<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class CacheServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
                if (PHP_SAPI === 'cli') {
                    $namespace = str_slug(env('APP_NAME', 'laravel'), '_').'_cache';
                } else {
                    $fqdn = request()->getHost();
                    $namespace = \DB::table('hostnames')
                        ->select('websites.uuid')
                        ->join('websites', 'hostnames.website_id', '=', 'websites.id')
                        ->where('fqdn', $fqdn)
                        ->value('uuid');
                }
                \Cache::setPrefix($namespace);
    }
}
  • Register the CacheServiceProvider in config/app.php providers section before AppServiceProvider. Depending on the way you use hyt/tenancy you have have some code like $env = app(Environment::class); in AppServiceProvider::boot which will instantiate the Cache object. So better place CacheServiceProvider before.
        /*
         * Application Service Providers...
         */
        App\Providers\CacheServiceProvider::class,
        App\Providers\AppServiceProvider::class,

After that cache should become tenant aware.

=====

If you are pretty sure you are not going to use anything like telescope instantiating Cache object early, you may try to use file or database cache driver.

Here the steps are different:

  • Choose file or database cache driver in .env

CACHE_DRIVER=file or CACHE_DRIVER=database

  • Disable hyn/tenancy auto-discovery

Update composer.json like this

    "extra": {
        "laravel": {
            "dont-discover": [
                "hyn/multi-tenant"
            ]
        }
    },

and run composer install to apply changes.

  • In config/app.php register providers likes this:
        /*
         * Application Service Providers...
         */
        App\Providers\CacheServiceProvider::class,
        Hyn\Tenancy\Providers\TenancyProvider::class,
        Hyn\Tenancy\Providers\WebserverProvider::class,
        App\Providers\AppServiceProvider::class,
  • And finally add our CacheServiceProvider
<?php

namespace App\Providers;

use Illuminate\Cache\FileStore;
use Illuminate\Cache\DatabaseStore;
use Illuminate\Support\ServiceProvider;


class CacheServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        $namespace = function($app) {

            if (PHP_SAPI === 'cli') {
                return $app['config']['cache.default'];
            }
            
            $fqdn = request()->getHost();

            $uuid = \DB::table('hostnames')
                ->select('websites.uuid')
                ->join('websites', 'hostnames.website_id', '=', 'websites.id')
                ->where('fqdn', $fqdn)
                ->value('uuid');

            return $uuid;
        };
        
        $cacheDriver = config('cache.default');
        switch ($cacheDriver) {
            case 'file':
                \Cache::extend($cacheDriver, function ($app) use ($namespace){
                    $namespace = $namespace($app);
        
                    return \Cache::repository(new FileStore(
                        $app['files'],
                        $app['config']['cache.stores.file.path'].$namespace
                    ));
                });
                break;
            case 'database':
                \Cache::extend($cacheDriver, function ($app) use ($namespace){
                    $namespace = $namespace($app);
        
                    return \Cache::repository(new DatabaseStore(
                        $app['db.connection'],
                        'cache',
                        $namespace
                    ));
                });
                break;
            case 'redis':
                // But if not yet instantiated, then we are able to redifine namespace (prefix). Works for Redis only
                if (PHP_SAPI === 'cli') {
                    $namespace = str_slug(env('APP_NAME', 'laravel'), '_').'_cache';
                } else {
                    $fqdn = request()->getHost();
                    $namespace = \DB::table('hostnames')
                        ->select('websites.uuid')
                        ->join('websites', 'hostnames.website_id', '=', 'websites.id')
                        ->where('fqdn', $fqdn)
                        ->value('uuid');
                }
                \Cache::setPrefix($namespace);
                break;
            default:
        }
    }
}

@luceos Please, have look to this post. Maybe it’s worth to update the docs.

What about this, without changing provider just change prefix?

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class CacheServiceProvider extends ServiceProvider
{
    public function boot()
    {
        if (PHP_SAPI === 'cli') {
            $uuid = str_slug(env('APP_NAME', 'laravel'), '_').'_cache';
        } else {
            $fqdn = request()->getHost();
            $uuid = \DB::table('hostnames')
                ->select('websites.uuid')
                ->join('websites', 'hostnames.website_id', '=', 'websites.id')
                ->where('fqdn', $fqdn)
                ->value('uuid');
        }
        \Cache::setPrefix(str_slug($uuid, '_').'_cache');
    }
}
Illuminate\Cache\CacheServiceProvider::class,
App\Providers\CacheServiceProvider::class,

is there any downsides of this approach?

Hi @luceos , I just implemented the tenant cache with the file driver, following the idea of @zomax above. However, it did not work straight off the bat, and I wonder if this is not the case for the redis version also.

The problem was obtaining the uuid for the new prefix and cache file path. Since the CacheServiceProvider is added before the providers for Tenancy, the tenant database is not defined yet (but it is set in the configuration file), so an error is given.

This was solved by changing the line obtaining the uuid to use specifically the system connection (\DB::connection(‘system’)->):

$uuid = \DB::connection(‘system’)->table(‘hostnames’) ->select(‘websites.uuid’) ->join(‘websites’, ‘hostnames.website_id’, ‘=’, ‘websites.id’) ->where(‘fqdn’, $fqdn) ->value(‘uuid’);

A small detail, but maybe it will help others as well.

Cheers!

PS: Btw, the tenant-aware cache is a huge issue, and if not implemented in Tenancy and left to the user to decide, it should be at least mentioned visibly in the installation documentation. Most tenancy websites will have auth (like in my case), and having user permissions messed up between instances is a nasty security breach (and a pain to debug if not knowing what to look for).

@gruz @luceos @kinsaz @36864

Hi,

I did a nice trick to solve this issue. The above solution not working for me as i have many packages that instantiate the Cache object.

The problem is with this commit: https://github.com/laravel/framework/pull/19646 Which make any Service Provider with namespace starting with Illuminate load first, then auto-discovered service providers, then any other providers added to config/app.php.

My solution is:

  • First change CacheServiceProvider namespace to Illuminate\UniqueAppName

  • Now just change cache.prefix value in the boot method

<?php

namespace Illuminate\UniqueAppName;

use Illuminate\Support\ServiceProvider;

class CacheServiceProvider extends ServiceProvider
{
	/**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
	if (PHP_SAPI === 'cli') {
		$uuid = str_slug(env('APP_NAME', 'laravel'), '_').'_cache';
	} else {
		$fqdn = request()->getHost();
		$uuid = \DB::table('hostnames')
				->select('websites.uuid')
				->join('websites', 'hostnames.website_id', '=', 'websites.id')
				->where('fqdn', $fqdn)
				->value('uuid');
	}
		
	// Change Cache Prefix
        config( ['cache.prefix' => $uuid. '_cache'] );
   }
}
  • Then add file to composer.json autoload files object
"autoload": {
		"files": [
			"app/Providers/CacheServiceProvider.php"
		],
        "classmap": [
            "database/seeds",
            "database/factories"
        ],
        "psr-4": {
            "App\\": "app/"
        }
    },
  • Run composer dump-autoload

  • Finally add our CacheServiceProvider service provider to config/app.php before or after AppServiceProvider

/*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
	Illuminate\UniqueAppName\CacheServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

It should work with any Cache drive

@kinsaz I didn’t meet such an error. You need to debug it at your own. Sorry, cannot help with issue.