1
0
Fork 0
mirror of https://github.com/archtechx/tenancy-queue-tester.git synced 2025-12-12 21:04:03 +00:00

initial commit

This commit is contained in:
Samuel Štancl 2024-12-31 06:35:15 +01:00
commit 52d95e9370
12 changed files with 525 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
src/

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# Tenancy for Laravel queue testing suite
In addition to the tests we can write using testbench, we have this repository which:
1. Creates a new Laravel application
2. Sets up Tenancy
3. Creates a sample job
4. Asserts that the queue worker is working as expected
This is mostly due to some past bugs that were hard to catch in our test suite.
With this repo, we can have a separate CI job validating queue behavior _in a real application_.
## TODOs
- Verify how `queue:restart` works in v4

25
cli/Dockerfile Normal file
View file

@ -0,0 +1,25 @@
ARG PHP_VERSION=8.4
FROM php:${PHP_VERSION}-cli-bookworm
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
RUN apt update && apt install -y git unzip libzip-dev libicu-dev \
&& apt autoremove && apt clean
RUN pecl install redis && docker-php-ext-enable redis
RUN docker-php-ext-install zip && docker-php-ext-enable zip
RUN docker-php-ext-install pdo_mysql && docker-php-ext-enable pdo_mysql
RUN docker-php-ext-install intl && docker-php-ext-enable intl
WORKDIR /var/www/html
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && php composer-setup.php
RUN mv composer.phar /usr/local/bin/composer
RUN mkdir /var/www/.composer && chmod -R 777 /var/www/.composer
RUN mkdir -p /var/www/.config/psysh && chmod -R 777 /var/www/.config/psysh
USER www-data

6
cli/build.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
tag="tenancy-queue-test-cli"
php="8.4"
docker build --build-arg PHP_VERSION=$php -t $tag .

26
docker-compose.yml Normal file
View file

@ -0,0 +1,26 @@
services:
redis:
image: redis:7-bookworm
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 2
start_period: 1s
queue:
image: tenancy-queue-test-cli
working_dir: /var/www/html
user: www-data
command: php artisan queue:work -vvv
healthcheck:
test: ["CMD-SHELL", "pidof php"]
interval: 30s
timeout: 2s
retries: 2
start_period: 2s
depends_on:
redis:
condition: service_healthy
volumes:
- ./src:/var/www/html

11
setup.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/sh
TENANCY_VERSION=${TENANCY_VERSION:-"dev-master"}
set -e
(cd cli && ./build.sh)
rm -rf src
docker run --rm -it -e TENANCY_VERSION="${TENANCY_VERSION}" -v .:/var/www/html tenancy-queue-test-cli bash setup/_tenancy_setup.sh

19
setup/FooJob.php Normal file
View file

@ -0,0 +1,19 @@
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Str;
class FooJob implements ShouldQueue
{
use Queueable;
public function handle(): void
{
User::create(['name' => Str::random(12), 'email' => Str::random(12), 'password' => Str::random(12)]);
}
}

13
setup/Tenant_base.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasDatabase, HasDomains;
}

39
setup/_tenancy_setup.sh Executable file
View file

@ -0,0 +1,39 @@
#!/bin/sh
composer global require laravel/installer
~/.composer/vendor/laravel/installer/bin/laravel new --no-interaction --database=sqlite --git src/
cd src/
# If a specific Laravel version is desired
# composer require -W laravel/framework:11.15.0
composer config minimum-stability dev
composer require "stancl/tenancy:$TENANCY_VERSION"
php artisan tenancy:install --no-interaction
php artisan migrate
rm bootstrap/providers.php
cp ../setup/providers.php bootstrap/providers.php
cp ../setup/Tenant_base.php app/Models/Tenant.php
if [ ! -f vendor/stancl/tenancy/src/Contracts/TenantWithDatabase.php ]; then
sed -i 's/use Stancl\\Tenancy\\Contracts\\TenantWithDatabase/use Stancl\\Tenancy\\Database\\Contracts\\TenantWithDatabase/' app/Models/Tenant.php
fi
sed -i 's/QUEUE_CONNECTION=database/QUEUE_CONNECTION=redis/' .env
sed -i 's/REDIS_HOST=127.0.0.1/REDIS_HOST=redis/' .env
sed -i 's/CACHE_STORE=database/CACHE_STORE=redis/' .env
sed -i 's/Stancl\\Tenancy\\Database\\Models\\Tenant/App\\Models\\Tenant/' config/tenancy.php
sed -i 's/.*RedisTenancyBootstrapper::class.*/ \\Stancl\\Tenancy\\Bootstrappers\\RedisTenancyBootstrapper::class,/' config/tenancy.php
sed -i 's/'\''prefixed_connections'\'' => \[.*$/'\''prefixed_connections'\'' => [ '\''cache'\'',/' config/tenancy.php
echo "REDIS_QUEUE_CONNECTION=queue" >> .env
rm config/database.php
cp ../setup/database.php config/database.php
cp database/migrations/*create_users*.php database/migrations/tenant
mkdir app/Jobs
cp ../setup/FooJob.php app/Jobs/FooJob.php

182
setup/database.php Normal file
View file

@ -0,0 +1,182 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],
'default' => [
'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'),
],
'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'),
],
'cache' => [
'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_CACHE_DB', '1'),
],
],
];

6
setup/providers.php Normal file
View file

@ -0,0 +1,6 @@
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\TenancyServiceProvider::class,
];

182
test.sh Executable file
View file

@ -0,0 +1,182 @@
#!/bin/sh
set -e
assert_queue_worker_running() {
if docker compose ps -a --format '{{.Status}}' queue | grep -q "Exited"; then
echo "ERR: Queue worker has exited!"
docker compose logs queue
exit 1
fi
}
assert_queue_worker_exited() {
if ! docker compose ps -a --format '{{.Status}}' queue | grep -q "Exited"; then
echo "ERR: Queue worker has NOT exited!"
docker compose logs queue
exit 1
fi
}
assert_no_queue_failures() {
assert_queue_worker_running
if docker compose logs queue -n 2 | grep -q "FAIL"; then
echo "ERR: Queue failures detected in logs"
exit 1
fi
}
assert_tenant_users() {
assert_no_queue_failures
local expected_count=$1
test "$(sqlite3 src/database/tenantfoo.sqlite 'SELECT count(*) from USERS')" -eq "$expected_count" || { echo "ERR: Tenant DB expects $expected_count user(s)."; exit 1; }
}
assert_central_users() {
assert_no_queue_failures
local expected_count=$1
test "$(sqlite3 src/database/database.sqlite 'SELECT count(*) from USERS')" -eq "$expected_count" || { echo "ERR: Central DB expects $expected_count user(s)."; exit 1; }
}
without_queue_assertions() {
# Store the original function
local original_assert_no_queue_failures=$(declare -f assert_no_queue_failures)
assert_no_queue_failures() { :; }
# Run the provided command with its arguments
"$@"
# Restore the original function
eval "$original_assert_no_queue_failures"
}
dispatch_central_job() {
echo "Dispatching job from central context..."
docker compose exec -T queue php artisan tinker --execute "dispatch(new App\Jobs\FooJob);"
sleep 5
}
dispatch_tenant_job() {
echo "Dispatching job from tenant context..."
docker compose exec -T queue php artisan tinker --execute "App\\Models\\Tenant::first()->run(function () { dispatch(new App\Jobs\FooJob); });"
sleep 5
}
###################################### SETUP ######################################
rm -f src/database.sqlite
rm -f src/database/tenantfoo.sqlite
docker compose start redis # in case it's not running - the below setup code needs Redis to be running
docker compose run --rm queue php artisan migrate:fresh >/dev/null
docker compose run --rm queue php artisan tinker -v --execute "App\\Models\\Tenant::create(['id' => 'foo', 'tenancy_db_name' => 'tenantfoo.sqlite']);"
docker compose down; docker compose up -d --wait
docker compose logs -f queue &
# Kill any log watchers that may still be alive
trap "docker compose stop queue" EXIT
echo "Setup complete, starting tests...\n"
################### BASIC PHASE: Assert jobs use the right context ###################
dispatch_tenant_job
assert_tenant_users 1
assert_central_users 0
echo "OK: User created in tenant\n"
dispatch_central_job
assert_tenant_users 1
assert_central_users 1
echo "OK: User created in central\n"
############# RESTART PHASE: Assert the worker always responds to signals #############
echo "Running queue:restart (after a central job)..."
docker compose exec -T queue php artisan queue:restart >/dev/null
sleep 3
assert_queue_worker_exited
echo "OK: Queue worker has exited\n"
echo "Starting queue worker again..."
docker compose restart queue
sleep 3
docker compose logs -f queue &
echo
dispatch_tenant_job
# IMPORTANT:
# If the worker remains in the tenant context after running a job
# it not only fails the final assertion here by not responding to queue:restart.
# It ALSO prematurely restarts right here! See https://github.com/archtechx/tenancy/issues/1229#issuecomment-2566111616
# However, we're not too interested in checking for an extra restart, so we skip
# queue assertions here and only check that the job executed correctly.
# Then, if the queue worker has shut down, we simply start it up again and continue
# with the tests. That said, if the warning has been printed, it should be pretty much
# guaranteed that the assertion about queue:restart post-tenant job will fail too.
without_queue_assertions assert_tenant_users 2
without_queue_assertions assert_central_users 1
echo "OK: User created in tenant\n"
if docker compose ps -a --format '{{.Status}}' queue | grep -q "Exited"; then
echo "WARN: Queue worker restarted after running a tenant job post-restart (https://github.com/archtechx/tenancy/issues/1229#issuecomment-2566111616) following assertions will likely fail."
docker compose start queue # Start the worker back up
sleep 3
docker compose logs -f queue &
else
echo "OK: No extra restart took place"
fi
# Following the above, we also want to check if this only happens the first
# time a job is dispatched for a tenant (with central illuminate:queue:restart) filled
# and fills the TENANT's illuminate:queue:restart from then on, or if this happens on
# future jobs of that tenant as well.
# This time, just to add more context, we can try to dispatch a central job first
# in case it changes anything. But odds are that in broken setups we'll see both warnings.
dispatch_central_job
without_queue_assertions assert_tenant_users 2
without_queue_assertions assert_central_users 2
echo "OK: User created in central\n"
dispatch_tenant_job
without_queue_assertions assert_tenant_users 3
without_queue_assertions assert_central_users 2
echo "OK: User created in tenant\n"
if docker compose ps -a --format '{{.Status}}' queue | grep -q "Exited"; then
echo "WARN: ANOTHER extra restart took place after running a tenant job"
docker compose start queue # Start the worker back up
sleep 3
docker compose logs -f queue &
else
echo "OK: No second extra restart took place"
fi
# We have to clear the central illuminate:queue:restart value here
# to make the last assertion work, because if the previous WARNs were
# triggered, that means the following *tenant job dispatch* will trigger
# a restart as well.
# -n 1 = DB number for cache connection, configured in setup/database.php
docker compose exec redis redis-cli -n 1 DEL laravel_database_illuminate:queue:restart >/dev/null
# Also make the queue worker reload the value from cache
docker compose restart queue
docker compose logs -f queue &
# Finally, we dispatch a tenant job *immediately* before a restart.
dispatch_tenant_job
assert_tenant_users 4
assert_central_users 2
echo "OK: User created in tenant\n"
echo "Running queue:restart (after a tenant job)..."
docker compose exec -T queue php artisan queue:restart >/dev/null
sleep 3
assert_queue_worker_exited
echo "OK: Queue worker has exited"