Skip to content

[4.x] Change tenant storage listeners into jobs#1446

Draft
lukinovec wants to merge 9 commits intomasterfrom
storage-listeners
Draft

[4.x] Change tenant storage listeners into jobs#1446
lukinovec wants to merge 9 commits intomasterfrom
storage-listeners

Conversation

@lukinovec
Copy link
Copy Markdown
Contributor

@lukinovec lukinovec commented Mar 26, 2026

The CreateTenantStorage and DeleteTenantStorage listeners were used alongside JobPipelines. When the TenantCreated JobPipeline had shouldBeQueued(true) and the Listeners\CreateTenantStorage was uncommented, the listener would throw an exception (Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException Database tenantX.sqlite does not exist.) because at the time of executing the listener, the tenant DB wasn't created yet.

This PR changes CreateTenantStorage and DeleteTenantStorage into jobs and puts these commented jobs into the JobPipelines, so that they can be queued with the rest of the jobs.

Summary by CodeRabbit

  • Refactor
    • Tenant storage creation and deletion now run as queued/background jobs instead of synchronous listeners, reducing request blocking during tenant provisioning and removal.
  • Tests
    • Tests updated to validate the job-based flow (with options to run jobs synchronously in tests) and assert tenant storage paths are created or removed as expected.
  • Chore
    • Template comments adjusted for job-based event wiring.

lukinovec and others added 2 commits March 26, 2026 15:34
Also move the commented jobs to the JobPipelines and update FilesystemTenancyBootstrapperTest accordingly.
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 72.72727% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.86%. Comparing base (c32f52c) to head (97e8566).

Files with missing lines Patch % Lines
src/Listeners/DeleteTenantStorage.php 0.00% 5 Missing ⚠️
src/Jobs/CreateTenantStorage.php 85.71% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1446      +/-   ##
============================================
- Coverage     86.03%   85.86%   -0.18%     
- Complexity     1156     1166      +10     
============================================
  Files           184      186       +2     
  Lines          3381     3402      +21     
============================================
+ Hits           2909     2921      +12     
- Misses          472      481       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lukinovec lukinovec marked this pull request as ready for review March 27, 2026 13:40
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 74585181-2d51-42b4-85b0-f135341b3efa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Synchronous tenant storage listeners were replaced by queued jobs: CreateTenantStorage moved to Jobs and implements ShouldQueue; a new DeleteTenantStorage job was added and the old listener removed. Provider stub and tests were updated to wire these via JobPipeline.

Changes

Cohort / File(s) Summary
Jobs added / migrated
src/Jobs/CreateTenantStorage.php, src/Jobs/DeleteTenantStorage.php
CreateTenantStorage moved from ListenersJobs, now implements ShouldQueue, accepts Tenant via constructor and uses handle(): void. New DeleteTenantStorage job added with constructor-injected Tenant and safe directory-deletion logic (checks central vs tenant path).
Listener removed
src/Listeners/DeleteTenantStorage.php
Synchronous DeleteTenantStorage listener deleted; its logic migrated into the new queued job.
Provider stub updates
assets/TenancyServiceProvider.stub.php
Event TenantCreated and DeletingTenant job-pipeline entries adjusted: commented job-class lines added and prior commented listener lines removed (no executable change since lines remain commented).
Test updates
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php
Tests switched from listener wiring to JobPipeline::make([...])->mapEventProperty(...)->shouldBeQueued(false) for creation/deletion, added assertion for tenant cache directory creation, and updated deletion assertions for config variants.

Sequence Diagram(s)

sequenceDiagram
    participant Event as Event Dispatcher
    participant Pipeline as JobPipeline / Queue
    participant Job as Create/Delete Tenant Job
    participant Tenancy as tenancy() context
    participant FS as Filesystem

    Event->>Pipeline: Fire TenantCreated (maps tenant, enqueue CreateTenantStorage)
    Pipeline->>Job: Dequeue & execute CreateTenantStorage
    Job->>Tenancy: tenancy()->run($tenant, callback)
    Tenancy->>FS: resolve storage_path(), create dirs
    FS-->>Tenancy: dirs created
    Job-->>Pipeline: complete

    Event->>Pipeline: Fire DeletingTenant (maps tenant, enqueue DeleteTenantStorage)
    Pipeline->>Job: Dequeue & execute DeleteTenantStorage
    Job->>Tenancy: tenancy()->central(...) & tenancy()->run($tenant,...)
    Tenancy->>FS: resolve central & tenant paths
    Job->>FS: compare realpaths, check dir
    FS->>FS: deleteDirectory(path) if safe
    Job-->>Pipeline: complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I swapped my listener hat for queueing hops,

I build the cache and tidy up the shops.
With careful checks and gentle paws,
I guard the central path from flaws.
Hooray — neat storage, no more flops!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: converting tenant storage listeners into jobs to resolve timing issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch storage-listeners

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php (1)

188-206: Make suffix assumptions explicit in the new creation test.

The assertion on Line 203 assumes default tenant suffix behavior. Pinning the relevant config in this test will make it less brittle to future default changes.

✅ Suggested test hardening
 test('tenant storage gets created when TenantCreated listens to CreateTenantStorage', function() {
     config([
         'tenancy.bootstrappers' => [
             FilesystemTenancyBootstrapper::class,
         ],
+        'tenancy.filesystem.suffix_storage_path' => true,
+        'tenancy.filesystem.suffix_base' => 'tenant',
     ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 188 -
206, The test assumes a hard-coded "tenant" suffix when building
$tenantStoragePath; make that explicit by pinning the suffix in the test (e.g.
add config(['tenancy.storage.suffix' => 'tenant']);) and/or build
$tenantStoragePath from the config value instead of the literal string so
FilesystemTenancyBootstrapperTest's test('tenant storage gets created when
TenantCreated listens to CreateTenantStorage', ...) and the final
$this->assertDirectoryExists(...) no longer rely on implicit defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Jobs/DeleteTenantStorage.php`:
- Around line 25-29: DeleteTenantStorage currently calls
tenancy()->run($this->tenant, fn () => storage_path()) and then
File::deleteDirectory($path) which can remove the central storage when
tenancy.filesystem.suffix_storage_path is false; change the logic in
DeleteTenantStorage so you first resolve both the tenant-specific path and the
central storage path and verify the tenant path is not identical to the central
one (or contains the tenant identifier) before calling File::deleteDirectory;
use tenancy()->run($this->tenant, ...) and storage_path() for comparisons and
bail out / log and skip deletion if the resolved $path equals the default
storage_path() or fails a tenant-specific containment check.

---

Nitpick comments:
In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php`:
- Around line 188-206: The test assumes a hard-coded "tenant" suffix when
building $tenantStoragePath; make that explicit by pinning the suffix in the
test (e.g. add config(['tenancy.storage.suffix' => 'tenant']);) and/or build
$tenantStoragePath from the config value instead of the literal string so
FilesystemTenancyBootstrapperTest's test('tenant storage gets created when
TenantCreated listens to CreateTenantStorage', ...) and the final
$this->assertDirectoryExists(...) no longer rely on implicit defaults.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a284bf55-8fa0-481b-9971-86bdd3e8225d

📥 Commits

Reviewing files that changed from the base of the PR and between 60dd522 and dc659ed.

📒 Files selected for processing (5)
  • assets/TenancyServiceProvider.stub.php
  • src/Jobs/CreateTenantStorage.php
  • src/Jobs/DeleteTenantStorage.php
  • src/Listeners/DeleteTenantStorage.php
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php
💤 Files with no reviewable changes (1)
  • src/Listeners/DeleteTenantStorage.php

Comment thread src/Jobs/DeleteTenantStorage.php
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Jobs/DeleteTenantStorage.php`:
- Around line 43-44: The directory deletion currently calls
File::deleteDirectory($path) and ignores its return value, so failures are
silent; update the DeleteTenantStorage job to capture the return from
File::deleteDirectory($path) (when is_dir($path) && ! $tenantPathIsCentral) and
if it returns false throw an exception (e.g., RuntimeException) including $path
in the message so the queue marks the job as failed and can be retried/alerted;
ensure you do this inside the method that performs the deletion (referencing
File::deleteDirectory, $path, and $tenantPathIsCentral).

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php`:
- Line 203: The test hard-codes '/tenant' when building $tenantStoragePath;
change it to use the configured suffix base instead so it follows tenancy
configuration. Replace the literal '/tenant' with a dynamic value obtained from
config('tenancy.filesystem.suffix_base') (or the same helper used in
production), i.e. build $tenantStoragePath by concatenating $centralStoragePath
with the configured suffix base and $tenant->getTenantKey(); update any related
expectations to use that same config-derived value.
- Around line 195-199: The test currently registers listeners for TenantCreated
using Event::listen with
JobPipeline::make([...])->shouldBeQueued(false)->toListener(), which disables
queuing and therefore doesn't exercise serialization/queued execution order;
change the listeners for both storage and database jobs (e.g., the
JobPipeline::make([...]) entries for CreateTenantStorage and the corresponding
CreateTenantDatabase) to use queued execution (remove or set
shouldBeQueued(true) / omit shouldBeQueued(false)) and then assert the jobs are
dispatched to the queue and executed in the expected order (use the framework's
queue fake / assertions to assert queuing and execution order for TenantCreated
handling).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 899f92bb-5773-4f84-b10c-6703b0e57ea8

📥 Commits

Reviewing files that changed from the base of the PR and between dc659ed and 33c2840.

📒 Files selected for processing (2)
  • src/Jobs/DeleteTenantStorage.php
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php

Comment thread src/Jobs/DeleteTenantStorage.php
Comment thread tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php
Comment thread tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php (1)

195-199: ⚠️ Potential issue | 🟠 Major

Queued JobPipeline path is still not exercised in tests.

Lines 195-199 and 210-214 force shouldBeQueued(false), so these tests only validate synchronous execution and miss the queue/serialization path that this PR targets.

#!/bin/bash
# Verify storage job tests are synchronous-only and lack queue assertions
rg -n -C2 'CreateTenantStorage|DeleteTenantStorage|shouldBeQueued\(' tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php
rg -n -C2 'Queue::fake|Queue::assertPushed|Bus::fake|Bus::assertDispatched' tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php

Expected verification outcome:

  • First command shows shouldBeQueued(false) for both storage job pipelines.
  • Second command returns no queue-fake/assert coverage in this file.

Also applies to: 210-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php` around lines 195 -
199, Tests currently force synchronous execution by calling
shouldBeQueued(false) on the Event listener pipelines
(JobPipeline::make([...])->shouldBeQueued(false)), so the queue/serialization
path (CreateTenantStorage, DeleteTenantStorage) is never exercised; change the
tests to cover the queued path by removing or changing shouldBeQueued(false) for
the TenantCreated/TenantDeleted listeners and use a queue fake (e.g.,
Queue::fake()) around the event dispatch, then assert the job was queued
(Queue::assertPushed or similar) for CreateTenantStorage and DeleteTenantStorage
after dispatching TenantCreated/TenantDeleted events to validate the
serialized/queued behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php`:
- Around line 195-199: Tests currently force synchronous execution by calling
shouldBeQueued(false) on the Event listener pipelines
(JobPipeline::make([...])->shouldBeQueued(false)), so the queue/serialization
path (CreateTenantStorage, DeleteTenantStorage) is never exercised; change the
tests to cover the queued path by removing or changing shouldBeQueued(false) for
the TenantCreated/TenantDeleted listeners and use a queue fake (e.g.,
Queue::fake()) around the event dispatch, then assert the job was queued
(Queue::assertPushed or similar) for CreateTenantStorage and DeleteTenantStorage
after dispatching TenantCreated/TenantDeleted events to validate the
serialized/queued behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5bf88f56-0bfa-4b2b-8dd5-223cb4f6d6ab

📥 Commits

Reviewing files that changed from the base of the PR and between 33c2840 and 5e0153c.

📒 Files selected for processing (1)
  • tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php

@lukinovec
Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Reviews resumed.

@lukinovec
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@lukinovec
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Full review triggered.

@stancl stancl marked this pull request as draft April 18, 2026 19:16
Without this, updating Tenancy in existing projects would break TenancyServiceProvider. The logic of the deprecated versions is up-to-date with the changes made in the jobs up until now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants