package rollout import ( "context" "time" "github.com/carlosframework/platform/internal/appconfig" ) // configTrack is one instance's two config revisions (spec §8.1). Delivered // is what config sync last wrote; active is the revision the current // generation started with. They are equal at rest. type configTrack struct { delivered string active string // sleeping: the instance had no process when delivered was written // (spec §8.1). Its next wake starts with that file, so no attempt is // pending and the ack goes out at once, marked Hibernating. sleeping bool fails int lastErr string ack *appconfig.ConfigAck ackRev string ackWrite func(context.Context, appconfig.ConfigAck) } // configBackoff is the wait after the nth consecutive failed config attempt: // one minute, doubling, at most thirty (spec §8.1). func configBackoff(n int) time.Duration { d := time.Minute for i := 1; i < n && d < 30*time.Minute; i++ { d *= 2 } return min(d, 30*time.Minute) } // deferAck holds a config ack until the revision it was written for is // active, and returns a func to run now when it already is. A route config // sync never reported a delivery for (a shape the scheduler does not // restart) acknowledges at once, as before. So does a sleeping instance, // with today's hibernating acknowledgement: the config landed and nothing // woke to run it (ConfigAck.Hibernating, "delivered on wake"). func (t *configTrack) deferAck(a appconfig.ConfigAck, write func(context.Context, appconfig.ConfigAck)) func(context.Context) { // A newer delivery supersedes any ack still held for an older one, whether // or not this one waits: releasing the old ack later would write it after // the newer ack, with a revision that is no longer delivered (spec §8.1). t.ack, t.ackWrite, t.ackRev = nil, nil, "" if t.sleeping { a.Hibernating, a.Restarted = true, false return func(ctx context.Context) { write(ctx, a) } } if t.delivered == "" || t.active == t.delivered { return func(ctx context.Context) { write(ctx, a) } } t.ack, t.ackRev, t.ackWrite = &a, t.delivered, write return nil } // activate records rev as active and returns the held ack when it was // waiting for exactly rev and rev is still the delivered revision. func (t *configTrack) activate(rev string) (func(context.Context), bool) { if rev == "" { return nil, false } t.active = rev if rev == t.delivered { t.fails, t.lastErr = 0, "" } if t.ack == nil || t.ackRev != rev || rev != t.delivered { return nil, false } a, write := *t.ack, t.ackWrite t.ack, t.ackWrite, t.ackRev = nil, nil, "" return func(ctx context.Context) { write(ctx, a) }, true }