platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
164
selective-vpn-api/app/refreshcoord/coordinator.go
Normal file
164
selective-vpn-api/app/refreshcoord/coordinator.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package refreshcoord
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Snapshot struct {
|
||||
UpdatedAt string
|
||||
Stale bool
|
||||
RefreshInProgress bool
|
||||
LastError string
|
||||
NextRetryAt string
|
||||
}
|
||||
|
||||
// Coordinator is a small reusable SWR-like state machine:
|
||||
// stale-while-refresh + single-flight + exponential backoff after failures.
|
||||
//
|
||||
// All methods are intentionally lock-free; caller owns synchronization.
|
||||
type Coordinator struct {
|
||||
freshTTL time.Duration
|
||||
backoffMin time.Duration
|
||||
backoffMax time.Duration
|
||||
|
||||
updatedAt time.Time
|
||||
|
||||
lastError string
|
||||
|
||||
refreshInProgress bool
|
||||
consecutiveErrors int
|
||||
nextRetryAt time.Time
|
||||
}
|
||||
|
||||
func New(freshTTL, backoffMin, backoffMax time.Duration) Coordinator {
|
||||
if freshTTL <= 0 {
|
||||
freshTTL = 10 * time.Minute
|
||||
}
|
||||
if backoffMin <= 0 {
|
||||
backoffMin = 2 * time.Second
|
||||
}
|
||||
if backoffMax <= 0 {
|
||||
backoffMax = 60 * time.Second
|
||||
}
|
||||
if backoffMax < backoffMin {
|
||||
backoffMax = backoffMin
|
||||
}
|
||||
return Coordinator{
|
||||
freshTTL: freshTTL,
|
||||
backoffMin: backoffMin,
|
||||
backoffMax: backoffMax,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Coordinator) SetUpdatedAt(at time.Time) {
|
||||
c.updatedAt = at
|
||||
}
|
||||
|
||||
func (c *Coordinator) BeginRefresh(now time.Time, force bool, hasData bool) bool {
|
||||
if !c.ShouldRefresh(now, force, hasData) {
|
||||
return false
|
||||
}
|
||||
c.refreshInProgress = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Coordinator) ShouldRefresh(now time.Time, force bool, hasData bool) bool {
|
||||
if c.refreshInProgress {
|
||||
return false
|
||||
}
|
||||
if !c.nextRetryAt.IsZero() && now.Before(c.nextRetryAt) {
|
||||
return false
|
||||
}
|
||||
if force {
|
||||
return true
|
||||
}
|
||||
if !hasData {
|
||||
return true
|
||||
}
|
||||
return c.IsStale(now)
|
||||
}
|
||||
|
||||
func (c *Coordinator) IsStale(now time.Time) bool {
|
||||
if c.updatedAt.IsZero() {
|
||||
return true
|
||||
}
|
||||
return now.Sub(c.updatedAt) > c.freshTTL
|
||||
}
|
||||
|
||||
func (c *Coordinator) FinishSuccess(now time.Time) {
|
||||
c.updatedAt = now
|
||||
c.lastError = ""
|
||||
c.refreshInProgress = false
|
||||
c.consecutiveErrors = 0
|
||||
c.nextRetryAt = time.Time{}
|
||||
}
|
||||
|
||||
func (c *Coordinator) FinishError(msg string, now time.Time) {
|
||||
c.lastError = strings.TrimSpace(msg)
|
||||
c.refreshInProgress = false
|
||||
c.consecutiveErrors++
|
||||
c.nextRetryAt = now.Add(c.nextBackoff())
|
||||
}
|
||||
|
||||
func (c *Coordinator) Snapshot(now time.Time) Snapshot {
|
||||
out := Snapshot{
|
||||
Stale: c.IsStale(now),
|
||||
RefreshInProgress: c.refreshInProgress,
|
||||
LastError: strings.TrimSpace(c.lastError),
|
||||
}
|
||||
if !c.updatedAt.IsZero() {
|
||||
out.UpdatedAt = c.updatedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if !c.nextRetryAt.IsZero() {
|
||||
out.NextRetryAt = c.nextRetryAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Coordinator) RefreshInProgress() bool {
|
||||
return c.refreshInProgress
|
||||
}
|
||||
|
||||
func (c *Coordinator) NextRetryAt() time.Time {
|
||||
return c.nextRetryAt
|
||||
}
|
||||
|
||||
func (c *Coordinator) ConsecutiveErrors() int {
|
||||
return c.consecutiveErrors
|
||||
}
|
||||
|
||||
func (c *Coordinator) LastError() string {
|
||||
return c.lastError
|
||||
}
|
||||
|
||||
func (c *Coordinator) ClearBackoff() {
|
||||
c.nextRetryAt = time.Time{}
|
||||
}
|
||||
|
||||
func (c *Coordinator) nextBackoff() time.Duration {
|
||||
backoff := c.backoffMin
|
||||
if backoff <= 0 {
|
||||
backoff = 2 * time.Second
|
||||
}
|
||||
maxBackoff := c.backoffMax
|
||||
if maxBackoff <= 0 {
|
||||
maxBackoff = backoff
|
||||
}
|
||||
if maxBackoff < backoff {
|
||||
maxBackoff = backoff
|
||||
}
|
||||
for i := 1; i < c.consecutiveErrors; i++ {
|
||||
if backoff >= maxBackoff {
|
||||
return maxBackoff
|
||||
}
|
||||
if backoff > maxBackoff/2 {
|
||||
return maxBackoff
|
||||
}
|
||||
backoff *= 2
|
||||
}
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
return backoff
|
||||
}
|
||||
Reference in New Issue
Block a user