Files
Archive/sing-box/service/ocm/service_usage.go
T
2026-02-24 20:15:32 +01:00

923 lines
20 KiB
Go

package ocm
import (
"encoding/json"
"math"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
)
type UsageStats struct {
RequestCount int `json:"request_count"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CachedTokens int64 `json:"cached_tokens"`
}
func (u *UsageStats) UnmarshalJSON(data []byte) error {
type Alias UsageStats
aux := &struct {
*Alias
PromptTokens int64 `json:"prompt_tokens"`
CompletionTokens int64 `json:"completion_tokens"`
}{
Alias: (*Alias)(u),
}
err := json.Unmarshal(data, aux)
if err != nil {
return err
}
if u.InputTokens == 0 && aux.PromptTokens > 0 {
u.InputTokens = aux.PromptTokens
}
if u.OutputTokens == 0 && aux.CompletionTokens > 0 {
u.OutputTokens = aux.CompletionTokens
}
return nil
}
type CostCombination struct {
Model string `json:"model"`
ServiceTier string `json:"service_tier,omitempty"`
Total UsageStats `json:"total"`
ByUser map[string]UsageStats `json:"by_user"`
}
type AggregatedUsage struct {
LastUpdated time.Time `json:"last_updated"`
Combinations []CostCombination `json:"combinations"`
mutex sync.Mutex
filePath string
logger log.ContextLogger
lastSaveTime time.Time
pendingSave bool
saveTimer *time.Timer
saveMutex sync.Mutex
}
type UsageStatsJSON struct {
RequestCount int `json:"request_count"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CachedTokens int64 `json:"cached_tokens"`
CostUSD float64 `json:"cost_usd"`
}
type CostCombinationJSON struct {
Model string `json:"model"`
ServiceTier string `json:"service_tier,omitempty"`
Total UsageStatsJSON `json:"total"`
ByUser map[string]UsageStatsJSON `json:"by_user"`
}
type CostsSummaryJSON struct {
TotalUSD float64 `json:"total_usd"`
ByUser map[string]float64 `json:"by_user"`
}
type AggregatedUsageJSON struct {
LastUpdated time.Time `json:"last_updated"`
Costs CostsSummaryJSON `json:"costs"`
Combinations []CostCombinationJSON `json:"combinations"`
}
type ModelPricing struct {
InputPrice float64
OutputPrice float64
CachedInputPrice float64
}
type modelFamily struct {
pattern *regexp.Regexp
pricing ModelPricing
}
const (
serviceTierAuto = "auto"
serviceTierDefault = "default"
serviceTierFlex = "flex"
serviceTierPriority = "priority"
serviceTierScale = "scale"
)
var (
gpt52Pricing = ModelPricing{
InputPrice: 1.75,
OutputPrice: 14.0,
CachedInputPrice: 0.175,
}
gpt5Pricing = ModelPricing{
InputPrice: 1.25,
OutputPrice: 10.0,
CachedInputPrice: 0.125,
}
gpt5MiniPricing = ModelPricing{
InputPrice: 0.25,
OutputPrice: 2.0,
CachedInputPrice: 0.025,
}
gpt5NanoPricing = ModelPricing{
InputPrice: 0.05,
OutputPrice: 0.4,
CachedInputPrice: 0.005,
}
gpt52CodexPricing = ModelPricing{
InputPrice: 1.75,
OutputPrice: 14.0,
CachedInputPrice: 0.175,
}
gpt51CodexPricing = ModelPricing{
InputPrice: 1.25,
OutputPrice: 10.0,
CachedInputPrice: 0.125,
}
gpt51CodexMiniPricing = ModelPricing{
InputPrice: 0.25,
OutputPrice: 2.0,
CachedInputPrice: 0.025,
}
gpt52ProPricing = ModelPricing{
InputPrice: 21.0,
OutputPrice: 168.0,
CachedInputPrice: 21.0,
}
gpt5ProPricing = ModelPricing{
InputPrice: 15.0,
OutputPrice: 120.0,
CachedInputPrice: 15.0,
}
gpt52FlexPricing = ModelPricing{
InputPrice: 0.875,
OutputPrice: 7.0,
CachedInputPrice: 0.0875,
}
gpt5FlexPricing = ModelPricing{
InputPrice: 0.625,
OutputPrice: 5.0,
CachedInputPrice: 0.0625,
}
gpt5MiniFlexPricing = ModelPricing{
InputPrice: 0.125,
OutputPrice: 1.0,
CachedInputPrice: 0.0125,
}
gpt5NanoFlexPricing = ModelPricing{
InputPrice: 0.025,
OutputPrice: 0.2,
CachedInputPrice: 0.0025,
}
gpt52PriorityPricing = ModelPricing{
InputPrice: 3.5,
OutputPrice: 28.0,
CachedInputPrice: 0.35,
}
gpt5PriorityPricing = ModelPricing{
InputPrice: 2.5,
OutputPrice: 20.0,
CachedInputPrice: 0.25,
}
gpt5MiniPriorityPricing = ModelPricing{
InputPrice: 0.45,
OutputPrice: 3.6,
CachedInputPrice: 0.045,
}
gpt52CodexPriorityPricing = ModelPricing{
InputPrice: 3.5,
OutputPrice: 28.0,
CachedInputPrice: 0.35,
}
gpt51CodexPriorityPricing = ModelPricing{
InputPrice: 2.5,
OutputPrice: 20.0,
CachedInputPrice: 0.25,
}
gpt4oPricing = ModelPricing{
InputPrice: 2.5,
OutputPrice: 10.0,
CachedInputPrice: 1.25,
}
gpt4oMiniPricing = ModelPricing{
InputPrice: 0.15,
OutputPrice: 0.6,
CachedInputPrice: 0.075,
}
gpt4oAudioPricing = ModelPricing{
InputPrice: 2.5,
OutputPrice: 10.0,
CachedInputPrice: 2.5,
}
gpt4oMiniAudioPricing = ModelPricing{
InputPrice: 0.15,
OutputPrice: 0.6,
CachedInputPrice: 0.15,
}
gptAudioMiniPricing = ModelPricing{
InputPrice: 0.6,
OutputPrice: 2.4,
CachedInputPrice: 0.6,
}
o1Pricing = ModelPricing{
InputPrice: 15.0,
OutputPrice: 60.0,
CachedInputPrice: 7.5,
}
o1ProPricing = ModelPricing{
InputPrice: 150.0,
OutputPrice: 600.0,
CachedInputPrice: 150.0,
}
o1MiniPricing = ModelPricing{
InputPrice: 1.1,
OutputPrice: 4.4,
CachedInputPrice: 0.55,
}
o3MiniPricing = ModelPricing{
InputPrice: 1.1,
OutputPrice: 4.4,
CachedInputPrice: 0.55,
}
o3Pricing = ModelPricing{
InputPrice: 2.0,
OutputPrice: 8.0,
CachedInputPrice: 0.5,
}
o3ProPricing = ModelPricing{
InputPrice: 20.0,
OutputPrice: 80.0,
CachedInputPrice: 20.0,
}
o3DeepResearchPricing = ModelPricing{
InputPrice: 10.0,
OutputPrice: 40.0,
CachedInputPrice: 2.5,
}
o4MiniPricing = ModelPricing{
InputPrice: 1.1,
OutputPrice: 4.4,
CachedInputPrice: 0.275,
}
o4MiniDeepResearchPricing = ModelPricing{
InputPrice: 2.0,
OutputPrice: 8.0,
CachedInputPrice: 0.5,
}
o3FlexPricing = ModelPricing{
InputPrice: 1.0,
OutputPrice: 4.0,
CachedInputPrice: 0.25,
}
o4MiniFlexPricing = ModelPricing{
InputPrice: 0.55,
OutputPrice: 2.2,
CachedInputPrice: 0.138,
}
o3PriorityPricing = ModelPricing{
InputPrice: 3.5,
OutputPrice: 14.0,
CachedInputPrice: 0.875,
}
o4MiniPriorityPricing = ModelPricing{
InputPrice: 2.0,
OutputPrice: 8.0,
CachedInputPrice: 0.5,
}
gpt41Pricing = ModelPricing{
InputPrice: 2.0,
OutputPrice: 8.0,
CachedInputPrice: 0.5,
}
gpt41MiniPricing = ModelPricing{
InputPrice: 0.4,
OutputPrice: 1.6,
CachedInputPrice: 0.1,
}
gpt41NanoPricing = ModelPricing{
InputPrice: 0.1,
OutputPrice: 0.4,
CachedInputPrice: 0.025,
}
gpt41PriorityPricing = ModelPricing{
InputPrice: 3.5,
OutputPrice: 14.0,
CachedInputPrice: 0.875,
}
gpt41MiniPriorityPricing = ModelPricing{
InputPrice: 0.7,
OutputPrice: 2.8,
CachedInputPrice: 0.175,
}
gpt41NanoPriorityPricing = ModelPricing{
InputPrice: 0.2,
OutputPrice: 0.8,
CachedInputPrice: 0.05,
}
gpt4oPriorityPricing = ModelPricing{
InputPrice: 4.25,
OutputPrice: 17.0,
CachedInputPrice: 2.125,
}
gpt4oMiniPriorityPricing = ModelPricing{
InputPrice: 0.25,
OutputPrice: 1.0,
CachedInputPrice: 0.125,
}
standardModelFamilies = []modelFamily{
{
pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`),
pricing: gpt52CodexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`),
pricing: gpt51CodexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.1-codex-mini(?:$|-)`),
pricing: gpt51CodexMiniPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`),
pricing: gpt51CodexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`),
pricing: gpt51CodexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.2-chat-latest$`),
pricing: gpt52Pricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.1-chat-latest$`),
pricing: gpt5Pricing,
},
{
pattern: regexp.MustCompile(`^gpt-5-chat-latest$`),
pricing: gpt5Pricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.2-pro(?:$|-)`),
pricing: gpt52ProPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5-pro(?:$|-)`),
pricing: gpt5ProPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
pricing: gpt5MiniPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`),
pricing: gpt5NanoPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
pricing: gpt52Pricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
pricing: gpt5Pricing,
},
{
pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
pricing: gpt5Pricing,
},
{
pattern: regexp.MustCompile(`^o4-mini-deep-research(?:$|-)`),
pricing: o4MiniDeepResearchPricing,
},
{
pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
pricing: o4MiniPricing,
},
{
pattern: regexp.MustCompile(`^o3-pro(?:$|-)`),
pricing: o3ProPricing,
},
{
pattern: regexp.MustCompile(`^o3-deep-research(?:$|-)`),
pricing: o3DeepResearchPricing,
},
{
pattern: regexp.MustCompile(`^o3-mini(?:$|-)`),
pricing: o3MiniPricing,
},
{
pattern: regexp.MustCompile(`^o3(?:$|-)`),
pricing: o3Pricing,
},
{
pattern: regexp.MustCompile(`^o1-pro(?:$|-)`),
pricing: o1ProPricing,
},
{
pattern: regexp.MustCompile(`^o1-mini(?:$|-)`),
pricing: o1MiniPricing,
},
{
pattern: regexp.MustCompile(`^o1(?:$|-)`),
pricing: o1Pricing,
},
{
pattern: regexp.MustCompile(`^gpt-4o-mini-audio(?:$|-)`),
pricing: gpt4oMiniAudioPricing,
},
{
pattern: regexp.MustCompile(`^gpt-audio-mini(?:$|-)`),
pricing: gptAudioMiniPricing,
},
{
pattern: regexp.MustCompile(`^(?:gpt-4o-audio|gpt-audio)(?:$|-)`),
pricing: gpt4oAudioPricing,
},
{
pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`),
pricing: gpt41NanoPricing,
},
{
pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`),
pricing: gpt41MiniPricing,
},
{
pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`),
pricing: gpt41Pricing,
},
{
pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`),
pricing: gpt4oMiniPricing,
},
{
pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`),
pricing: gpt4oPricing,
},
{
pattern: regexp.MustCompile(`^chatgpt-4o(?:$|-)`),
pricing: gpt4oPricing,
},
}
flexModelFamilies = []modelFamily{
{
pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
pricing: gpt5MiniFlexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`),
pricing: gpt5NanoFlexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
pricing: gpt52FlexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
pricing: gpt5FlexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
pricing: gpt5FlexPricing,
},
{
pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
pricing: o4MiniFlexPricing,
},
{
pattern: regexp.MustCompile(`^o3(?:$|-)`),
pricing: o3FlexPricing,
},
}
priorityModelFamilies = []modelFamily{
{
pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`),
pricing: gpt52CodexPriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`),
pricing: gpt51CodexPriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`),
pricing: gpt51CodexPriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`),
pricing: gpt51CodexPriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
pricing: gpt5MiniPriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
pricing: gpt52PriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
pricing: gpt5PriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
pricing: gpt5PriorityPricing,
},
{
pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
pricing: o4MiniPriorityPricing,
},
{
pattern: regexp.MustCompile(`^o3(?:$|-)`),
pricing: o3PriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`),
pricing: gpt41NanoPriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`),
pricing: gpt41MiniPriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`),
pricing: gpt41PriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`),
pricing: gpt4oMiniPriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`),
pricing: gpt4oPriorityPricing,
},
}
)
func modelFamiliesForTier(serviceTier string) []modelFamily {
switch serviceTier {
case serviceTierFlex:
return flexModelFamilies
case serviceTierPriority:
return priorityModelFamilies
default:
return standardModelFamilies
}
}
func findPricingInFamilies(model string, modelFamilies []modelFamily) (ModelPricing, bool) {
for _, family := range modelFamilies {
if family.pattern.MatchString(model) {
return family.pricing, true
}
}
return ModelPricing{}, false
}
func normalizeServiceTier(serviceTier string) string {
switch strings.ToLower(strings.TrimSpace(serviceTier)) {
case "", serviceTierAuto, serviceTierDefault:
return serviceTierDefault
case serviceTierFlex:
return serviceTierFlex
case serviceTierPriority:
return serviceTierPriority
case serviceTierScale:
// Scale-tier requests are prepaid differently and not listed in this usage file.
return serviceTierDefault
default:
return serviceTierDefault
}
}
func getPricing(model string, serviceTier string) ModelPricing {
normalizedServiceTier := normalizeServiceTier(serviceTier)
modelFamilies := modelFamiliesForTier(normalizedServiceTier)
if pricing, found := findPricingInFamilies(model, modelFamilies); found {
return pricing
}
normalizedModel := normalizeGPT5Model(model)
if normalizedModel != model {
if pricing, found := findPricingInFamilies(normalizedModel, modelFamilies); found {
return pricing
}
}
if normalizedServiceTier != serviceTierDefault {
if pricing, found := findPricingInFamilies(model, standardModelFamilies); found {
return pricing
}
if normalizedModel != model {
if pricing, found := findPricingInFamilies(normalizedModel, standardModelFamilies); found {
return pricing
}
}
}
return gpt4oPricing
}
func normalizeGPT5Model(model string) string {
if !strings.HasPrefix(model, "gpt-5.") {
return model
}
switch {
case strings.Contains(model, "-codex-mini"):
return "gpt-5.1-codex-mini"
case strings.Contains(model, "-codex-max"):
return "gpt-5.1-codex-max"
case strings.Contains(model, "-codex"):
return "gpt-5.2-codex"
case strings.Contains(model, "-chat-latest"):
return "gpt-5.2-chat-latest"
case strings.Contains(model, "-pro"):
return "gpt-5.2-pro"
case strings.Contains(model, "-mini"):
return "gpt-5-mini"
case strings.Contains(model, "-nano"):
return "gpt-5-nano"
default:
return "gpt-5.2"
}
}
func calculateCost(stats UsageStats, model string, serviceTier string) float64 {
pricing := getPricing(model, serviceTier)
regularInputTokens := stats.InputTokens - stats.CachedTokens
if regularInputTokens < 0 {
regularInputTokens = 0
}
cost := (float64(regularInputTokens)*pricing.InputPrice +
float64(stats.OutputTokens)*pricing.OutputPrice +
float64(stats.CachedTokens)*pricing.CachedInputPrice) / 1_000_000
return math.Round(cost*100) / 100
}
func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
u.mutex.Lock()
defer u.mutex.Unlock()
result := &AggregatedUsageJSON{
LastUpdated: u.LastUpdated,
Combinations: make([]CostCombinationJSON, len(u.Combinations)),
Costs: CostsSummaryJSON{
TotalUSD: 0,
ByUser: make(map[string]float64),
},
}
for i, combo := range u.Combinations {
totalCost := calculateCost(combo.Total, combo.Model, combo.ServiceTier)
result.Costs.TotalUSD += totalCost
comboJSON := CostCombinationJSON{
Model: combo.Model,
ServiceTier: combo.ServiceTier,
Total: UsageStatsJSON{
RequestCount: combo.Total.RequestCount,
InputTokens: combo.Total.InputTokens,
OutputTokens: combo.Total.OutputTokens,
CachedTokens: combo.Total.CachedTokens,
CostUSD: totalCost,
},
ByUser: make(map[string]UsageStatsJSON),
}
for user, userStats := range combo.ByUser {
userCost := calculateCost(userStats, combo.Model, combo.ServiceTier)
result.Costs.ByUser[user] += userCost
comboJSON.ByUser[user] = UsageStatsJSON{
RequestCount: userStats.RequestCount,
InputTokens: userStats.InputTokens,
OutputTokens: userStats.OutputTokens,
CachedTokens: userStats.CachedTokens,
CostUSD: userCost,
}
}
result.Combinations[i] = comboJSON
}
result.Costs.TotalUSD = math.Round(result.Costs.TotalUSD*100) / 100
for user, cost := range result.Costs.ByUser {
result.Costs.ByUser[user] = math.Round(cost*100) / 100
}
return result
}
func (u *AggregatedUsage) Load() error {
u.mutex.Lock()
defer u.mutex.Unlock()
data, err := os.ReadFile(u.filePath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
var temp struct {
LastUpdated time.Time `json:"last_updated"`
Combinations []CostCombination `json:"combinations"`
}
err = json.Unmarshal(data, &temp)
if err != nil {
return err
}
u.LastUpdated = temp.LastUpdated
u.Combinations = temp.Combinations
for i := range u.Combinations {
u.Combinations[i].ServiceTier = normalizeServiceTier(u.Combinations[i].ServiceTier)
if u.Combinations[i].ByUser == nil {
u.Combinations[i].ByUser = make(map[string]UsageStats)
}
}
return nil
}
func (u *AggregatedUsage) Save() error {
jsonData := u.ToJSON()
data, err := json.MarshalIndent(jsonData, "", " ")
if err != nil {
return err
}
tmpFile := u.filePath + ".tmp"
err = os.WriteFile(tmpFile, data, 0o644)
if err != nil {
return err
}
defer os.Remove(tmpFile)
err = os.Rename(tmpFile, u.filePath)
if err == nil {
u.saveMutex.Lock()
u.lastSaveTime = time.Now()
u.saveMutex.Unlock()
}
return err
}
func (u *AggregatedUsage) AddUsage(model string, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error {
if model == "" {
return E.New("model cannot be empty")
}
normalizedServiceTier := normalizeServiceTier(serviceTier)
u.mutex.Lock()
defer u.mutex.Unlock()
u.LastUpdated = time.Now()
var combo *CostCombination
for i := range u.Combinations {
comboServiceTier := normalizeServiceTier(u.Combinations[i].ServiceTier)
if u.Combinations[i].ServiceTier != comboServiceTier {
u.Combinations[i].ServiceTier = comboServiceTier
}
if u.Combinations[i].Model == model && comboServiceTier == normalizedServiceTier {
combo = &u.Combinations[i]
break
}
}
if combo == nil {
newCombo := CostCombination{
Model: model,
ServiceTier: normalizedServiceTier,
Total: UsageStats{},
ByUser: make(map[string]UsageStats),
}
u.Combinations = append(u.Combinations, newCombo)
combo = &u.Combinations[len(u.Combinations)-1]
}
combo.Total.RequestCount++
combo.Total.InputTokens += inputTokens
combo.Total.OutputTokens += outputTokens
combo.Total.CachedTokens += cachedTokens
if user != "" {
userStats := combo.ByUser[user]
userStats.RequestCount++
userStats.InputTokens += inputTokens
userStats.OutputTokens += outputTokens
userStats.CachedTokens += cachedTokens
combo.ByUser[user] = userStats
}
go u.scheduleSave()
return nil
}
func (u *AggregatedUsage) scheduleSave() {
const saveInterval = time.Minute
u.saveMutex.Lock()
defer u.saveMutex.Unlock()
timeSinceLastSave := time.Since(u.lastSaveTime)
if timeSinceLastSave >= saveInterval {
go u.saveAsync()
return
}
if u.pendingSave {
return
}
u.pendingSave = true
remainingTime := saveInterval - timeSinceLastSave
u.saveTimer = time.AfterFunc(remainingTime, func() {
u.saveMutex.Lock()
u.pendingSave = false
u.saveMutex.Unlock()
u.saveAsync()
})
}
func (u *AggregatedUsage) saveAsync() {
err := u.Save()
if err != nil {
if u.logger != nil {
u.logger.Error("save usage statistics: ", err)
}
}
}
func (u *AggregatedUsage) cancelPendingSave() {
u.saveMutex.Lock()
defer u.saveMutex.Unlock()
if u.saveTimer != nil {
u.saveTimer.Stop()
u.saveTimer = nil
}
u.pendingSave = false
}