Skip to content

Identify the hostname per request, so the package works on Octane - #1056

Open
dazza-dev wants to merge 1 commit into
tenancy:5.xfrom
dazza-dev:contrib/octane-identification
Open

Identify the hostname per request, so the package works on Octane#1056
dazza-dev wants to merge 1 commit into
tenancy:5.xfrom
dazza-dev:contrib/octane-identification

Conversation

@dazza-dev

Copy link
Copy Markdown

Closes the question in #1015, which has been open since 2021: the package does not work on Octane, and this is why.

The symptom

On a long lived process — Octane, Swoole, RoadRunner — the second request an application serves is given the tenant identified for the first one, and its database with it.

Measured by driving the lifecycle a real request goes through (bind the request, bootstrap, boot providers, run the middleware), one application, two requests, different hosts:

before   lifecycle.testing => e7910b09…   second.testing => e7910b09…
after    lifecycle.testing => e7910b09…   second.testing => fcd0cfb6…

Measured on this branch against 5.x itself, on the laravel/laravel skeleton the suite already uses.

PHP-FPM never sees it, because every request builds an application. That is why the suite has never caught it either: it boots one application per test.

Why it happens

Environment::identifyHostname() does not identify anything. It registers a lazy singleton and returns:

public function identifyHostname()
{
    $this->app->singleton(CurrentHostname::class, function () {
        $hostname = $this->dispatch(new HostnameIdentification());
        $this->tenant(optional($hostname)->website);
        return $hostname;
    });
}

What actually identifies is the constructor, through $app->make(CurrentHostname::class) right after. And the constructor runs while the providers boot — once per application, not once per request. After that the binding is resolved and stays resolved, so no later request re-identifies.

There is a second, quieter effect. TenancyProvider::register() registers the Environment singleton inside a booted callback, while the deferred HostnameProvider::boot() resolves it earlier. The first resolution finds no singleton and builds an instance through reflection that is then discarded; a second is built once the singleton exists. Two constructions per application, measured with a backtrace in the constructor.

The change

  • TenancyProvider registers the singleton in register(), so one environment is built.
  • The constructor only arranges for identification. Identification reads the request, and boot may run without one.
  • identifyHostname() registers and resolves, so it does what its name says.
  • EagerIdentification asks for it on every request, honouring both auto-identification and early-identification.

Kernel::sendRequestThroughRouter() binds the request before it bootstraps, so a request still identifies with its own Host.

No change for applications on PHP-FPM. Every request builds an application there, so identification happened per request already.

The one test change

tests/Test.php rebuilds the environment after migrateSystem(). The harness creates its schema after the application boots, which no real application does, so the environment concluded tenancy was not installed and cached that. Leaving the rebuild to whoever resolved it first made the request under test look unlike a real one.

Notes

I also chased whether this doubled Websites\Switched per request, since two constructions each identify. It does not: the constructions happen back to back and the second identification nests inside the first, so one Identified and one Switched are emitted either way. Measured, not assumed.

This is also the answer to the make(CurrentHostname::class) that #1050 and #1051 add inside identifyHostname() without being able to say why — I left the detail on both PRs.

For context: I maintain a fork of 5.9 at dazza-dev/hyn-multi-tenant, where this is released as v0.10.4, and have been sending back what applies here — #1054 for the isolation and provisioning fixes, #1055 for the test harness.

On a long lived process the second request an application serves is
given the tenant identified for the first one, and its database with it.
PHP-FPM never sees it, because every request builds an application.

identifyHostname() does not identify: it registers a lazy singleton and
returns. What identifies is the environment's constructor, resolving
that binding right after, and the constructor runs while the providers
boot -- once per application, not once per request. Once resolved, the
binding stays resolved and no later request re-identifies.

The environment is also built twice per application, because the
singleton is registered from a booted callback while the deferred
HostnameProvider resolves it from boot, which runs earlier.

So:

- TenancyProvider registers the singleton in register()
- the constructor only arranges for identification, since identification
  reads the request and boot may run without one
- identifyHostname() registers and resolves, doing what it says
- EagerIdentification asks for it per request, honouring both
  auto-identification and early-identification

Kernel::sendRequestThroughRouter() binds the request before it
bootstraps, so a request identifies with its own Host.

tests/Test.php rebuilds the environment after migrateSystem(). The
harness creates its schema after the application boots, which no real
application does, so the environment concluded tenancy was not installed
and cached that.

128 tests green, the same count and assertions as 5.x without this.

Closes tenancy#1015
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.

1 participant