@startuml failure_indexer_crash title Indexer Failure → replenish from a Native participant "Node" as N participant "Indexer A (alive)" as IA participant "Indexer B (crashed)" as IB participant "Native A" as NA participant "Native B" as NB note over N: Active Pool : Indexers = [IA, IB]\\nActive Heartbeat long-lived from IA & IB == IB Failure == IB ->x N: heartbeat fails (sendHeartbeat err) note over N: doTick() dans SendHeartbeat triggers failure\\n→ delete(Indexers[IB])\\n→ delete(IndexerMeta[IB])\\nUnique heartbeat goroutine continue N -> N: go replenishIndexersFromNative(need=1) note over N: Reduced Pool to 1 indexers.\\nReplenish triggers with goroutine. == Replenish from natives == par Fetch pool (timeout 6s) N -> NA: GET /opencloud/native/indexers/1.0\\nGetIndexersRequest{Count: max} NA -> NA: reachableLiveIndexers()\\n(IB absent because of a expired heartbeat) NA --> N: GetIndexersResponse{Indexers:[IA,IC], FillRates:{IA:0.4,IC:0.2}} else N -> NB: GET /opencloud/native/indexers/1.0 NB --> N: GetIndexersResponse{Indexers:[IA,IC]} end par note over N: Fusion + duplication → candidates = [IA, IC]\\n(IA already in pool → IC new candidate) par Consensus Phase 1 (timeout 4s) N -> NA: /opencloud/native/consensus/1.0\\nConsensusRequest{Candidates:[IA,IC]} NA --> N: ConsensusResponse{Trusted:[IA,IC]} else N -> NB: /opencloud/native/consensus/1.0 NB --> N: ConsensusResponse{Trusted:[IA,IC]} end par note over N: IC → 2/2 votes → admit\\nadmittedAt = time.Now() par Phase 2 — liveness vote (if stable voters ) N -> IA: /opencloud/indexer/consensus/1.0\\nIndexerConsensusRequest{Candidates:[IC]} IA -> IA: StreamRecords[ProtocolHB][IC]\\nLastSeen ≤ 120s && LastScore ≥ 30 IA --> N: IndexerConsensusResponse{Alive:[IC]} end par note over N: IC confirmed alive → add to pool N -> N: replaceStaticIndexers(pool={IA,IC}) N -> IC: SendHeartbeat /opencloud/heartbeat/1.0 (goroutine long-live) note over N: Pool restaured to 2 indexers. @enduml