@@ -20,6 +20,7 @@ package glutton
2020import (
2121 "bytes"
2222 "context"
23+ "errors"
2324 "fmt"
2425 "io"
2526 "log/slog"
@@ -113,16 +114,27 @@ func (r *taskRuntime) iterate() {
113114 }
114115 user := val .(* gluttonUser )
115116
116- ctx := context .Background ()
117- if ! user .resume (ctx ) {
118- return
119- }
120- user .ping (ctx )
121- user .suspend (ctx )
117+ _ = user .runIteration (context .Background ())
122118
123119 time .Sleep (r .dynamicWait ())
124120}
125121
122+ // runIteration is one boomer iteration for a single user: Resume, then
123+ // (on successful resume) Ping and Suspend. Ping and Suspend both run
124+ // regardless of Ping's outcome so a failed ping still puts the actor
125+ // back to Suspended. Returned error aggregates whatever went wrong;
126+ // production callers ignore it (failures land in metrics inside
127+ // tracedCall), tests use it to fail loudly on contract breakage.
128+ func (u * gluttonUser ) runIteration (ctx context.Context ) error {
129+ ok , err := u .resume (ctx )
130+ if ! ok {
131+ return err
132+ }
133+ pingErr := u .ping (ctx )
134+ suspendErr := u .suspend (ctx )
135+ return errors .Join (pingErr , suspendErr )
136+ }
137+
126138func (r * taskRuntime ) startUser (ctx context.Context ) (* gluttonUser , error ) {
127139 u := & gluttonUser {
128140 cfg : r .cfg ,
@@ -150,9 +162,9 @@ func (r *taskRuntime) shutdown(ctx context.Context) {
150162 r .users .Range (func (_ , val any ) bool {
151163 u := val .(* gluttonUser )
152164 if u .actorRunning {
153- u .suspend (ctx )
165+ _ = u .suspend (ctx )
154166 }
155- u .delete (ctx )
167+ _ = u .delete (ctx )
156168 bmetrics .UpdateUsers (userClass , - 1 )
157169 return true
158170 })
@@ -209,7 +221,11 @@ func (u *gluttonUser) create(ctx context.Context) error {
209221 })
210222}
211223
212- func (u * gluttonUser ) resume (ctx context.Context ) bool {
224+ // resume returns (ok, err): ok mirrors iterate()'s "should I keep going"
225+ // gate, err carries the underlying gRPC error so contract tests can fail
226+ // loudly. Production callers ignore err — the failure is already reported
227+ // to metrics inside tracedCall.
228+ func (u * gluttonUser ) resume (ctx context.Context ) (bool , error ) {
213229 metricName := "ResumeActor"
214230 if u .firstResume {
215231 metricName = "ResumeActorColdStart"
@@ -222,25 +238,26 @@ func (u *gluttonUser) resume(ctx context.Context) bool {
222238 return err
223239 })
224240 if err != nil {
225- return false
241+ return false , err
226242 }
227243 u .firstResume = false
228244 u .actorRunning = true
229- return true
245+ return true , nil
230246}
231247
232- func (u * gluttonUser ) suspend (ctx context.Context ) {
233- _ = u .tracedCall (ctx , "SuspendActor" , func (callCtx context.Context , tr * metadata.MD ) error {
248+ func (u * gluttonUser ) suspend (ctx context.Context ) error {
249+ err : = u .tracedCall (ctx , "SuspendActor" , func (callCtx context.Context , tr * metadata.MD ) error {
234250 _ , err := u .cfg .APIStub .SuspendActor (callCtx , & ateapipb.SuspendActorRequest {
235251 ActorRef : u .ref (),
236252 }, grpc .Trailer (tr ))
237253 return err
238254 })
239255 u .actorRunning = false
256+ return err
240257}
241258
242- func (u * gluttonUser ) delete (ctx context.Context ) {
243- _ = u .tracedCall (ctx , "DeleteActor" , func (callCtx context.Context , tr * metadata.MD ) error {
259+ func (u * gluttonUser ) delete (ctx context.Context ) error {
260+ return u .tracedCall (ctx , "DeleteActor" , func (callCtx context.Context , tr * metadata.MD ) error {
244261 _ , err := u .cfg .APIStub .DeleteActor (callCtx , & ateapipb.DeleteActorRequest {
245262 ActorRef : u .ref (),
246263 }, grpc .Trailer (tr ))
@@ -274,21 +291,24 @@ func (u *gluttonUser) tracedCall(ctx context.Context, name string, do func(conte
274291 return nil
275292}
276293
277- func (u * gluttonUser ) ping (ctx context.Context ) {
294+ // ping returns nil on a successful echo. Production callers ignore the
295+ // return value — failures are already reported to metrics — but contract
296+ // tests use it to fail loudly on router/actor breakage.
297+ func (u * gluttonUser ) ping (ctx context.Context ) error {
278298 ctx , span := u .cfg .Tracer .Start (ctx , "GluttonPing" )
279299 defer span .End ()
280300
281301 message := uuid .NewString ()
282302 body , err := proto .Marshal (& gluttonpb.PingRequest {Message : message })
283303 if err != nil {
284304 bmetrics .RecordFailure ("http" , "GluttonPing" , userClass , 0 , err .Error ())
285- return
305+ return err
286306 }
287307
288308 httpReq , err := http .NewRequestWithContext (ctx , http .MethodPost , u .cfg .RouterURL + pingPath , bytes .NewReader (body ))
289309 if err != nil {
290310 bmetrics .RecordFailure ("http" , "GluttonPing" , userClass , 0 , err .Error ())
291- return
311+ return err
292312 }
293313 httpReq .Host = u .hostHeader
294314 httpReq .Header .Set ("Content-Type" , "application/x-protobuf" )
@@ -299,37 +319,38 @@ func (u *gluttonUser) ping(ctx context.Context) {
299319 clientLatency := time .Since (start )
300320 if err != nil {
301321 bmetrics .RecordFailure ("http" , "GluttonPing" , userClass , clientLatency , err .Error ())
302- return
322+ return err
303323 }
304324 defer resp .Body .Close ()
305325
306326 respBody , readErr := io .ReadAll (resp .Body )
307327 if readErr != nil {
308328 bmetrics .RecordFailure ("http" , "GluttonPing" , userClass , clientLatency , readErr .Error ())
309- return
329+ return readErr
310330 }
311331
312332 if resp .StatusCode >= 400 {
313333 httpErr := fmt .Errorf ("HTTP %d: %s" , resp .StatusCode , strings .TrimSpace (string (respBody )))
314334 logSampledTrace (span , "GluttonPing" , clientLatency , sourceClient , httpErr )
315335 bmetrics .RecordFailure ("http" , "GluttonPing" , userClass , clientLatency , httpErr .Error ())
316- return
336+ return httpErr
317337 }
318338
319339 pong := & gluttonpb.PingResponse {}
320340 if err := proto .Unmarshal (respBody , pong ); err != nil {
321341 logSampledTrace (span , "GluttonPing" , clientLatency , sourceClient , err )
322342 bmetrics .RecordFailure ("http" , "GluttonPing" , userClass , clientLatency , err .Error ())
323- return
343+ return err
324344 }
325345 if pong .Message != message {
326346 mismatch := fmt .Errorf ("ping echo mismatch: sent=%q recv=%q" , message , pong .Message )
327347 logSampledTrace (span , "GluttonPing" , clientLatency , sourceClient , mismatch )
328348 bmetrics .RecordFailure ("http" , "GluttonPing" , userClass , clientLatency , mismatch .Error ())
329- return
349+ return mismatch
330350 }
331351 logSampledTrace (span , "GluttonPing" , clientLatency , sourceClient , nil )
332352 bmetrics .RecordSuccess ("http" , "GluttonPing" , userClass , clientLatency , int64 (len (respBody )))
353+ return nil
333354}
334355
335356// logSampledTrace emits a single structured line per sampled span. Operators
0 commit comments