]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(worker): terminate a deleted station's worker once it hosts zero elements (...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 19 Jul 2026 19:17:18 +0000 (21:17 +0200)
committerGitHub <noreply@github.com>
Sun, 19 Jul 2026 19:17:18 +0000 (21:17 +0200)
commit75897eb250d10bd4b060d2d02f7ba515bc4ac4c2
treec41b69b704f9d7285b2bf3eab3b707c8d4c635f9
parent2bee4896149f6000ecd27feb118d39a2406b1194
feat(worker): terminate a deleted station's worker once it hosts zero elements (#2027) (#2038)

* feat(worker): terminate a deleted station's worker once it hosts zero elements (#2027)

Add an element-granular removeElement primitive to the worker abstraction and
wire it into the station-delete path so a deleted charging station's hosting
worker thread is terminated once it no longer hosts any station, without
disrupting sibling stations that share the worker (elementsPerWorker > 1).

- WorkerAbstract: new abstract removeElement(elementKey: PropertyKey).
- WorkerSet: internal PropertyKey -> WorkerSetElement map fed by an injected
  generic elementKey selector; decrement numberOfWorkerElements and reuse a
  factored terminateWorker helper (shared with stop()) to terminate only at
  zero elements with no in-flight addition; purge the map on element removal.
- WorkerFixedPool/WorkerDynamicPool: documented no-op (poolifier exposes no safe
  single-element eviction without destroying the worker and its siblings).
- Bootstrap: pass stationInfo => stationInfo.hashId; call removeElement from
  workerEventDeleted.

Set-vs-pool decision: element-granular termination is workerSet-only; pool
worker threads are a bounded, reused resource and are not reclaimed per delete.
Cleanup/timeout contract: termination reuses the existing worker exit handler
for pending-promise rejection and set/map cleanup; any in-flight broadcast
request still resolves via the existing 60s aggregation timeout backstop.
Part A (#2031 cancellable reset) and bulk stop() are untouched.

* fix(worker): harden element-granular termination against concurrency races

Cross-validated review of the removeElement primitive surfaced two reachable
defects and one latent robustness issue, now fixed:

- Phantom-count leak on re-adding the same element key: numberOfWorkerElements
  was incremented unconditionally, so a duplicate/re-added key inflated the
  count and the worker was never terminated. Count is now key-aware (distinct
  keys per worker); factored into trackAddedWorkerElement.
- New addition routed onto a terminating worker: getWorkerSetElement could
  select a worker parked in terminateWorker, losing the message and rejecting
  an unrelated add. Terminating workers are now flagged and excluded from
  selection.
- getWorkerSetElementByWorker matched by threadId, which collapses to -1 after
  terminate(); switched to worker object identity.

Tests: add coverage for the in-flight-sibling gate, drain-to-last-sibling,
add-during-terminate, same-key re-add, and unknown-key no-op; consolidate the
silent worker fixture into echoWorker via a `hold` flag. Mutation-verified.

* fix(worker): make element termination robust and tighten its types

Second cross-validated review round hardened the removeElement primitive:

- terminateWorker no longer awaits a re-attached 'exit' listener (redundant,
  since worker.terminate() fulfils once the worker has exited, and it could
  deadlock if 'exit' fired first). It now catches a rejected terminate()
  (surfaced via the error event, not rethrown so the removal still succeeds)
  and performs pending-promise rejection + set/map cleanup in a finally, closing
  the zombie-on-reject leak, the orphaned-pending edge, and the stop-concurrent-
  exit deadlock in one place.
- Drop the dead, semantically fictional migration branch in
  trackAddedWorkerElement: duplicate live element keys cannot occur (identities
  are deduplicated upstream), so a key is counted once per worker and a same-key
  re-add is a no-op.
- Make WorkerSetElement.terminating a required boolean (initialized false) and
  narrow the element key from PropertyKey to string, matching the sole caller
  (hashId) and avoiding non-serializable keys.

Tests: add reject-path coverage (removeElement resolves and cleans up when
terminate() rejects; an in-flight addition is still rejected on stop when
terminate() rejects). Mutation-verified. All project gates green.

* test(worker): align WorkerSet internals view element key type to string

Coherence with the production narrowing of the element key from PropertyKey
to string; test-only, no runtime change.

* docs(worker): drop the worker-termination note from the README
src/charging-station/Bootstrap.ts
src/worker/WorkerAbstract.ts
src/worker/WorkerDynamicPool.ts
src/worker/WorkerFactory.ts
src/worker/WorkerFixedPool.ts
src/worker/WorkerSet.ts
src/worker/WorkerTypes.ts
tests/worker/WorkerSet.test.ts [new file with mode: 0644]
tests/worker/fixtures/echoWorker.mjs [new file with mode: 0644]