or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.mdrate-limiter.mdsometimes.md
tile.json

sometimes.mddocs/

Sometimes Utility

The Sometimes utility executes an action occasionally based on configurable rules. It provides a simple way to rate-limit function execution using three filters: First, Every, and Interval. These filters operate as a union (not intersection), meaning the action runs if ANY filter condition is met.

Import

import "golang.org/x/time/rate"

Type Definition

type Sometimes struct {
    First    int           // if non-zero, the first N calls to Do will run f
    Every    int           // if non-zero, every Nth call to Do will run f
    Interval time.Duration // if non-zero and Interval has elapsed since f's last run, Do will run f
}

A Sometimes struct controls when an action executes through three configurable fields:

  • First: If non-zero, the first N calls to Do will run the function
  • Every: If non-zero, every Nth call to Do will run the function (starting with the first call)
  • Interval: If non-zero and this duration has elapsed since the last execution, Do will run the function

Zero Value Behavior: A zero Sometimes value will execute the action exactly once (on the first call to Do).

Thread Safety: Safe for concurrent use by multiple goroutines.

Method

Do

func (s *Sometimes) Do(f func())

Runs the function f as allowed by the First, Every, and Interval filters.

Parameters:

  • f: The function to execute occasionally

Behavior:

  1. The first call to Do always runs f
  2. Subsequent calls run f if allowed by First OR Every OR Interval (union of filters)
  3. If multiple goroutines call Do simultaneously, calls block and run serially

Important Notes:

  • Do is intended for lightweight operations since it blocks concurrent calls
  • If f calls Do on the same Sometimes, it will deadlock

Filter Semantics

The filters form a union (not intersection):

First Filter

Executes the action for the first N calls.

s := rate.Sometimes{First: 5}
// Calls 1-5: execute
// Calls 6+: skip

Every Filter

Executes the action every Nth call, starting with the first call.

s := rate.Sometimes{Every: 3}
// Calls 1, 3, 6, 9, 12...: execute
// Calls 2, 4, 5, 7, 8, 10...: skip

Interval Filter

Executes the action if Interval has elapsed since the last execution.

s := rate.Sometimes{Interval: 10 * time.Second}
// Execute if 10+ seconds have passed since last execution

Combined Filters (Union)

When multiple filters are specified, the action runs if ANY filter allows it:

s := rate.Sometimes{
    First:    3,
    Every:    10,
    Interval: 1 * time.Minute,
}
// Executes on:
// - Calls 1, 2, 3 (First: 3)
// - Calls 10, 20, 30... (Every: 10)
// - Any call after 1 minute has elapsed since last execution (Interval)

Usage Examples

Example 1: Basic Periodic Logging

import (
    "log"
    "golang.org/x/time/rate"
    "time"
)

// Log every 10 seconds
var statusLogger = rate.Sometimes{
    Interval: 10 * time.Second,
}

func processData(data []byte) {
    // This will log at most once every 10 seconds
    statusLogger.Do(func() {
        log.Printf("Processed %d bytes", len(data))
    })

    // Actual processing logic
    process(data)
}

Example 2: First-N-Then-Periodic Pattern

import (
    "log"
    "golang.org/x/time/rate"
    "time"
)

// Log first 3 times, then every 30 seconds
var logger = rate.Sometimes{
    First:    3,
    Interval: 30 * time.Second,
}

func SpammyFunction() {
    logger.Do(func() {
        log.Println("SpammyFunction called")
    })

    // Function logic
    doWork()
}

Example 3: Every-N-Calls Pattern

import (
    "fmt"
    "golang.org/x/time/rate"
)

// Report progress every 1000 items
var progressReporter = rate.Sometimes{
    Every: 1000,
}

func processBatch(items []Item) {
    for i, item := range items {
        progressReporter.Do(func() {
            fmt.Printf("Processed %d items\n", i)
        })

        processItem(item)
    }
}

Example 4: Zero-Value (Once-Only)

import (
    "log"
    "golang.org/x/time/rate"
)

func initializeOnce() {
    var once rate.Sometimes // Zero value

    // This will execute exactly once
    once.Do(func() {
        log.Println("Initialization happening")
        initialize()
    })
}

Example 5: Debug Logging

import (
    "log"
    "golang.org/x/time/rate"
    "time"
)

// Log first 5 calls for debugging, then every 100 calls or every minute
var debugLogger = rate.Sometimes{
    First:    5,
    Every:    100,
    Interval: 1 * time.Minute,
}

func criticalPath(request Request) {
    debugLogger.Do(func() {
        log.Printf("Request: %+v", request)
    })

    handleRequest(request)
}

Example 6: Health Check Reporting

import (
    "fmt"
    "golang.org/x/time/rate"
    "time"
)

type Service struct {
    healthReporter rate.Sometimes
}

func NewService() *Service {
    return &Service{
        healthReporter: rate.Sometimes{
            First:    1,               // Report immediately on first call
            Interval: 5 * time.Minute, // Then every 5 minutes
        },
    }
}

func (s *Service) ProcessRequest() {
    s.healthReporter.Do(func() {
        reportHealth()
    })

    // Process request
}

Example 7: Sampling in High-Throughput Systems

import (
    "log"
    "golang.org/x/time/rate"
)

// Sample 1 out of every 10,000 events
var sampler = rate.Sometimes{
    Every: 10000,
}

func handleEvent(event Event) {
    sampler.Do(func() {
        log.Printf("Sample event: %+v", event)
        sendToAnalytics(event)
    })

    processEvent(event)
}

Comparison with sync.Once

While sync.Once ensures something happens exactly once, Sometimes provides flexible repetition:

import (
    "sync"
    "golang.org/x/time/rate"
)

// sync.Once: Executes exactly once, ever
var once sync.Once
once.Do(func() { initialize() })

// rate.Sometimes (zero value): Executes exactly once, ever
var sometimes rate.Sometimes
sometimes.Do(func() { initialize() })

// rate.Sometimes with config: Executes multiple times per rules
var periodic = rate.Sometimes{Interval: 10 * time.Second}
periodic.Do(func() { logStatus() }) // Executes repeatedly

Thread Safety

Sometimes is safe for concurrent use by multiple goroutines. However, note that:

  1. Calls to Do serialize execution of f - if multiple goroutines call Do simultaneously, they will block and execute in sequence
  2. This serialization ensures thread safety but means f should be lightweight
  3. The blocking behavior makes Sometimes unsuitable for heavy computations in f

Avoiding Deadlocks

NEVER call Do from within the function passed to Do on the same Sometimes instance:

// ❌ DEADLOCK - DO NOT DO THIS
var s rate.Sometimes
s.Do(func() {
    s.Do(func() {  // This will deadlock!
        log.Println("This never executes")
    })
})

Solution: Use separate Sometimes instances or restructure your code:

// ✓ CORRECT
var s1 rate.Sometimes
var s2 rate.Sometimes

s1.Do(func() {
    s2.Do(func() {  // Different instance, no deadlock
        log.Println("This works fine")
    })
})

Performance Considerations

Lightweight Operations Only

Since Do serializes execution, keep f fast:

// ✓ GOOD - Fast operations
logger.Do(func() {
    log.Println("Quick message")
})

// ❌ AVOID - Slow operations block other goroutines
logger.Do(func() {
    queryDatabase()        // Slow
    processLargeFile()     // Slow
    makeNetworkRequest()   // Slow
})

High-Throughput Scenarios

For high-throughput systems, consider the overhead:

// If this is called millions of times per second,
// the mutex overhead in Do might be noticeable
func hotPath() {
    sampler.Do(func() {
        collectSample()
    })
    // ... rest of hot path
}

Common Patterns

Pattern 1: Startup-Then-Periodic

// Log immediately on first call, then periodically
var logger = rate.Sometimes{
    First:    1,
    Interval: 10 * time.Second,
}

Pattern 2: Multiple-Startup-Then-Periodic

// Log first 3 times (for debugging), then every minute
var logger = rate.Sometimes{
    First:    3,
    Interval: 1 * time.Minute,
}

Pattern 3: Sparse Sampling

// Log every 10,000th call and also every hour
var logger = rate.Sometimes{
    Every:    10000,
    Interval: 1 * time.Hour,
}

Pattern 4: Debug-Mode Sampling

func NewSampler(debug bool) rate.Sometimes {
    if debug {
        // In debug mode, log first 10 then every 100
        return rate.Sometimes{First: 10, Every: 100}
    }
    // In production, log every 10,000
    return rate.Sometimes{Every: 10000}
}

When to Use Sometimes vs Limiter

Use Sometimes when:

  • You want simple periodic execution based on call count or time
  • The action is lightweight
  • You don't need precise rate control
  • You want automatic filtering without manual checks

Use Limiter when:

  • You need precise rate control (events per second)
  • You need burst handling
  • You want to block until an action is allowed
  • You need token-based rate limiting with multiple consumers
// Sometimes: Simple, automatic filtering
var s = rate.Sometimes{Interval: 10 * time.Second}
func handler() {
    s.Do(func() { log.Println("Status") })
    // No need to check return value
    process()
}

// Limiter: Explicit rate control
var limiter = rate.NewLimiter(10, 5)
func handler() {
    if !limiter.Allow() {
        return errors.New("rate limit exceeded")
    }
    process()
}