Codeception / Codeception/module-yii2
`closeSession()` rebuilds a throwaway Application every test (redundant boot + memory leak)
- Dominant language
- PHP
- Stars
- 19
- Forks
- 43
- PR merge metrics
- No merged PRs in 30d
Description
### Summary
In the per-test lifecycle, `Connector\Yii2::resetApplication()` calls `closeSession()`, which calls `getApplication()`. When `Yii::$app` is already `null` (the normal state at that point), `getApplication()` **builds a complete throwaway application** just to close a session that doesn't exist, then immediately discards it.
The result is **two full application boots per test instead of one**, and the throwaway app leaves behind process-global state (most visibly the `ErrorHandler`'s `register_shutdown_function` + its 256 KB memory reserve) that is never released, so memory grows linearly with the number of tests and large suites OOM.
### Environment
- `codeception/module-yii2`: **2.0.5**
- `codeception/codeception`: 5.3.5
- `phpunit/phpunit`: 13.x
- PHP: 8.x
### Root cause
`Lib/Connector/Yii2.php` (line numbers from 2.0.5):
```php
// getApplication() — rebuilds when no app exists
protected function getApplication(): \yii\base\Application // ~L141
{
if (! isset(Yii::$app)) {
$this->startApp(); // <-- full application boot
}
return Yii::$app ?? throw new RuntimeException('Failed to create Yii2 application');
}
public function closeSession(): void // ~L542
{
$app = $this->getApplication(); // <-- triggers rebuild when null
if ($app instanceof \yii\web\Application && $app->has('session', true)) {
$app->session->close();
}
}
// resetApplication() — calls closeSession() before nulling the app
public function resetApplication(bool $closeSession = true): void // ~L158
{
if ($closeSession) {
$this->closeSession(); // <-- rebuild happens here
}
Yii::$app = null; // ~L166
...
}
```
The triggering call path each test (Module/Yii2.php):
1. _after() → getClient()->resetApplication() (L448) → Yii::$app = null.
2. next test _before() → recreateClient() → configureClient() → $client->resetApplication() (L378).
At this point Yii::$app is null, so closeSession() → getApplication() → startApp()
builds a throwaway app. startApp() proper then builds the real app a moment later.
Note configureClient() calls resetApplication() with no argument, so the
closeSessionOnRecreateApplication config (only consulted in beforeRequest(), behind
if ($this->recreateApplication)) does not affect this path.
Reproduction / evidence
Counting Application constructions in a real unit suite (subclass that increments a counter
in __construct): 20 constructions for 10 tests = 2× per test. Backtraces:
build #1: Yii::createObject ← startApp ← Module\Yii2::_before (real app)
build #2: Yii::createObject ← startApp ← getApplication ← closeSession ← resetApplication (throwaway)
A standalone loop replicating the module's per-test cycle leaks ~0.33 MB/iteration;
calling resetApplication(false) (skipping the closeSession rebuild) drops it to ~0.04 MB/iteration.
Impact
- A full, redundant application boot on every test (CPU/time).
- Linear memory growth across a suite (orphaned shutdown-function + 256 KB reserve per throwaway
app), causing OOM on large suites.
Proposed fix
closeSession() should not create an application — there's nothing to close if none exists:
Yii::$app = null; // ~L166
...
}
The triggering call path each test (Module/Yii2.php):
1. _after() → getClient()->resetApplication() (L448) → Yii::$app = null.
2. next test _before() → recreateClient() → configureClient() → $client->resetApplication() (L378).
At this point Yii::$app is null, so closeSession() → getApplication() → startApp()
builds a throwaway app. startApp() proper then builds the real app a moment later.
Note configureClient() calls resetApplication() with no argument, so the
closeSessionOnRecreateApplication config (only consulted in beforeRequest(), behind
if ($this->recreateApplication)) does not affect this path.
Reproduction / evidence
Counting Application constructions in a real unit suite (subclass that increments a counter
in __construct): 20 constructions for 10 tests = 2× per test. Backtraces:
build #1: Yii::createObject ← startApp ← Module\Yii2::_before (real app)
build #2: Yii::createObject ← startApp ← getApplication ← closeSession ← resetApplication (throwaway)
A standalone loop replicating the module's per-test cycle leaks ~0.33 MB/iteration;
calling resetApplication(false) (skipping the closeSession rebuild) drops it to ~0.04 MB/iteration.
Impact
- A full, redundant application boot on every test (CPU/time).
- Linear memory growth across a suite (orphaned shutdown-function + 256 KB reserve per throwaway
app), causing OOM on large suites.
Proposed fix
closeSession() should not create an application — there's nothing to close if none exists:
```php
public function closeSession(): void
{
$app = Yii::$app; // was: $this->getApplication();
if ($app instanceof \yii\web\Application && $app->has('session', true)) {
$app->session->close();
}
}
```
(Equivalently, configureClient() could call resetApplication(false), since that pre-startApp reset has no live session to close.)
Happy to open a PR.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.