QueueTenancyBootstrapper
The QueueTenancyBootstrapper makes queued jobs dispatched from the context of a tenant run in the same tenant context.
It does this by adding the tenant key to the job payload and listening to events related to queued jobs being processed.
In most setups, you’ll need to make slight changes to your queue configuration to make it work correctly.
Multi-database setups
Section titled “Multi-database setups”If you’re using the database queue driver and the DatabaseTenancyBootstrapper, you need to ensure your queued jobs
get stored in the central database. This is because you can have only one queue worker (there’s not a queue worker per tenant)
so all the jobs need to be in one place even if they contain information about which tenant they should execute for.
To achieve this, force the central connection on your database queue connection:
'connections' => [ 'database' => [ // ... 'connection' => env('DB_CONNECTION'), ],],Redis queue driver
Section titled “Redis queue driver”If you’re using the redis queue driver and the RedisTenancyBootstrapper, you need to use a connection that isn’t part of
your configured prefixed_connections:
'redis' => [ 'prefixed_connections' => [ 'default', // 'cache', ],],You can check which Redis connection is being used for queued jobs in your queue config:
'connections' => [ 'redis' => [ 'driver' => 'redis', 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90), 'block_for' => null, 'after_commit' => false, ],],Since you likely don’t have REDIS_QUEUE_CONNECTION set (and haven’t configured a new Redis connection), default will be used here.
Let’s fix that by setting this connection to queue and configuring a new connection with that name:
'connections' => [ 'redis' => [ 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 'connection' => 'queue', // ... ],],'redis' => [ 'queue' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), ],],The configuration of the connection is identical to default, what’s important here is that it’s a separate connection
and isn’t included in tenancy.redis.prefixed_connections.
Central queues
Section titled “Central queues”If you’d like to dispatch a central job from the tenant context, you can do this by configuring a new queue and marking it as central:
'connections' => [ 'central' => [ 'driver' => 'database', 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 90, 'central' => true, ],],Then simply dispatch jobs to this queue:
dispatch(new MyJob())->onConnection('central');Persistent bootstrapper
Section titled “Persistent bootstrapper”There’s also an alternative implementation of this bootstrapper: PersistentQueueTenancyBootstrapper.
The main difference is that the persistent bootstrapper remains in the tenant’s context after processing a job. This comes with some benefits, but requires more careful use which is why it’s not the default.
The main benefit is that JobProcessed listeners will run in the tenant context. This is used for instance
by Telescope, which uses a JobProcessed listener in its JobWatcher and tries to unserialize the job.
If a job injects a model from the tenant context — therefore a model with connection = 'tenant' — it will
be impossible to unserialize such a job in the central context since that connection no longer exists.
There are some some workarounds that can be used but they require code changes.
You can instead use the PersistentQueueTenancyBootstrapper with the following caveats:
- If you use the Redis queue driver, you may not use
RedisTenancyBootstrapper. - If you do need to use
RedisTenancyBootstrapper, at least make sure the queue doesn’t use a prefixed connection:config/queue.php 'redis' => ['driver' => 'redis','connection' => env('REDIS_QUEUE_CONNECTION', 'default'),config/tenancy.php 'redis' => ['prefix' => 'tenant_%tenant%_', // Each key in Redis will be prepended by this prefix format, with %tenant% replaced by the tenant key.'prefixed_connections' => [ // Redis connections whose keys are prefixed, to separate one tenant's keys from another.'default',// 'cache', // Enable this if you want to scope cache using RedisTenancyBootstrapper],], - Make sure your queue worker responds to
php artisan queue:restartsignals directly after processing a tenant job.
That’s the reason why this persistent queue bootstrapper implementation was replaced with the simpler one we
use by default now — when paired with Redis and RedisTenancyBootstrapper the queue worker wasn’t responding
to queue:restart signals.
You can get both persistence and reliability, but make sure to follow the guidelines above and test that your queue worker
responds to queue:restart signals directly after processing a tenant job both locally and in production.