Ticker provides a channel-based mechanism for retry operations, similar to time.Ticker. It delivers ticks at intervals determined by a backoff policy, allowing integration with Go's channel-based concurrency patterns.
// Ticker holds a channel that delivers ticks of a clock at times
// reported by a BackOff
//
// Ticks will continue to arrive when the previous operation is still running,
// so operations that take a while to fail could run in quick succession
type Ticker struct {
// C is the channel on which ticks are delivered
C <-chan time.Time
}Creates a ticker with the default timer implementation.
// NewTicker returns a new Ticker containing a channel that will send
// the time at times specified by the BackOff argument
//
// Ticker is guaranteed to tick at least once
// The channel is closed when Stop method is called or BackOff stops
//
// It is not safe to manipulate the provided backoff policy
// (notably calling NextBackOff or Reset) while the ticker is running
func NewTicker(b BackOff) *TickerCreates a ticker with a custom timer implementation.
// NewTickerWithTimer returns a new Ticker with a custom timer
// A default timer that uses system timer is used when nil is passed
func NewTickerWithTimer(b BackOff, timer Timer) *Ticker// Timer interface for custom timer implementations
type Timer interface {
// Start the timer to fire after the given duration
// Can be called multiple times to reset the timer
Start(duration time.Duration)
// Stop the timer and free resources
Stop()
// C returns the channel that receives the time when timer fires
// Should return the same channel instance across calls
C() <-chan time.Time
}// Stop turns off a ticker
// After Stop, no more ticks will be sent
func (t *Ticker) Stop()import (
"log"
"github.com/cenkalti/backoff/v4"
)
operation := func() error {
// Your operation that may fail
return performTask()
}
ticker := backoff.NewTicker(backoff.NewExponentialBackOff())
defer ticker.Stop()
var err error
for range ticker.C {
if err = operation(); err != nil {
log.Printf("Operation failed: %v, retrying...", err)
continue
}
// Success - stop the ticker
ticker.Stop()
break
}
if err != nil {
log.Fatalf("Operation failed after retries: %v", err)
}import (
"context"
"time"
"github.com/cenkalti/backoff/v4"
)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
b := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
ticker := backoff.NewTicker(b)
defer ticker.Stop()
var err error
for {
select {
case <-ticker.C:
if err = operation(); err != nil {
log.Printf("Retry attempt failed: %v", err)
continue
}
ticker.Stop()
return nil
case <-ctx.Done():
return ctx.Err()
}
}Integrate ticker with other channel operations:
import (
"context"
"github.com/cenkalti/backoff/v4"
)
func RetryWithUpdates(ctx context.Context) error {
ticker := backoff.NewTicker(
backoff.WithContext(backoff.NewExponentialBackOff(), ctx),
)
defer ticker.Stop()
updateChan := make(chan Update)
for {
select {
case <-ticker.C:
if err := operation(); err != nil {
log.Printf("Retry: %v", err)
continue
}
ticker.Stop()
return nil
case update := <-updateChan:
// Handle updates while retrying
processUpdate(update)
case <-ctx.Done():
return ctx.Err()
}
}
}Track retry attempts with ticker:
import (
"log"
"github.com/cenkalti/backoff/v4"
)
func RetryWithMetrics() error {
ticker := backoff.NewTicker(backoff.NewExponentialBackOff())
defer ticker.Stop()
attemptNum := 0
var err error
for t := range ticker.C {
attemptNum++
log.Printf("Attempt %d at %v", attemptNum, t)
if err = operation(); err != nil {
metrics.RecordRetry(attemptNum, err)
continue
}
ticker.Stop()
metrics.RecordSuccess(attemptNum)
break
}
return err
}Use custom timer for testing or special timing requirements:
import (
"time"
"github.com/cenkalti/backoff/v4"
)
// Mock timer for testing
type MockTimer struct {
ch chan time.Time
}
func (t *MockTimer) Start(duration time.Duration) {
// Custom timing logic
go func() {
time.Sleep(duration)
t.ch <- time.Now()
}()
}
func (t *MockTimer) Stop() {
close(t.ch)
}
func (t *MockTimer) C() <-chan time.Time {
return t.ch
}
// Usage
mockTimer := &MockTimer{ch: make(chan time.Time, 1)}
ticker := backoff.NewTickerWithTimer(
backoff.NewExponentialBackOff(),
mockTimer,
)// Example: Coordinating with multiple channels
select {
case <-ticker.C:
// Retry operation
case msg := <-messageChan:
// Handle message
case <-ctx.Done():
// Handle cancellation
}// Example: Simple retry
err := backoff.Retry(operation, backoff.NewExponentialBackOff())import (
"context"
"fmt"
"net/http"
"time"
"github.com/cenkalti/backoff/v4"
)
func FetchWithRetry(ctx context.Context, url string) ([]byte, error) {
b := backoff.WithContext(
backoff.NewExponentialBackOff(
backoff.WithMaxElapsedTime(2*time.Minute),
),
ctx,
)
ticker := backoff.NewTicker(b)
defer ticker.Stop()
var (
resp *http.Response
err error
)
for {
select {
case t := <-ticker.C:
log.Printf("Attempt at %v", t)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err = http.DefaultClient.Do(req)
if err != nil {
log.Printf("Request failed: %v", err)
continue
}
if resp.StatusCode == 200 {
defer resp.Body.Close()
ticker.Stop()
return io.ReadAll(resp.Body)
}
resp.Body.Close()
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
// Client error - don't retry
return nil, fmt.Errorf("client error: %d", resp.StatusCode)
}
log.Printf("Server error %d, retrying...", resp.StatusCode)
case <-ctx.Done():
return nil, ctx.Err()
}
}
}Ticker is guaranteed to tick at least once immediately after creation:
ticker := backoff.NewTicker(b)
// First tick arrives immediately
<-ticker.CThe ticker channel is closed when:
Stop() is calledStopWithContext)ticker := backoff.NewTicker(b)
// Loop automatically exits when channel closes
for range ticker.C {
// Handle ticks
}
// Channel is closed, loop exitsTicks continue to arrive even if the previous operation is still running:
ticker := backoff.NewTicker(backoff.NewConstantBackOff(1*time.Second))
for range ticker.C {
// This takes 5 seconds
time.Sleep(5 * time.Second)
// Next tick may arrive immediately if it was waiting
}This means operations that take longer than the backoff interval could run in quick succession.
Do not modify the backoff policy while the ticker is running:
b := backoff.NewExponentialBackOff()
ticker := backoff.NewTicker(b)
// DON'T DO THIS - not safe
// b.Reset()
// b.NextBackOff()
ticker.Stop()
// Safe to modify after stopping
b.Reset()Each ticker spawns a goroutine. Always call Stop() to clean up:
ticker := backoff.NewTicker(b)
defer ticker.Stop() // Ensures cleanupThe ticker channel is buffered with size 1. Slow consumers won't block the ticker, but may miss ticks if they don't read fast enough:
ticker := backoff.NewTicker(b)
// Slow consumer
for t := range ticker.C {
time.Sleep(10 * time.Second) // Very slow
// May miss intermediate ticks
}When using WithContext, the ticker automatically stops on context cancellation:
ctx, cancel := context.WithCancel(context.Background())
ticker := backoff.NewTicker(backoff.WithContext(b, ctx))
// Ticker stops automatically when context is canceled
cancel()
// Channel will be closedUse custom timer for deterministic tests:
import (
"testing"
"time"
"github.com/cenkalti/backoff/v4"
)
func TestRetryOperation(t *testing.T) {
// Create controlled timer
timer := &TestTimer{
ch: make(chan time.Time, 10),
}
// Manually control tick timing
timer.ch <- time.Now()
ticker := backoff.NewTickerWithTimer(
backoff.NewConstantBackOff(time.Second),
timer,
)
defer ticker.Stop()
// Test with controlled timing
select {
case <-ticker.C:
// Expected tick
case <-time.After(100 * time.Millisecond):
t.Fatal("Expected tick")
}
}C field is receive-only to prevent external sendsStop() multiple times is safe (uses sync.Once)BackOffContext, ticker respects context cancellation