Skip to content

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.

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:

config/queue.php
'connections' => [
'database' => [
// ...
'connection' => env('DB_CONNECTION'),
],
],

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:

config/tenancy.php
'redis' => [
'prefixed_connections' => [
'default',
// 'cache',
],
],

You can check which Redis connection is being used for queued jobs in your queue config:

config/queue.php
'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:

config/queue.php
'connections' => [
'redis' => [
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'connection' => 'queue',
// ...
],
],
config/database.php
'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.

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:

config/queue.php
'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');

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:

  1. If you use the Redis queue driver, you may not use RedisTenancyBootstrapper.
  2. 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
    ],
    ],
  3. Make sure your queue worker responds to php artisan queue:restart signals 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.