Files
tailscale/util/progresstracking/progresstracking.go
Brad Fitzpatrick a8f3c861a4 util/progresstracking: add Ticker, NewWriter, and CountingWriter
Add three new helpers to the existing progresstracking package:

  - Ticker: spawns a 1 Hz goroutine that calls a report function with
    the current value of an atomic counter and a total. Returns a stop
    function (safe to call multiple times via sync.OnceFunc) that fires
    one final report and blocks until the goroutine exits.

  - NewWriter: wraps an io.Writer and calls onProgress at most once per
    interval with the cumulative byte count.

  - CountingWriter: an io.Writer that atomically counts bytes written,
    for use with Ticker.

These will be used by the appliance flash and OTA update code in
subsequent commits.

Updates #1866

Change-Id: If353cea6506f5351b6fb19bfdb7bc9b78fe7855e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-01 10:02:05 -07:00

122 lines
3.4 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package progresstracking provides wrappers around io.Reader and io.Writer
// that track progress, and a Ticker for reporting progress from an atomic
// counter on a regular interval.
package progresstracking
import (
"io"
"sync"
"sync/atomic"
"time"
)
// NewReader wraps the given Reader with a progress tracking Reader that
// reports progress at the following points:
//
// - First read
// - Every read spaced at least interval since the prior read
// - Last read
func NewReader(r io.Reader, interval time.Duration, onProgress func(totalRead int, err error)) io.Reader {
return &reader{Reader: r, interval: interval, onProgress: onProgress}
}
type reader struct {
io.Reader
interval time.Duration
onProgress func(int, error)
lastTracked time.Time
totalRead int
}
func (r *reader) Read(p []byte) (int, error) {
n, err := r.Reader.Read(p)
r.totalRead += n
if time.Since(r.lastTracked) > r.interval || err != nil {
r.onProgress(r.totalRead, err)
r.lastTracked = time.Now()
}
return n, err
}
// NewWriter wraps w with a writer that calls onProgress after every write
// that brings the total past the next interval threshold. onProgress receives
// the cumulative byte count. If expectedTotal > 0, a final onProgress call is
// guaranteed when the cumulative count reaches or exceeds it, even if the
// interval hasn't elapsed.
func NewWriter(w io.Writer, expectedTotal int64, interval time.Duration, onProgress func(totalWritten int64)) io.Writer {
return &writer{w: w, expectedTotal: expectedTotal, interval: interval, onProgress: onProgress}
}
type writer struct {
w io.Writer
expectedTotal int64 // non-zero if known
interval time.Duration
onProgress func(int64)
lastTracked time.Time
total int64
reachedTotal bool
}
func (pw *writer) Write(p []byte) (int, error) {
n, err := pw.w.Write(p)
pw.total += int64(n)
if !pw.reachedTotal && pw.expectedTotal > 0 && pw.total >= pw.expectedTotal {
pw.onProgress(pw.total)
pw.reachedTotal = true
} else if time.Since(pw.lastTracked) > pw.interval {
pw.onProgress(pw.total)
pw.lastTracked = time.Now()
}
return n, err
}
// Ticker reports progress on a regular interval by polling a counter function.
// It spawns a background goroutine that calls report approximately every
// second. Call the returned stop function when the operation is complete;
// stop calls report one final time and blocks until the goroutine exits.
// The stop function is safe to call multiple times, but will only call
// report the first time it is invoked.
func Ticker(done func() int64, total int64, report func(done, total int64)) (stop func()) {
stopCh := make(chan struct{})
finished := make(chan struct{})
go func() {
defer close(finished)
t := time.NewTicker(time.Second)
defer t.Stop()
for {
select {
case <-stopCh:
report(done(), total)
return
case <-t.C:
report(done(), total)
}
}
}()
return sync.OnceFunc(func() {
close(stopCh)
<-finished
})
}
// CountingWriter wraps an io.Writer and atomically tracks total bytes
// written, suitable for use with Ticker.
type CountingWriter struct {
W io.Writer
count atomic.Int64
}
// Count returns the total number of bytes written.
func (c *CountingWriter) Count() int64 { return c.count.Load() }
func (c *CountingWriter) Write(b []byte) (int, error) {
n, err := c.W.Write(b)
if n > 0 {
c.count.Add(int64(n))
}
return n, err
}