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

Open Beginner friendly
#144 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
68/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
php
Domain
testing

Research direction

Start in Lib/Connector/Yii2.php at closeSession() and resetApplication(), then trace the configureClient() path in Module/Yii2.php. Verify that resetting with no Yii::$app does not construct a throwaway application, while an existing web application can still close its session. Done means the redundant boot and associated per-test memory growth are eliminated.

Written by the indexing model from the issue text.

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):

  // 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:

  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.

Dominant language
PHP
Stars
19
Forks
43
PR merge metrics
No merged PRs in 30d

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Codeception/module-yii2

All issues in Codeception/module-yii2

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.