fix: KernelFactory — Singleton Kernel Without State Reset (closes #22)#123
fix: KernelFactory — Singleton Kernel Without State Reset (closes #22)#123
Conversation
…uests (closes #22) - Add services_resetter call after kernel->terminate() to reset services - Add tests to verify services reset is called when available - Gracefully handle missing services_resetter (e.g., in test mocks)
src/Middleware/SymfonyController.php
Outdated
| private function resetServices(): void | ||
| { | ||
| try { | ||
| $container = $this->kernel->getContainer(); |
There was a problem hiding this comment.
[Suggestion] Double container lookup (has() + get()) on the hot path — resetServices() performs two separate service-locator lookups per request:
$container->has('services_resetter') // first lookup
$container->get('services_resetter') // second lookupSince the presence of services_resetter is a static property of the application's service configuration (it never changes between requests), this double-lookup repeats unnecessary work on every single request.
In a high-throughput Workerman server, caching the services_resetter reference after the first lookup would eliminate this per-request overhead. For example, cache the resolved ResetInterface (or null if unavailable) in a private property after the first call:
private ?ResetInterface $servicesResetter = null;
private bool $servicesResetterInitialized = false;
private function resetServices(): void
{
if (!$this->servicesResetterInitialized) {
try {
$container = $this->kernel->getContainer();
if ($container->has('services_resetter')) {
$resetter = $container->get('services_resetter');
if ($resetter instanceof ResetInterface) {
$this->servicesResetter = $resetter;
}
}
} catch (\Throwable) {
// log or handle
}
$this->servicesResetterInitialized = true;
}
$this->servicesResetter?->reset();
}— coder-model via Qwen Code /review
src/Middleware/SymfonyController.php
Outdated
| $servicesResetter->reset(); | ||
| } | ||
| } | ||
| } catch (\Throwable) { |
There was a problem hiding this comment.
[Suggestion] catch (\Throwable) silently swallows all exceptions, including Error subclasses — with an empty body, there is zero observability into reset failures.
This is problematic because:
- Contradicts the PR's goal. The PR aims to prevent memory leaks in long-running processes. But if the container is in an inconsistent state or the resetter throws, the failure is invisible — services never get reset, and memory leaks accumulate with no trace.
- No debugging trail. If
getContainer()returns something unexpected, orhas()/get()throws for a reason other than service absence, the failure disappears completely. - Asymmetric error handling. The
HttpRequestHandlerwrapsterminateIfNeeded()in try-catch witherror_log()— but becauseresetServices()absorbs its own exceptions, reset failures never reach that logging.
Recommended fix — at minimum, log the exception:
} catch (\Throwable $e) {
error_log(sprintf(
'[workerman-bundle] Failed to reset services: %s in %s:%d',
$e->getMessage(),
$e->getFile(),
$e->getLine(),
));
}Alternative — narrow the catch to ContainerExceptionInterface and re-throw other exceptions, letting the caller's existing error handling take over. This ensures structural errors (e.g., container API misuse) are not silently ignored.
— coder-model via Qwen Code /review
tests/SymfonyControllerTest.php
Outdated
| public bool $bootCalled = false; | ||
| public bool $terminateCalled = false; | ||
| public int $terminateCount = 0; | ||
| public bool $servicesResetCalled = false; |
There was a problem hiding this comment.
[Suggestion] TestTerminableKernel::$servicesResetCalled is dead code — the property is declared but never written to or asserted.
TestTerminableKernel::getContainer() throws RuntimeException('Not implemented'), which resetServices() silently catches. So $servicesResetCalled stays false forever. The property appears to have been copied from TestKernelWithServicesResetter, but it serves no purpose here.
Fix: Remove public bool $servicesResetCalled = false; from TestTerminableKernel to avoid misleading readers.
— coder-model via Qwen Code /review
tests/SymfonyControllerTest.php
Outdated
| $this->assertTrue($kernel->servicesResetCalled, 'Services reset should be called after terminateIfNeeded'); | ||
| } | ||
|
|
||
| public function testTerminateIfNeededDoesNotCallServicesResetterWhenNotAvailable(): void |
There was a problem hiding this comment.
[Suggestion] testTerminateIfNeededDoesNotCallServicesResetterWhenNotAvailable doesn't actually test the 'service unavailable' path.
The test uses TestTerminableKernel, whose getContainer() throws RuntimeException('Not implemented'). The test passes because resetServices() swallows the exception — but the assertion only verifies $kernel->terminateCalled. The test never reaches the has('services_resetter') check; it only verifies that exception-swallowing works.
This gives false confidence that the 'service not available' branch is correct. If the exception-handling behavior were ever changed (e.g., per the logging suggestion above), this test would fail unexpectedly.
Fix: Use a kernel mock whose getContainer() returns a container where has('services_resetter') returns false. This exercises the actual availability check rather than an exception path. Alternatively, rename the test to reflect what it actually verifies.
— coder-model via Qwen Code /review
- Cache services_resetter to avoid double lookup on every request - Add logging for reset failures for observability - Remove dead code from TestTerminableKernel - Add TestKernelWithoutServicesResetter for proper 'not available' testing - Add optional LoggerInterface dependency
Summary
Changes
src/Middleware/SymfonyController.php- AddedresetServices()method called after terminatetests/SymfonyControllerTest.php- Added tests for service reset functionalityTesting