174 lines
5.2 KiB
Go
174 lines
5.2 KiB
Go
package backgroundtask
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// Stats provides observability into the runner's operation.
|
|
// All methods are thread-safe and can be called concurrently.
|
|
type Stats struct {
|
|
mu sync.RWMutex
|
|
workCycles atomic.Int64 // Total number of work cycles completed
|
|
lastStart time.Time // Time when the last work cycle started
|
|
lastFinish time.Time // Time when the last work cycle finished
|
|
lastErr error // Last error returned from work function
|
|
maxConcurrent atomic.Int32 // Maximum concurrent work attempts (should always be 1)
|
|
currentConcurrent atomic.Int32 // Current concurrent work executions
|
|
}
|
|
|
|
// WorkCycles returns the total number of work cycles completed.
|
|
func (s *Stats) WorkCycles() int64 {
|
|
return s.workCycles.Load()
|
|
}
|
|
|
|
// LastStart returns the time when the last work cycle started.
|
|
func (s *Stats) LastStart() time.Time {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.lastStart
|
|
}
|
|
|
|
// LastFinish returns the time when the last work cycle finished.
|
|
func (s *Stats) LastFinish() time.Time {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.lastFinish
|
|
}
|
|
|
|
// LastErr returns the last error returned from the work function.
|
|
func (s *Stats) LastErr() error {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.lastErr
|
|
}
|
|
|
|
// MaxConcurrent returns the maximum number of concurrent work executions observed.
|
|
// This should always be 1 if the runner is working correctly.
|
|
func (s *Stats) MaxConcurrent() int32 {
|
|
return s.maxConcurrent.Load()
|
|
}
|
|
|
|
// CurrentConcurrent returns the current number of concurrent work executions.
|
|
// This should be 0 or 1.
|
|
func (s *Stats) CurrentConcurrent() int32 {
|
|
return s.currentConcurrent.Load()
|
|
}
|
|
|
|
// incrementWorkCycles increments the work cycle counter.
|
|
func (s *Stats) incrementWorkCycles() {
|
|
s.workCycles.Add(1)
|
|
}
|
|
|
|
// setLastStart updates the last start time.
|
|
func (s *Stats) setLastStart(t time.Time) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.lastStart = t
|
|
}
|
|
|
|
// setLastFinish updates the last finish time.
|
|
func (s *Stats) setLastFinish(t time.Time) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.lastFinish = t
|
|
}
|
|
|
|
// setLastErr updates the last error.
|
|
func (s *Stats) setLastErr(err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.lastErr = err
|
|
}
|
|
|
|
// enterWork marks the beginning of a work cycle and tracks concurrency.
|
|
func (s *Stats) enterWork() {
|
|
current := s.currentConcurrent.Add(1)
|
|
|
|
// Update max concurrent if needed
|
|
for {
|
|
max := s.maxConcurrent.Load()
|
|
if current <= max {
|
|
break
|
|
}
|
|
if s.maxConcurrent.CompareAndSwap(max, current) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// exitWork marks the end of a work cycle.
|
|
func (s *Stats) exitWork() {
|
|
s.currentConcurrent.Add(-1)
|
|
}
|
|
|
|
// Reset clears all statistics. Used primarily for testing.
|
|
func (s *Stats) Reset() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.workCycles.Store(0)
|
|
s.lastStart = time.Time{}
|
|
s.lastFinish = time.Time{}
|
|
s.lastErr = nil
|
|
s.maxConcurrent.Store(0)
|
|
s.currentConcurrent.Store(0)
|
|
}
|
|
|
|
// Print writes a formatted representation of all statistics to the provided writer.
|
|
// The output includes work cycles, timing information, error status, and concurrency metrics.
|
|
func (s *Stats) Print(w io.Writer) {
|
|
s.mu.RLock()
|
|
lastStart := s.lastStart
|
|
lastFinish := s.lastFinish
|
|
lastErr := s.lastErr
|
|
s.mu.RUnlock()
|
|
|
|
workCycles := s.workCycles.Load()
|
|
maxConcurrent := s.maxConcurrent.Load()
|
|
currentConcurrent := s.currentConcurrent.Load()
|
|
|
|
fmt.Fprintf(w, "╭─────────────────────────────────────────────╮\n")
|
|
fmt.Fprintf(w, "│ Background Task Runner Stats │\n")
|
|
fmt.Fprintf(w, "├─────────────────────────────────────────────┤\n")
|
|
fmt.Fprintf(w, "│ Work Cycles: %-23d │\n", workCycles)
|
|
fmt.Fprintf(w, "│ Current Concurrent: %-23d │\n", currentConcurrent)
|
|
fmt.Fprintf(w, "│ Max Concurrent: %-23d │\n", maxConcurrent)
|
|
fmt.Fprintf(w, "├─────────────────────────────────────────────┤\n")
|
|
|
|
// Format timing information
|
|
if !lastStart.IsZero() {
|
|
fmt.Fprintf(w, "│ Last Start: %-30s │\n", lastStart.Format("2006-01-02 15:04:05.000"))
|
|
} else {
|
|
fmt.Fprintf(w, "│ Last Start: %-30s │\n", "Never")
|
|
}
|
|
|
|
if !lastFinish.IsZero() {
|
|
fmt.Fprintf(w, "│ Last Finish: %-30s │\n", lastFinish.Format("2006-01-02 15:04:05.000"))
|
|
if !lastStart.IsZero() {
|
|
duration := lastFinish.Sub(lastStart)
|
|
fmt.Fprintf(w, "│ Duration: %-30s │\n", duration.String())
|
|
}
|
|
} else {
|
|
fmt.Fprintf(w, "│ Last Finish: %-30s │\n", "Never")
|
|
}
|
|
|
|
fmt.Fprintf(w, "├─────────────────────────────────────────────┤\n")
|
|
|
|
// Format error information
|
|
if lastErr != nil {
|
|
errStr := lastErr.Error()
|
|
if len(errStr) > 30 {
|
|
errStr = errStr[:27] + "..."
|
|
}
|
|
fmt.Fprintf(w, "│ Last Error: %-30s │\n", errStr)
|
|
} else {
|
|
fmt.Fprintf(w, "│ Last Error: %-30s │\n", "None")
|
|
}
|
|
|
|
fmt.Fprintf(w, "╰─────────────────────────────────────────────╯\n")
|
|
}
|