File CodeView

This is a code view which proxies my personal git server. On the clearnet the following file is available at:

personal-gemini-capsule/src/branch/main/counter.go

----------------------------

package main

import (
	"encoding/json"
	"errors"
	"log/slog"
	"os"
	"sync"
	"time"
)

// RequestCounter tracks request counts per path in memory with periodic snapshots
type RequestCounter struct {
	counts       map[string]int64
	mu           sync.RWMutex
	snapshotPath string
	stopChan     chan struct{}
	wg           sync.WaitGroup
}

// NewRequestCounter creates a new request counter with periodic snapshots
func NewRequestCounter(snapshotInterval time.Duration) (*RequestCounter, error) {
	snapshotPath := os.Getenv("REQUEST_COUNTS_PATH")
	if snapshotPath == "" {
		err := errors.New("REQUEST_COUNTS_PATH environment variable must be set and non-empty")
		slog.Error("failed to initialize request counter", "error", err)
		os.Exit(1)
	}
	c := &RequestCounter{
		counts:       make(map[string]int64),
		snapshotPath: snapshotPath,
		stopChan:     make(chan struct{}),
	}

	// Load existing counts from snapshot file
	if err := c.load(); err != nil {
		slog.Warn("failed to load counter snapshot, starting fresh", "error", err, "path", snapshotPath)
	}

	// Start periodic snapshot goroutine
	c.wg.Add(1)
	go c.periodicSnapshot(snapshotInterval)

	return c, nil
}

// Increment increments the counter for a given path and returns the new count
func (c *RequestCounter) Increment(path string) int64 {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.counts[path]++
	return c.counts[path]
}

// Get returns the current count for a given path
func (c *RequestCounter) Get(path string) int64 {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.counts[path]
}

func (c *RequestCounter) GetTotal() int64 {
	c.mu.Lock()
	defer c.mu.Unlock()
	var total int64
	for _, count := range c.counts {
		total += count
	}
	return total
}

// load reads the snapshot file and loads counts into memory
func (c *RequestCounter) load() error {
	data, err := os.ReadFile(c.snapshotPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // File doesn't exist yet, that's okay
		}
		return err
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	if err := json.Unmarshal(data, &c.counts); err != nil {
		return err
	}

	slog.Info("loaded request counter snapshot", "paths", len(c.counts), "file", c.snapshotPath)
	return nil
}

// snapshot writes current counts to disk
func (c *RequestCounter) snapshot() error {
	c.mu.RLock()
	data, err := json.MarshalIndent(c.counts, "", "  ")
	c.mu.RUnlock()

	if err != nil {
		return err
	}

	// Write to temp file first, then rename for atomicity
	tempPath := c.snapshotPath + ".tmp"
	if err := os.WriteFile(tempPath, data, 0644); err != nil {
		return err
	}

	if err := os.Rename(tempPath, c.snapshotPath); err != nil {
		return err
	}

	slog.Debug("saved request counter snapshot", "paths", len(c.counts), "file", c.snapshotPath)
	return nil
}

// periodicSnapshot runs in a goroutine and saves snapshots at regular intervals
func (c *RequestCounter) periodicSnapshot(interval time.Duration) {
	defer c.wg.Done()
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			if err := c.snapshot(); err != nil {
				slog.Error("failed to save counter snapshot", "error", err)
			}
		case <-c.stopChan:
			// Final snapshot before shutdown
			if err := c.snapshot(); err != nil {
				slog.Error("failed to save final counter snapshot", "error", err)
			}
			return
		}
	}
}

// Close stops the periodic snapshot goroutine and saves a final snapshot
func (c *RequestCounter) Close() error {
	close(c.stopChan)
	c.wg.Wait()
	return nil
}

-----------------------------

Back to home