2121import org .springframework .core .env .Environment ;
2222import org .springframework .test .util .ReflectionTestUtils ;
2323
24+ import com .imageworks .spcue .FrameDetail ;
2425import com .imageworks .spcue .FrameInterface ;
26+ import com .imageworks .spcue .HostInterface ;
2527import com .imageworks .spcue .VirtualProc ;
2628import com .imageworks .spcue .dao .FrameDao ;
29+ import com .imageworks .spcue .dao .HostDao ;
2730import com .imageworks .spcue .dao .JobDao ;
2831import com .imageworks .spcue .dao .LayerDao ;
2932import com .imageworks .spcue .dao .ProcDao ;
@@ -56,11 +59,14 @@ public class DispatchSupportServiceLostProcTests {
5659
5760 private static final String KILL_BEFORE_RELEASE_PROPERTY =
5861 "dispatcher.kill_running_frame_before_release_enabled" ;
62+ private static final String DEFER_RELEASE_PROPERTY =
63+ "dispatcher.defer_release_on_failed_kill_enabled" ;
5964
6065 private DispatchSupportService dispatchSupport ;
6166 private RqdClient rqdClient ;
6267 private ProcDao procDao ;
6368 private FrameDao frameDao ;
69+ private HostDao hostDao ;
6470 private Environment env ;
6571 private VirtualProc proc ;
6672
@@ -70,19 +76,26 @@ public void setup() {
7076 rqdClient = mock (RqdClient .class );
7177 procDao = mock (ProcDao .class );
7278 frameDao = mock (FrameDao .class );
79+ hostDao = mock (HostDao .class );
7380 env = mock (Environment .class );
7481
7582 dispatchSupport .setRqdClient (rqdClient );
7683 dispatchSupport .setProcDao (procDao );
7784 dispatchSupport .setFrameDao (frameDao );
85+ dispatchSupport .setHostDao (hostDao );
7886 dispatchSupport .setShowDao (mock (ShowDao .class ));
7987 dispatchSupport .setJobDao (mock (JobDao .class ));
8088 dispatchSupport .setLayerDao (mock (LayerDao .class ));
8189 ReflectionTestUtils .setField (dispatchSupport , "env" , env );
8290
83- // Kill-before-release enabled by default.
91+ // Kill-before-release and defer-release both enabled by default.
8492 when (env .getProperty (eq (KILL_BEFORE_RELEASE_PROPERTY ), eq (Boolean .class ), eq (true )))
8593 .thenReturn (true );
94+ when (env .getProperty (eq (DEFER_RELEASE_PROPERTY ), eq (Boolean .class ), eq (true )))
95+ .thenReturn (true );
96+
97+ // Default: the host still looks Up in the DB (the dangerous flapping case).
98+ when (hostDao .isHostUp (any (HostInterface .class ))).thenReturn (true );
8699
87100 proc = new VirtualProc ();
88101 proc .id = "00000000-0000-0000-0000-000000000001" ;
@@ -102,27 +115,111 @@ public void killsRqdBeforeReleasingProc() {
102115 InOrder inOrder = inOrder (rqdClient , procDao );
103116 inOrder .verify (rqdClient , times (1 )).killFrame (eq (proc ), anyString ());
104117 inOrder .verify (procDao , times (1 )).deleteVirtualProc (proc );
118+ verify (frameDao , times (1 )).updateFrameStopped (any (FrameInterface .class ),
119+ eq (FrameState .WAITING ), anyInt ());
105120 }
106121
107122 @ Test
108123 public void skipsRqdKillForFailedKillExitStatus () {
109124 // The failed-kill path already attempted this exact kill, so lostProc must not re-issue it.
125+ // Since that kill demonstrably failed and the host still looks Up, the release is deferred
126+ // to avoid double-booking a possibly-still-rendering host.
127+ dispatchSupport .lostProc (proc , "failed kill" , Dispatcher .EXIT_STATUS_FAILED_KILL );
128+
129+ verify (rqdClient , never ()).killFrame (any (VirtualProc .class ), anyString ());
130+ verify (procDao , never ()).deleteVirtualProc (any (VirtualProc .class ));
131+ verify (frameDao , never ()).updateFrameStopped (any (FrameInterface .class ),
132+ any (FrameState .class ), anyInt ());
133+ }
134+
135+ @ Test
136+ public void releasesForFailedKillWhenHostConfirmedDownByDb () {
137+ // FAILED_KILL but the host is no longer Up in the DB: no live RQD remains, release
138+ // proceeds.
139+ when (hostDao .isHostUp (any (HostInterface .class ))).thenReturn (false );
140+
110141 dispatchSupport .lostProc (proc , "failed kill" , Dispatcher .EXIT_STATUS_FAILED_KILL );
111142
112143 verify (rqdClient , never ()).killFrame (any (VirtualProc .class ), anyString ());
113144 verify (procDao , times (1 )).deleteVirtualProc (proc );
145+ verify (frameDao , times (1 )).updateFrameStopped (any (FrameInterface .class ),
146+ eq (FrameState .WAITING ), anyInt ());
114147 }
115148
116149 @ Test
117- public void releasesProcWhenRqdKillThrows () {
118- // A genuinely dead/unreachable host makes the kill throw; release must still proceed.
150+ public void releasesProcWhenRqdKillThrowsForDownHost () {
151+ // A host reported DOWN is confirmed dead: the kill may throw, but release must still
152+ // proceed.
119153 doThrow (new RuntimeException ("host unreachable" )).when (rqdClient )
120154 .killFrame (any (VirtualProc .class ), anyString ());
121155
122156 dispatchSupport .lostProc (proc , "down host" , Dispatcher .EXIT_STATUS_DOWN_HOST );
123157
124158 verify (rqdClient , times (1 )).killFrame (eq (proc ), anyString ());
125159 verify (procDao , times (1 )).deleteVirtualProc (proc );
160+ verify (frameDao , times (1 )).updateFrameStopped (any (FrameInterface .class ),
161+ eq (FrameState .WAITING ), anyInt ());
162+ }
163+
164+ @ Test
165+ public void updatesFrameHostDownWhenFrameNotRunningForDownHost () {
166+ // Down host whose frame is no longer RUNNING but already DEAD: updateFrameStopped reports
167+ // nothing to stop, so the down-host fallback must reset it via updateFrameHostDown.
168+ when (frameDao .updateFrameStopped (any (FrameInterface .class ), any (FrameState .class ),
169+ anyInt ())).thenReturn (false );
170+ FrameDetail deadFrame = new FrameDetail ();
171+ deadFrame .state = FrameState .DEAD ;
172+ when (frameDao .getFrameDetail (any (FrameInterface .class ))).thenReturn (deadFrame );
173+
174+ dispatchSupport .lostProc (proc , "down host" , Dispatcher .EXIT_STATUS_DOWN_HOST );
175+
176+ verify (procDao , times (1 )).deleteVirtualProc (proc );
177+ verify (frameDao , times (1 )).updateFrameHostDown (any (FrameInterface .class ));
178+ }
179+
180+ @ Test
181+ public void defersReleaseWhenKillThrowsAndHostStillUp () {
182+ // The flapping-host case: the kill throws and the host still looks Up. Releasing now would
183+ // re-book the frame onto a second host while this RQD keeps rendering, so we must defer.
184+ doThrow (new RuntimeException ("transient unreachable" )).when (rqdClient )
185+ .killFrame (any (VirtualProc .class ), anyString ());
186+
187+ dispatchSupport .lostProc (proc , "orphaned" , Dispatcher .EXIT_STATUS_FRAME_ORPHAN );
188+
189+ verify (rqdClient , times (1 )).killFrame (eq (proc ), anyString ());
190+ verify (procDao , never ()).deleteVirtualProc (any (VirtualProc .class ));
191+ verify (frameDao , never ()).updateFrameStopped (any (FrameInterface .class ),
192+ any (FrameState .class ), anyInt ());
193+ }
194+
195+ @ Test
196+ public void releasesWhenKillThrowsAndHostNotUp () {
197+ // The kill throws but the host is no longer Up: confirmed dead, so release is safe.
198+ doThrow (new RuntimeException ("host unreachable" )).when (rqdClient )
199+ .killFrame (any (VirtualProc .class ), anyString ());
200+ when (hostDao .isHostUp (any (HostInterface .class ))).thenReturn (false );
201+
202+ dispatchSupport .lostProc (proc , "orphaned" , Dispatcher .EXIT_STATUS_FRAME_ORPHAN );
203+
204+ verify (rqdClient , times (1 )).killFrame (eq (proc ), anyString ());
205+ verify (procDao , times (1 )).deleteVirtualProc (proc );
206+ verify (frameDao , times (1 )).updateFrameStopped (any (FrameInterface .class ),
207+ eq (FrameState .WAITING ), anyInt ());
208+ }
209+
210+ @ Test
211+ public void releasesWhenDeferDisabledByPropertyEvenIfKillThrows () {
212+ // With defer disabled, behavior falls back to the prior always-release semantics.
213+ when (env .getProperty (eq (DEFER_RELEASE_PROPERTY ), eq (Boolean .class ), eq (true )))
214+ .thenReturn (false );
215+ doThrow (new RuntimeException ("transient unreachable" )).when (rqdClient )
216+ .killFrame (any (VirtualProc .class ), anyString ());
217+
218+ dispatchSupport .lostProc (proc , "orphaned" , Dispatcher .EXIT_STATUS_FRAME_ORPHAN );
219+
220+ verify (procDao , times (1 )).deleteVirtualProc (proc );
221+ verify (frameDao , times (1 )).updateFrameStopped (any (FrameInterface .class ),
222+ eq (FrameState .WAITING ), anyInt ());
126223 }
127224
128225 @ Test
@@ -134,5 +231,7 @@ public void skipsRqdKillWhenDisabledByProperty() {
134231
135232 verify (rqdClient , never ()).killFrame (any (VirtualProc .class ), anyString ());
136233 verify (procDao , times (1 )).deleteVirtualProc (proc );
234+ verify (frameDao , times (1 )).updateFrameStopped (any (FrameInterface .class ),
235+ eq (FrameState .WAITING ), anyInt ());
137236 }
138237}
0 commit comments