@@ -25,8 +25,24 @@ const (
2525 programStartSentinelPrefix = "HYPEMAN-PROGRAM-START "
2626 agentReadySentinelPrefix = "HYPEMAN-AGENT-READY "
2727 bootMarkerRescanInterval = 1 * time .Second
28+ // hypervisorStateCacheTTL bounds how long a cached hypervisor state may be
29+ // reused before a fresh GetVMInfo call is required. Short enough to detect
30+ // guest-driven shutdowns promptly, long enough that bursty list calls
31+ // collapse onto one underlying socket query.
32+ hypervisorStateCacheTTL = 5 * time .Second
33+ // getVMInfoTimeout bounds a single hypervisor /vm.info query. The cloud
34+ // hypervisor API socket serializes requests, so a snapshot in flight can
35+ // otherwise park derive_state for tens of seconds.
36+ getVMInfoTimeout = 500 * time .Millisecond
2837)
2938
39+ // hypervisorStateCacheEntry stores the last observed hypervisor VM state for
40+ // an instance, used to skip /vm.info queries on hot list paths.
41+ type hypervisorStateCacheEntry struct {
42+ state hypervisor.VMState
43+ refreshedAt time.Time
44+ }
45+
3046// stateResult holds the result of state derivation
3147type stateResult struct {
3248 State State
@@ -56,39 +72,55 @@ func (m *manager) deriveStateWithOptions(ctx context.Context, stored *StoredMeta
5672 // 1. Check if socket exists
5773 if _ , err := os .Stat (stored .SocketPath ); err != nil {
5874 // No socket - check for snapshot to distinguish Stopped vs Standby
75+ m .invalidateCachedHypervisorState (stored .Id )
5976 if m .hasSnapshot (stored .DataDir ) {
6077 return stateResult {State : StateStandby }
6178 }
6279 return stateResult {State : StateStopped }
6380 }
6481
65- // 2. Socket exists - query hypervisor for actual state
66- hv , err := m .getHypervisor (stored .SocketPath , stored .HypervisorType )
67- if err != nil {
68- // Failed to create client - this is unexpected if socket exists
69- errMsg := fmt .Sprintf ("failed to create hypervisor client: %v" , err )
70- log .WarnContext (ctx , "failed to determine instance state" ,
71- "instance_id" , stored .Id ,
72- "socket" , stored .SocketPath ,
73- "error" , err ,
74- )
75- return stateResult {State : StateUnknown , Error : & errMsg }
76- }
82+ // 2. Socket exists - resolve hypervisor state, preferring the in-memory
83+ // cache (populated by lifecycle events and prior queries) and falling
84+ // back to a time-bounded /vm.info call.
85+ var hvState hypervisor.VMState
86+ if cached , ok := m .loadCachedHypervisorState (stored .Id ); ok {
87+ span .SetAttributes (attribute .Bool ("hypervisor_state.cache_hit" , true ))
88+ hvState = cached
89+ } else {
90+ span .SetAttributes (attribute .Bool ("hypervisor_state.cache_hit" , false ))
91+ hv , err := m .getHypervisor (stored .SocketPath , stored .HypervisorType )
92+ if err != nil {
93+ // Failed to create client - this is unexpected if socket exists
94+ errMsg := fmt .Sprintf ("failed to create hypervisor client: %v" , err )
95+ log .WarnContext (ctx , "failed to determine instance state" ,
96+ "instance_id" , stored .Id ,
97+ "socket" , stored .SocketPath ,
98+ "error" , err ,
99+ )
100+ return stateResult {State : StateUnknown , Error : & errMsg }
101+ }
77102
78- info , err := hv .GetVMInfo (ctx )
79- if err != nil {
80- // Socket exists but hypervisor is unreachable - this is unexpected
81- errMsg := fmt .Sprintf ("failed to query hypervisor: %v" , err )
82- log .WarnContext (ctx , "failed to query hypervisor state" ,
83- "instance_id" , stored .Id ,
84- "socket" , stored .SocketPath ,
85- "error" , err ,
86- )
87- return stateResult {State : StateUnknown , Error : & errMsg }
103+ queryCtx , cancel := context .WithTimeout (ctx , getVMInfoTimeout )
104+ info , err := hv .GetVMInfo (queryCtx )
105+ cancel ()
106+ if err != nil {
107+ // Socket exists but hypervisor query failed or timed out. The API
108+ // socket serializes requests, so a snapshot in flight will trip
109+ // the timeout — return Unknown and let the next list call retry.
110+ errMsg := fmt .Sprintf ("failed to query hypervisor: %v" , err )
111+ log .WarnContext (ctx , "failed to query hypervisor state" ,
112+ "instance_id" , stored .Id ,
113+ "socket" , stored .SocketPath ,
114+ "error" , err ,
115+ )
116+ return stateResult {State : StateUnknown , Error : & errMsg }
117+ }
118+ hvState = info .State
119+ m .storeCachedHypervisorState (stored .Id , hvState )
88120 }
89121
90122 // 3. Map hypervisor state to our state
91- switch info . State {
123+ switch hvState {
92124 case hypervisor .StateCreated :
93125 return stateResult {State : StateCreated }
94126 case hypervisor .StateRunning :
@@ -106,15 +138,74 @@ func (m *manager) deriveStateWithOptions(ctx context.Context, stored *StoredMeta
106138 return stateResult {State : StateShutdown }
107139 default :
108140 // Unknown state - log and return Unknown
109- errMsg := fmt .Sprintf ("unexpected hypervisor state: %s" , info . State )
141+ errMsg := fmt .Sprintf ("unexpected hypervisor state: %s" , hvState )
110142 log .WarnContext (ctx , "hypervisor returned unexpected state" ,
111143 "instance_id" , stored .Id ,
112- "hypervisor_state" , info . State ,
144+ "hypervisor_state" , hvState ,
113145 )
114146 return stateResult {State : StateUnknown , Error : & errMsg }
115147 }
116148}
117149
150+ // loadCachedHypervisorState returns the cached hypervisor state for an instance
151+ // when present and within the TTL window. Stale entries are treated as a miss.
152+ func (m * manager ) loadCachedHypervisorState (id string ) (hypervisor.VMState , bool ) {
153+ v , ok := m .hypervisorStateCache .Load (id )
154+ if ! ok {
155+ return "" , false
156+ }
157+ entry , ok := v .(hypervisorStateCacheEntry )
158+ if ! ok {
159+ return "" , false
160+ }
161+ if m .nowUTC ().Sub (entry .refreshedAt ) > hypervisorStateCacheTTL {
162+ return "" , false
163+ }
164+ return entry .state , true
165+ }
166+
167+ // storeCachedHypervisorState records the latest known hypervisor state for an
168+ // instance so subsequent derive_state calls within the TTL window avoid the
169+ // socket round-trip.
170+ func (m * manager ) storeCachedHypervisorState (id string , state hypervisor.VMState ) {
171+ m .hypervisorStateCache .Store (id , hypervisorStateCacheEntry {
172+ state : state ,
173+ refreshedAt : m .nowUTC (),
174+ })
175+ }
176+
177+ // invalidateCachedHypervisorState drops the cached hypervisor state for an
178+ // instance. Called when the instance no longer has a live hypervisor socket
179+ // (Stopped, Standby, Deleted) or transitions through a state where the cached
180+ // value would be stale.
181+ func (m * manager ) invalidateCachedHypervisorState (id string ) {
182+ m .hypervisorStateCache .Delete (id )
183+ }
184+
185+ // updateCachedHypervisorStateFromInstance reconciles the hypervisor state
186+ // cache with the post-transition instance state observed by a lifecycle
187+ // event. Lifecycle events are the source of truth for state changes hypeman
188+ // itself drives; mirroring them into the cache avoids re-querying /vm.info
189+ // on the next list call.
190+ func (m * manager ) updateCachedHypervisorStateFromInstance (inst * Instance ) {
191+ if inst == nil {
192+ return
193+ }
194+ switch inst .State {
195+ case StateRunning , StateInitializing :
196+ m .storeCachedHypervisorState (inst .Id , hypervisor .StateRunning )
197+ case StateCreated :
198+ m .storeCachedHypervisorState (inst .Id , hypervisor .StateCreated )
199+ case StatePaused :
200+ m .storeCachedHypervisorState (inst .Id , hypervisor .StatePaused )
201+ case StateShutdown :
202+ m .storeCachedHypervisorState (inst .Id , hypervisor .StateShutdown )
203+ default :
204+ // Stopped, Standby, Unknown — no live socket worth caching.
205+ m .invalidateCachedHypervisorState (inst .Id )
206+ }
207+ }
208+
118209func deriveRunningState (stored * StoredMetadata ) State {
119210 if stored .ProgramStartedAt == nil {
120211 return StateInitializing
0 commit comments