[rb] Release a stopped service instead of holding it until the process exits - #17894
[rb] Release a stopped service instead of holding it until the process exits#17894ikraamg wants to merge 3 commits into
Conversation
…s exits ServiceManager#start registered an at_exit block per service, and the block captured the service, so every service ever started stayed reachable for the life of the process along with its ChildProcess. Stopping the service did not release it. Starting 500 services and stopping all of them left 500 alive. Services are now tracked in one list, with a single exit hook per process, and #stop removes the service from it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR Summary by QodoRelease stopped ServiceManagers by tracking running services with a single exit hook
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
Code Review by Qodo
1.
|
| config = instance_double(Service, executable_path: '/path/to/service', port: port, | ||
| log: nil, args: [], shutdown_supported: true) | ||
| described_class.new(config).tap do |service_manager| | ||
| allow(service_manager).to receive_messages(socket_lock: yielding_lock, find_free_port: nil, |
There was a problem hiding this comment.
1. Rspec mocks in new spec 📘 Rule violation ▣ Testability
The new unit spec relies on RSpec mocking (instance_double, allow(...).to receive_messages) rather than real or contract-driven integrations, which risks tests diverging from real interfaces over time.
Agent Prompt
## Issue description
The newly added unit spec uses RSpec mocks/doubles (e.g., `instance_double` and `allow(...).to receive_messages`) instead of using real implementations or simple in-memory fakes.
## Issue Context
Per compliance guidance, mocking frameworks should be avoided unless backed by a machine-checked contract; otherwise, tests can drift from the real API.
## Fix Focus Areas
- rb/spec/unit/selenium/webdriver/common/service_manager_spec.rb[31-42]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
stop_running is public, so a child could call it before starting anything of its own and stop services belonging to its parent. The check that drops inherited state now runs there too, rather than only in track. The exit hook spec asserted at_most(:once), which a call count of zero satisfies. Because the pid that armed the hook outlived an example, that is what it was measuring. The tracking state is now reset per example and the count is exact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit b6b834d |
It is a private singleton method in Ruby, so the signature should not offer it as part of the class surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit bf2a605 |
|
Can you use the PR template and explain what is being done here? Letting AI do the work is not enough for us to accept a PR. |
🔗 Related Issues
None open that I could find.
💥 What does this PR do?
ServiceManager#startregisters anat_exitblock per service, and the block captures the service, so every service ever started stays reachable for the life of the process along with itsChildProcess. Stopping the service does not release it, so the growth is unbounded in a process that starts many drivers: a suite that starts one per spec file, or a pool that recycles browsers.I found it running the Firefox WebDriver BiDi render pipeline for trmnl.com in production.
Starting 500 services and stopping every one of them, then running GC:
One retained
ServiceManagerholds 720 bytes across 12 objects once you follow the whole reachable graph, with a realChildProcessattached, after a normalstop.Services are now tracked in one list with a single exit hook per process, and
#stopremoves the service from it. A service that is still running is still held, which is what lets the exit hook stop it.ServiceManager.track,.untrackand.stop_runningstartrather than at load, so a service started in a forked child is still stopped when that child exitsrb/spec/unit/selenium/webdriver/common/service_manager_spec.rb🔧 Implementation Notes
The per-service
at_exitwas doing two jobs: making sure a running service gets stopped, and, as a side effect, keeping the service alive to do it. Only the first is wanted. A single class-level list gives the exit hook everything it needs to stop, and#stopremoving itself from that list is what lets a stopped service be collected.Alternatives I considered:
WeakRefor anObjectSpace::WeakMapof services. A stopped service would be collected, but so could a running one before the process exits, which is the case the hook exists for.at_exitblock onstop. Ruby has no API for that.startalone and havingstopclear the service's own state so the retained object is small. It shrinks the leak instead of removing it, and still grows without bound.Registering the hook lazily on the first
startis the part I would look at hardest. Registering at load would mean a process that forks after loading selenium-webdriver has the hook only in the parent, so a driver started in the child is never stopped. Arming it insideclaim_for_this_process, which also clears the list when the pid has changed, gives the child its own hook and its own list. I verified this with a realfork: with a service started in the parent and another in a forked child, the child tracks only its own and the parent is unaffected.The list is guarded by a mutex because drivers are commonly started from several threads, and
stop_runningiterates a copy so a service stopping itself during the walk cannot mutate the list underneath it.🤖 AI assistance
💡 Additional Considerations
stop_runningis public because the exit hook calls it, and it is useful to a pool that wants to shut everything down; happy to make it private if you would rather not add surface.🔄 Types of changes