mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 09:57:57 -04:00
Avoid holding the global loader lock across backend lifecycle waits and propagate forced shutdown through distributed workers. Track parallel requests with in-flight counters and reserve worker ports until process termination. Add focused race tests and an authoritative FizzBee lifecycle model with a fail-closed conformance target. Assisted-by: Codex:GPT-5 [FizzBee] [Ginkgo] Signed-off-by: Richard Palethorpe <io@richiejp.com>
196 lines
6.6 KiB
Plaintext
196 lines
6.6 KiB
Plaintext
---
|
|
# Formal design of the model-loader shutdown paths exercised by pkg/model and
|
|
# distributed workers. This is deliberately not a realtime model: realtime
|
|
# merely made a loader-wide stall visible while loading the shared opus backend.
|
|
#
|
|
# The model distinguishes four cases:
|
|
# 1. Local forced shutdown makes progress.
|
|
# 2. Graceful busy waiting is bounded without holding the global loader lock.
|
|
# 3. Distributed force reaches the worker, skips Free(), and reserves its port
|
|
# until process termination completes.
|
|
# 4. Parallel request tracking remains busy until every request completes.
|
|
deadlock_detection: false
|
|
options:
|
|
max_actions: 30
|
|
max_concurrent_actions: 2
|
|
---
|
|
|
|
role LocalForce:
|
|
action Init:
|
|
self.backend = 1 # 1 = stuck busy, 2 = stopped
|
|
self.timed_out = 0
|
|
self.loader = 0 # 0 = free, 1 = held by shutdown
|
|
self.other = 0 # 0 = absent, 1 = pending, 2 = loaded
|
|
|
|
atomic action RequestOtherLoad:
|
|
if self.other == 0:
|
|
self.other = 1
|
|
|
|
atomic fair action BusyTimeout:
|
|
if self.timed_out == 0:
|
|
self.timed_out = 1
|
|
|
|
# ShutdownModelForce skips both the IsBusy loop and Free RPC. Stopping the
|
|
# local process and releasing the loader are one abstract transition; the
|
|
# process manager's eventual termination is the fairness assumption.
|
|
atomic fair action ForceShutdown:
|
|
if self.timed_out == 1 and self.backend == 1:
|
|
self.loader = 1
|
|
self.backend = 2
|
|
self.loader = 0
|
|
|
|
atomic fair action CompleteOtherLoad:
|
|
if self.other == 1 and self.loader == 0:
|
|
self.other = 2
|
|
|
|
role GracefulShutdown:
|
|
action Init:
|
|
self.backend = 1 # permanently stuck busy
|
|
self.waiting = 0
|
|
self.done = 0 # bounded wait returned ErrModelBusy
|
|
self.global_loader = 0
|
|
self.other = 0
|
|
|
|
atomic action RequestOtherLoad:
|
|
if self.other == 0:
|
|
self.other = 1
|
|
|
|
atomic action BeginGracefulShutdown:
|
|
if self.waiting == 0 and self.done == 0:
|
|
self.waiting = 1
|
|
|
|
# The backend is allowed to remain busy forever. The shutdown context is
|
|
# the fairness boundary: expiry returns ErrModelBusy and releases only the
|
|
# per-model operation lock; the global loader lock was never acquired.
|
|
atomic fair action BusyDeadline:
|
|
if self.waiting == 1:
|
|
self.waiting = 0
|
|
self.done = 1
|
|
|
|
atomic fair action CompleteOtherLoad:
|
|
if self.other == 1 and self.global_loader == 0:
|
|
self.other = 2
|
|
|
|
role DistributedForce:
|
|
action Init:
|
|
self.timed_out = 0
|
|
self.stop_sent = 0
|
|
self.process = 1 # 1 = running
|
|
self.supervisor_tracked = 1
|
|
self.port_recycled = 0
|
|
self.stopping = 0
|
|
self.free_called = 0
|
|
self.reinstall = 0
|
|
|
|
atomic fair action BusyTimeout:
|
|
if self.timed_out == 0:
|
|
self.timed_out = 1
|
|
|
|
# The controller publishes force=true in BackendStopRequest.
|
|
atomic fair action SendRemoteStop:
|
|
if self.timed_out == 1 and self.stop_sent == 0:
|
|
self.stop_sent = 1
|
|
|
|
# Force marks the process as stopping but keeps both supervisor ownership
|
|
# and the port reservation. The Free RPC is skipped.
|
|
atomic fair action WorkerReceivesStop:
|
|
if self.stop_sent == 1 and self.supervisor_tracked == 1:
|
|
self.stopping = 1
|
|
|
|
# Only completed process termination releases supervisor ownership and the
|
|
# port. Eventual termination is the process-manager fairness assumption.
|
|
atomic fair action ProcessStops:
|
|
if self.stopping == 1 and self.process == 1:
|
|
self.process = 0
|
|
self.supervisor_tracked = 0
|
|
self.port_recycled = 1
|
|
|
|
atomic action RequestReinstall:
|
|
if self.port_recycled == 1 and self.reinstall == 0:
|
|
self.reinstall = 1
|
|
|
|
role ParallelBusyTracker:
|
|
action Init:
|
|
self.inflight = 0
|
|
self.busy = 0
|
|
|
|
atomic action StartFirst:
|
|
if self.inflight == 0:
|
|
self.inflight = 1
|
|
self.busy = 1
|
|
|
|
atomic action StartSecond:
|
|
if self.inflight == 1 and self.busy == 1:
|
|
self.inflight = 2
|
|
self.busy = 1
|
|
|
|
atomic action FinishOne:
|
|
if self.inflight == 2:
|
|
self.inflight = 1
|
|
self.busy = 1
|
|
|
|
atomic action FinishLast:
|
|
if self.inflight == 1:
|
|
self.inflight = 0
|
|
self.busy = 0
|
|
|
|
action Init:
|
|
local = LocalForce()
|
|
graceful = GracefulShutdown()
|
|
remote = DistributedForce()
|
|
tracker = ParallelBusyTracker()
|
|
|
|
# Proven behavior of the implementation's local force path. Once selected by the watchdog,
|
|
# the stuck backend is eventually stopped and an unrelated pending load can
|
|
# eventually acquire the loader.
|
|
always eventually assertion LocalTimedOutBackendStops:
|
|
return local.timed_out == 0 or local.backend == 2
|
|
|
|
always eventually assertion LocalUnrelatedLoadProgresses:
|
|
return local.other != 1
|
|
|
|
always assertion LocalNeverWaitsBusyWithLoaderHeld:
|
|
return not (local.loader == 1 and local.backend == 1)
|
|
|
|
exists assertion LocalForcePathExercised:
|
|
return local.timed_out == 1 and local.backend == 2
|
|
|
|
exists assertion LocalOtherLoadCompletes:
|
|
return local.other == 2
|
|
|
|
# The permanently busy backend cannot wedge the global loader, and its bounded
|
|
# busy-wait attempt eventually returns.
|
|
always assertion GracefulNeverHoldsGlobalLoader:
|
|
return graceful.global_loader == 0
|
|
|
|
always eventually assertion GracefulShutdownIsBounded:
|
|
return graceful.waiting == 0
|
|
|
|
always eventually assertion GracefulUnrelatedLoadProgresses:
|
|
return graceful.other != 1
|
|
|
|
exists assertion GracefulDeadlineExercised:
|
|
return graceful.done == 1 and graceful.other == 2
|
|
|
|
# Distributed force skips Free, terminates the process, and never recycles its
|
|
# port while the old process can still own it.
|
|
always assertion DistributedForceSkipsFree:
|
|
return remote.free_called == 0
|
|
|
|
always assertion DistributedPortReservedUntilStop:
|
|
return not (remote.port_recycled == 1 and remote.process == 1)
|
|
|
|
always eventually assertion DistributedForcedStopProgresses:
|
|
return remote.stop_sent == 0 or remote.process == 0
|
|
|
|
exists assertion DistributedForcedStopExercised:
|
|
return remote.process == 0 and remote.port_recycled == 1 and remote.free_called == 0
|
|
|
|
# Busy is derived from the in-flight count, so a partial completion cannot
|
|
# expose a false-idle state.
|
|
always assertion ParallelBusyMatchesInflight:
|
|
return (tracker.inflight > 0 and tracker.busy == 1) or (tracker.inflight == 0 and tracker.busy == 0)
|
|
|
|
exists assertion ParallelOverlapPreserved:
|
|
return tracker.inflight == 1 and tracker.busy == 1
|