aquaman
aquaman

Reputation: 1658

Golang: using multiple tickers cases in single select blocks entire loop

I have a requirements where I need to do multiple things (irrelevant here) at some regular intervals. I achieved it using the code block mentioned below -

func (processor *Processor) process() {
    defaultTicker := time.NewTicker(time.Second*2)
    updateTicker := time.NewTicker(time.Second*5)
    heartbeatTicker := time.NewTicker(time.Second*5)
    timeoutTicker := time.NewTicker(30*time.Second)
    refreshTicker := time.NewTicker(2*time.Minute)
    defer func() {
        logger.Info("processor for ", processor.id, " exited")
        defaultTicker.Stop()
        timeoutTicker.Stop()
        updateTicker.Stop()
        refreshTicker.Stop()
        heartbeatTicker.Stop()
    }()
    for {
        select {
        case <-defaultTicker.C:
            // spawn some go routines
        case <-updateTicker.C:
            // do something
        case <-timeoutTicker.C:
            // do something else
        case <-refreshTicker.C:
            // log
        case <-heartbeatTicker.C:
            // push metrics to redis
        }
    }
}

But I noticed that every once in a while, my for select loop gets stuck somewhere and I cannot seem to find where or why. By stuck I mean I stop receiving refresh ticker logs. But it starts working again normally in some time (5-10 mins)

I have made sure that all operations within each ticker completes within very little amount of time (~0ms, checked by putting logs).

My questions:

  1. Is using multiple tickers in single select a good/normal practice (honestly I did not find many examples using multiple tickers online)
  2. Anyone aware of any known issues/pitfalls where tickers can block the loop for longer duration.

Any help is appreciated. Thanks

Upvotes: 0

Views: 2941

Answers (1)

Juve
Juve

Reputation: 10834

Go does not provide any smart draining behavior for multiple channels, e.g., that older messages in one channel would get processed earlier than more recent messages in other channels. Anytime the loop enters the select statement a random channel is chosen.

Also see this answer and read the part about GOMAXPROCS=1. This could be related to your issue. The issue could also be in your logging package. Maybe the logs are just delayed. In general, I think the issue must be in your case statements. Either you have a blocking function or some dysfunctional code. (Note: confirmed by the OP)

But to answer your questions:

1. Is using multiple tickers in single select a good/normal practice?

It is common to read from multiple channels randomly in a blocking way, one message at a time, e.g., to sort incoming data from multiple channels into a slice or map and avoid concurrent data access. It is also common to add one or more tickers, e.g., to flush data and for logging or reporting. Usually the non-ticker code paths will do most of the work.

In your case, you use tickers that will run code paths that should block each other, which is a very specific use case but may be required in some scenarios. This uncommon, but not bad practice I think. As the commenters suggested, you could also schedule different recurring tasks in separate goroutines.

2. Is anyone aware of any known issues/pitfalls where tickers can block the loop for longer duration?

The tickers themselves will not block the loop in any hidden way. The fastest ticker will always ensure the loop is looping at the speed of this ticker at least.

Note that the docs of time.NewTicker say:

The ticker will adjust the time interval or drop ticks to make up for slow receivers

This just means, internally no new ticks are scheduled until you have consumed the last one from the single-element ticker channel.

In your example, the main pitfall is that any code in the case statements will block and thus delay the other cases. If this is intended, everything is fine.

There may be other pitfalls if you have Microsecond or Nanosecond tickers where you may see some measurable runtime overhead or if you have hundreds of tickers and case blocks. But then you should have chose another scheduling pattern from the beginning.

Upvotes: 1

Related Questions