Codeception / Codeception/module-yii2

`closeSession()` rebuilds a throwaway Application every test (redundant boot + memory leak)

Open
#144 1 comment 1 reaction 0 assignees View on GitHub
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.