package mfs import ( "context" "time" cid "github.com/ipfs/go-cid" ) // PubFunc is the user-defined function that determines exactly what // logic entails "publishing" a `Cid` value. type PubFunc func(context.Context, cid.Cid) error // Republisher manages when to publish a given entry. type Republisher struct { TimeoutLong time.Duration TimeoutShort time.Duration RetryTimeout time.Duration pubfunc PubFunc update chan cid.Cid immediatePublish chan chan struct{} ctx context.Context cancel func() } // NewRepublisher creates a new Republisher object to republish the given root // using the given short and long time intervals. func NewRepublisher(ctx context.Context, pf PubFunc, tshort, tlong time.Duration) *Republisher { ctx, cancel := context.WithCancel(ctx) return &Republisher{ TimeoutShort: tshort, TimeoutLong: tlong, RetryTimeout: tlong, update: make(chan cid.Cid, 1), pubfunc: pf, immediatePublish: make(chan chan struct{}), ctx: ctx, cancel: cancel, } } // WaitPub waits for the current value to be published (or returns early // if it already has). func (rp *Republisher) WaitPub(ctx context.Context) error { wait := make(chan struct{}) select { case rp.immediatePublish <- wait: case <-ctx.Done(): return ctx.Err() } select { case <-wait: return nil case <-ctx.Done(): return ctx.Err() } } func (rp *Republisher) Close() error { // TODO(steb): Wait for `Run` to stop err := rp.WaitPub(rp.ctx) rp.cancel() return err } // Update the current value. The value will be published after a delay but each // consecutive call to Update may extend this delay up to TimeoutLong. func (rp *Republisher) Update(c cid.Cid) { select { case <-rp.update: select { case rp.update <- c: default: // Don't try again. If we hit this case, there's a // concurrent publish and we can safely let that // concurrent publish win. } case rp.update <- c: } } // Run contains the core logic of the `Republisher`. It calls the user-defined // `pubfunc` function whenever the `Cid` value is updated to a *new* value. The // complexity comes from the fact that `pubfunc` may be slow so we need to batch // updates. // // Algorithm: // 1. When we receive the first update after publishing, we set a `longer` timer. // 2. When we receive any update, we reset the `quick` timer. // 3. If either the `quick` timeout or the `longer` timeout elapses, // we call `publish` with the latest updated value. // // The `longer` timer ensures that we delay publishing by at most // `TimeoutLong`. The `quick` timer allows us to publish sooner if // it looks like there are no more updates coming down the pipe. // // Note: If a publish fails, we retry repeatedly every TimeoutRetry. func (rp *Republisher) Run(lastPublished cid.Cid) { quick := time.NewTimer(0) if !quick.Stop() { <-quick.C } longer := time.NewTimer(0) if !longer.Stop() { <-longer.C } var toPublish cid.Cid for rp.ctx.Err() == nil { var waiter chan struct{} select { case <-rp.ctx.Done(): return case newValue := <-rp.update: // Skip already published values. if lastPublished.Equals(newValue) { // Break to the end of the switch to cleanup any // timers. toPublish = cid.Undef break } // If we aren't already waiting to publish something, // reset the long timeout. if !toPublish.Defined() { longer.Reset(rp.TimeoutLong) } // Always reset the short timeout. quick.Reset(rp.TimeoutShort) // Finally, set the new value to publish. toPublish = newValue continue case waiter = <-rp.immediatePublish: // Make sure to grab the *latest* value to publish. select { case toPublish = <-rp.update: default: } // Avoid publishing duplicate values if lastPublished.Equals(toPublish) { toPublish = cid.Undef } case <-quick.C: case <-longer.C: } // Cleanup, publish, and close waiters. // 1. Stop any timers. Don't use the `if !t.Stop() { ... }` // idiom as these timers may not be running. quick.Stop() select { case <-quick.C: default: } longer.Stop() select { case <-longer.C: default: } // 2. If we have a value to publish, publish it now. if toPublish.Defined() { for { err := rp.pubfunc(rp.ctx, toPublish) if err == nil { break } // Keep retrying until we succeed or we abort. // TODO(steb): We could try pulling new values // off `update` but that's not critical (and // complicates this code a bit). We'll pull off // a new value on the next loop through. select { case <-time.After(rp.RetryTimeout): case <-rp.ctx.Done(): return } } lastPublished = toPublish toPublish = cid.Undef } // 3. Trigger anything waiting in `WaitPub`. if waiter != nil { close(waiter) } } }