@@ -19,6 +19,7 @@ import (
1919 "testing"
2020 "time"
2121
22+ "github.com/cartesi/rollups-node/internal/appstatus"
2223 "github.com/cartesi/rollups-node/internal/manager"
2324 . "github.com/cartesi/rollups-node/internal/model"
2425 "github.com/cartesi/rollups-node/internal/repository"
@@ -118,6 +119,10 @@ func (s *AdvancerSuite) TestServiceInterface() {
118119 // Test service interface methods
119120 require .True (advancer .Alive ())
120121 require .True (advancer .Ready ())
122+ machineManager .PendingApplicationFailures = true
123+ require .False (advancer .Ready ())
124+ machineManager .PendingApplicationFailures = false
125+ require .True (advancer .Ready ())
121126 require .Empty (advancer .Reload ())
122127 require .Equal (advancer .Name , advancer .String ())
123128
@@ -224,6 +229,40 @@ func (s *AdvancerSuite) TestStep() {
224229 require .Contains (err .Error (), "update machines error" )
225230 })
226231
232+ s .Run ("Error/UnconfirmedApplicationFailureStatus" , func () {
233+ require := s .Require ()
234+ persistenceErr := & manager.ApplicationFailurePersistenceError {
235+ ApplicationID : 7 ,
236+ WriteErr : errors .New ("status write unavailable" ),
237+ ReadErr : errors .New ("status read unavailable" ),
238+ }
239+ healthy := newMockMachine (8 )
240+ machineManager := & MockMachineManager {
241+ Map : map [int64 ]* MockMachineInstance {
242+ healthy .Application .ID : newMockInstance (healthy ),
243+ },
244+ UpdateMachinesError : persistenceErr ,
245+ PendingApplicationFailures : true ,
246+ }
247+ repo := & MockRepository {
248+ GetEpochsReturn : map [common.Address ][]* Epoch {
249+ healthy .Application .IApplicationAddress : {{Index : 0 , Status : EpochStatus_Open }},
250+ },
251+ GetInputsReturn : map [common.Address ][]* Input {
252+ healthy .Application .IApplicationAddress : {
253+ newInput (healthy .Application .ID , 0 , 0 , marshal (randomAdvanceResult (0 ))),
254+ },
255+ },
256+ }
257+ advancer , err := newMockAdvancerService (machineManager , repo )
258+ require .NoError (err )
259+
260+ _ , err = advancer .Step (context .Background ())
261+ require .ErrorIs (err , manager .ErrApplicationFailureNotDurable )
262+ require .Len (repo .StoredResults , 1 , "the healthy application must still advance" )
263+ require .False (advancer .Ready ())
264+ })
265+
227266 s .Run ("Error/GetInputs" , func () {
228267 require := s .Require ()
229268 env := s .setupOneApp ()
@@ -295,6 +334,48 @@ func (s *AdvancerSuite) TestStep() {
295334 // app2's input was processed despite app1's failure
296335 require .Len (repo .StoredResults , 1 )
297336 })
337+
338+ s .Run ("LiveCycleLimitWriteFailureFencesAppWithoutBlockingHealthySibling" , func () {
339+ require := s .Require ()
340+ mm := newMockMachineManager ()
341+ limited := newMockMachine (1 )
342+ healthy := newMockMachine (2 )
343+ limitErr := fmt .Errorf (
344+ "advance execution reached configured cycle limit: %w" ,
345+ pkgmachine .ErrReachedLimitMcycle ,
346+ )
347+ limited .AdvanceError = limitErr
348+ mm .Map [limited .Application .ID ] = newMockInstance (limited )
349+ mm .Map [healthy .Application .ID ] = newMockInstance (healthy )
350+ repo := & MockRepository {
351+ GetEpochsReturn : map [common.Address ][]* Epoch {
352+ limited .Application .IApplicationAddress : {{Index : 0 , Status : EpochStatus_Open }},
353+ healthy .Application .IApplicationAddress : {{Index : 0 , Status : EpochStatus_Open }},
354+ },
355+ GetInputsReturn : map [common.Address ][]* Input {
356+ limited .Application .IApplicationAddress : {
357+ newInput (limited .Application .ID , 0 , 0 , []byte ("limited input" )),
358+ },
359+ healthy .Application .IApplicationAddress : {
360+ newInput (healthy .Application .ID , 0 , 0 , marshal (randomAdvanceResult (0 ))),
361+ },
362+ },
363+ UpdateApplicationStatusError : errors .New ("FAILED write unavailable" ),
364+ }
365+ svc , err := newMockAdvancerService (mm , repo )
366+ require .NoError (err )
367+
368+ _ , err = svc .Step (context .Background ())
369+
370+ require .ErrorIs (err , pkgmachine .ErrReachedLimitMcycle )
371+ require .Equal (
372+ appstatus .NormalizeReason (limitErr .Error ()),
373+ mm .RecordedApplicationFailures [limited .Application .ID ],
374+ )
375+ require .False (svc .Ready (), "an unconfirmed FAILED write must fail readiness immediately" )
376+ require .Len (repo .StoredResults , 1 , "the healthy sibling must still advance" )
377+ require .Equal (healthy .Application .ID , repo .StoredAppIDs [0 ])
378+ })
298379}
299380
300381func (s * AdvancerSuite ) TestGetUnprocessedInputs () {
@@ -601,6 +682,11 @@ func (s *AdvancerSuite) TestContextCancellation() {
601682 // The expired context prevents the immediate database write, so the
602683 // application failure is queued for a later status-write retry.
603684 require .Zero (env .repo .ApplicationStatusUpdates )
685+ require .Equal (
686+ context .DeadlineExceeded .Error (),
687+ env .mm .RecordedApplicationFailures [env .app .Application .ID ],
688+ )
689+ require .True (env .mm .PendingApplicationFailures )
604690 require .Equal (1 , env .mm .Map [env .app .Application .ID ].closeCalls )
605691 require .True (logs .contains (slog .LevelError , "Error executing advance" ))
606692 require .False (logs .contains (
@@ -893,6 +979,23 @@ func (s *AdvancerSuite) TestHandleEpochAfterInputsProcessed() {
893979 require .Equal (ApplicationStatus_Failed , env .repo .LastApplicationStatus )
894980 })
895981
982+ s .Run ("EmptyEpochIndex0ErrMachineClosedWriteFailureQueuesDurableFence" , func () {
983+ require := s .Require ()
984+ env := s .setupOneApp ()
985+ env .app .OutputsProofError = manager .ErrMachineClosed
986+ env .repo .UpdateApplicationStatusError = errors .New ("FAILED write unavailable" )
987+ epoch := & Epoch {Index : 0 , Status : EpochStatus_Closed , InputIndexLowerBound : 0 , InputIndexUpperBound : 0 }
988+
989+ err := env .service .handleEpochAfterInputsProcessed (context .Background (), env .app .Application , epoch )
990+
991+ require .ErrorIs (err , manager .ErrMachineClosed )
992+ require .Equal (
993+ appstatus .NormalizeReason (manager .ErrMachineClosed .Error ()),
994+ env .mm .RecordedApplicationFailures [env .app .Application .ID ],
995+ )
996+ require .False (env .service .Ready ())
997+ })
998+
896999 s .Run ("EmptyEpochIndexGt0RepeatsPreviousProof" , func () {
8971000 require := s .Require ()
8981001 env := s .setupOneApp ()
@@ -1957,13 +2060,16 @@ func newMockInstance(impl *MockMachineImpl) *MockMachineInstance {
19572060// ------------------------------------------------------------------------------------------------
19582061
19592062type MockMachineManager struct {
1960- Map map [int64 ]* MockMachineInstance
1961- UpdateMachinesError error
2063+ Map map [int64 ]* MockMachineInstance
2064+ UpdateMachinesError error
2065+ PendingApplicationFailures bool
2066+ RecordedApplicationFailures map [int64 ]string
19622067}
19632068
19642069func newMockMachineManager () * MockMachineManager {
19652070 return & MockMachineManager {
1966- Map : map [int64 ]* MockMachineInstance {},
2071+ Map : map [int64 ]* MockMachineInstance {},
2072+ RecordedApplicationFailures : map [int64 ]string {},
19672073 }
19682074}
19692075
@@ -1979,6 +2085,14 @@ func (mock *MockMachineManager) UpdateMachines(ctx context.Context) error {
19792085 return mock .UpdateMachinesError
19802086}
19812087
2088+ func (mock * MockMachineManager ) FenceApplicationFailure (app * Application , reason string ) {
2089+ if mock .RecordedApplicationFailures == nil {
2090+ mock .RecordedApplicationFailures = map [int64 ]string {}
2091+ }
2092+ mock .RecordedApplicationFailures [app .ID ] = appstatus .NormalizeReason (reason )
2093+ mock .PendingApplicationFailures = true
2094+ }
2095+
19822096func (mock * MockMachineManager ) Applications () []* Application {
19832097 apps := make ([]* Application , 0 , len (mock .Map ))
19842098 for _ , v := range mock .Map {
@@ -1993,6 +2107,10 @@ func (mock *MockMachineManager) HasMachine(appID int64) bool {
19932107 return exists
19942108}
19952109
2110+ func (mock * MockMachineManager ) HasPendingApplicationFailures () bool {
2111+ return mock .PendingApplicationFailures
2112+ }
2113+
19962114func (mock * MockMachineManager ) Close () error {
19972115 return nil
19982116}
0 commit comments