Skip to content

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

Open
lukinovec wants to merge 7 commits intomasterfrom
storage-listeners
Open

[4.x] Change tenant storage listeners into jobs#1446
lukinovec wants to merge 7 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 jobs via the job pipeline instead of synchronous listeners, reducing request blocking during tenant provisioning and removal.
  • Tests
    • Tests updated to validate the new job-based flow (with options to run jobs synchronously in test) and to assert tenant storage paths are created or removed as expected.

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

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.23%. Comparing base (60dd522) to head (5e0153c).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1446      +/-   ##
============================================
+ Coverage     86.08%   86.23%   +0.15%     
- Complexity     1154     1158       +4     
============================================
  Files           184      184              
  Lines          3377     3385       +8     
============================================
+ Hits           2907     2919      +12     
+ Misses          470      466       -4     

☔ 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

📝 Walkthrough

Walkthrough

Tenant storage handling was migrated from synchronous listeners to queued jobs: CreateTenantStorage was moved to Jobs and now implements ShouldQueue; DeleteTenantStorage listener was removed and replaced by a queued job. 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, implements ShouldQueue, accepts Tenant via constructor and uses handle(): void; new DeleteTenantStorage job added with constructor-injected Tenant and safe directory-deletion logic.
Listener removed
src/Listeners/DeleteTenantStorage.php
Synchronous DeleteTenantStorage listener deleted; its deletion logic was migrated into the new queued job.
Provider stub updates
assets/TenancyServiceProvider.stub.php
Updated job pipeline entries in TenantCreated and DeletingTenant sections — storage-related entries converted to commented job entries and redundant comment-only listener lines removed.
Test updates
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php
Tests updated to register CreateTenantStorage/DeleteTenantStorage via JobPipeline (map event → $event->tenant, shouldBeQueued(false)), added assertion for tenant cache directory creation, and adjusted deletion scenario 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 (enqueue CreateTenantStorage)
    Pipeline->>Job: Dequeue & execute CreateTenantStorage
    Job->>Tenancy: tenancy()->run($tenant, callback)
    Tenancy->>FS: resolve storage_path() and create dirs
    FS-->>Tenancy: dirs created
    Tenancy-->>Job: return
    Job-->>Pipeline: complete

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

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped from Listener lane to Queue,

With careful paws I cradle each new view.
I plant a cache when tenants start,
and gently clear the path apart.
Hooray — tidy storage, through and through!

🚥 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 accurately summarizes the main change: converting tenant storage listeners into jobs to address a timing issue with database access.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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

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

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

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