| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- package ccm
- import (
- "encoding/json"
- "fmt"
- "math"
- "os"
- "regexp"
- "sync"
- "time"
- "github.com/sagernet/sing-box/log"
- E "github.com/sagernet/sing/common/exceptions"
- )
- type UsageStats struct {
- RequestCount int `json:"request_count"`
- MessagesCount int `json:"messages_count"`
- InputTokens int64 `json:"input_tokens"`
- OutputTokens int64 `json:"output_tokens"`
- CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
- CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
- CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"`
- CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"`
- }
- type CostCombination struct {
- Model string `json:"model"`
- ContextWindow int `json:"context_window"`
- WeekStartUnix int64 `json:"week_start_unix,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"`
- MessagesCount int `json:"messages_count"`
- InputTokens int64 `json:"input_tokens"`
- OutputTokens int64 `json:"output_tokens"`
- CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
- CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
- CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"`
- CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"`
- CostUSD float64 `json:"cost_usd"`
- }
- type CostCombinationJSON struct {
- Model string `json:"model"`
- ContextWindow int `json:"context_window"`
- WeekStartUnix int64 `json:"week_start_unix,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"`
- ByWeek map[string]float64 `json:"by_week,omitempty"`
- ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"`
- }
- type AggregatedUsageJSON struct {
- LastUpdated time.Time `json:"last_updated"`
- Costs CostsSummaryJSON `json:"costs"`
- Combinations []CostCombinationJSON `json:"combinations"`
- }
- type WeeklyCycleHint struct {
- WindowMinutes int64
- ResetAt time.Time
- }
- type ModelPricing struct {
- InputPrice float64
- OutputPrice float64
- CacheReadPrice float64
- CacheWritePrice5Minute float64
- CacheWritePrice1Hour float64
- }
- type modelFamily struct {
- pattern *regexp.Regexp
- standardPricing ModelPricing
- premiumPricing *ModelPricing
- }
- var (
- opus46StandardPricing = ModelPricing{
- InputPrice: 5.0,
- OutputPrice: 25.0,
- CacheReadPrice: 0.5,
- CacheWritePrice5Minute: 6.25,
- CacheWritePrice1Hour: 10.0,
- }
- opus46PremiumPricing = ModelPricing{
- InputPrice: 10.0,
- OutputPrice: 37.5,
- CacheReadPrice: 1.0,
- CacheWritePrice5Minute: 12.5,
- CacheWritePrice1Hour: 20.0,
- }
- opus45Pricing = ModelPricing{
- InputPrice: 5.0,
- OutputPrice: 25.0,
- CacheReadPrice: 0.5,
- CacheWritePrice5Minute: 6.25,
- CacheWritePrice1Hour: 10.0,
- }
- opus4Pricing = ModelPricing{
- InputPrice: 15.0,
- OutputPrice: 75.0,
- CacheReadPrice: 1.5,
- CacheWritePrice5Minute: 18.75,
- CacheWritePrice1Hour: 30.0,
- }
- sonnet46StandardPricing = ModelPricing{
- InputPrice: 3.0,
- OutputPrice: 15.0,
- CacheReadPrice: 0.3,
- CacheWritePrice5Minute: 3.75,
- CacheWritePrice1Hour: 6.0,
- }
- sonnet46PremiumPricing = ModelPricing{
- InputPrice: 6.0,
- OutputPrice: 22.5,
- CacheReadPrice: 0.6,
- CacheWritePrice5Minute: 7.5,
- CacheWritePrice1Hour: 12.0,
- }
- sonnet45StandardPricing = ModelPricing{
- InputPrice: 3.0,
- OutputPrice: 15.0,
- CacheReadPrice: 0.3,
- CacheWritePrice5Minute: 3.75,
- CacheWritePrice1Hour: 6.0,
- }
- sonnet45PremiumPricing = ModelPricing{
- InputPrice: 6.0,
- OutputPrice: 22.5,
- CacheReadPrice: 0.6,
- CacheWritePrice5Minute: 7.5,
- CacheWritePrice1Hour: 12.0,
- }
- sonnet4StandardPricing = ModelPricing{
- InputPrice: 3.0,
- OutputPrice: 15.0,
- CacheReadPrice: 0.3,
- CacheWritePrice5Minute: 3.75,
- CacheWritePrice1Hour: 6.0,
- }
- sonnet4PremiumPricing = ModelPricing{
- InputPrice: 6.0,
- OutputPrice: 22.5,
- CacheReadPrice: 0.6,
- CacheWritePrice5Minute: 7.5,
- CacheWritePrice1Hour: 12.0,
- }
- sonnet37Pricing = ModelPricing{
- InputPrice: 3.0,
- OutputPrice: 15.0,
- CacheReadPrice: 0.3,
- CacheWritePrice5Minute: 3.75,
- CacheWritePrice1Hour: 6.0,
- }
- sonnet35Pricing = ModelPricing{
- InputPrice: 3.0,
- OutputPrice: 15.0,
- CacheReadPrice: 0.3,
- CacheWritePrice5Minute: 3.75,
- CacheWritePrice1Hour: 6.0,
- }
- haiku45Pricing = ModelPricing{
- InputPrice: 1.0,
- OutputPrice: 5.0,
- CacheReadPrice: 0.1,
- CacheWritePrice5Minute: 1.25,
- CacheWritePrice1Hour: 2.0,
- }
- haiku4Pricing = ModelPricing{
- InputPrice: 1.0,
- OutputPrice: 5.0,
- CacheReadPrice: 0.1,
- CacheWritePrice5Minute: 1.25,
- CacheWritePrice1Hour: 2.0,
- }
- haiku35Pricing = ModelPricing{
- InputPrice: 0.8,
- OutputPrice: 4.0,
- CacheReadPrice: 0.08,
- CacheWritePrice5Minute: 1.0,
- CacheWritePrice1Hour: 1.6,
- }
- haiku3Pricing = ModelPricing{
- InputPrice: 0.25,
- OutputPrice: 1.25,
- CacheReadPrice: 0.03,
- CacheWritePrice5Minute: 0.3,
- CacheWritePrice1Hour: 0.5,
- }
- opus3Pricing = ModelPricing{
- InputPrice: 15.0,
- OutputPrice: 75.0,
- CacheReadPrice: 1.5,
- CacheWritePrice5Minute: 18.75,
- CacheWritePrice1Hour: 30.0,
- }
- modelFamilies = []modelFamily{
- {
- pattern: regexp.MustCompile(`^claude-opus-4-6(?:-|$)`),
- standardPricing: opus46StandardPricing,
- premiumPricing: &opus46PremiumPricing,
- },
- {
- pattern: regexp.MustCompile(`^claude-opus-4-5(?:-|$)`),
- standardPricing: opus45Pricing,
- premiumPricing: nil,
- },
- {
- pattern: regexp.MustCompile(`^claude-(?:opus-4(?:-|$)|4-opus-)`),
- standardPricing: opus4Pricing,
- premiumPricing: nil,
- },
- {
- pattern: regexp.MustCompile(`^claude-(?:opus-3(?:-|$)|3-opus-)`),
- standardPricing: opus3Pricing,
- premiumPricing: nil,
- },
- {
- pattern: regexp.MustCompile(`^claude-(?:sonnet-4-6(?:-|$)|4-6-sonnet-)`),
- standardPricing: sonnet46StandardPricing,
- premiumPricing: &sonnet46PremiumPricing,
- },
- {
- pattern: regexp.MustCompile(`^claude-(?:sonnet-4-5(?:-|$)|4-5-sonnet-)`),
- standardPricing: sonnet45StandardPricing,
- premiumPricing: &sonnet45PremiumPricing,
- },
- {
- pattern: regexp.MustCompile(`^claude-(?:sonnet-4(?:-|$)|4-sonnet-)`),
- standardPricing: sonnet4StandardPricing,
- premiumPricing: &sonnet4PremiumPricing,
- },
- {
- pattern: regexp.MustCompile(`^claude-3-7-sonnet(?:-|$)`),
- standardPricing: sonnet37Pricing,
- premiumPricing: nil,
- },
- {
- pattern: regexp.MustCompile(`^claude-3-5-sonnet(?:-|$)`),
- standardPricing: sonnet35Pricing,
- premiumPricing: nil,
- },
- {
- pattern: regexp.MustCompile(`^claude-(?:haiku-4-5(?:-|$)|4-5-haiku-)`),
- standardPricing: haiku45Pricing,
- premiumPricing: nil,
- },
- {
- pattern: regexp.MustCompile(`^claude-haiku-4(?:-|$)`),
- standardPricing: haiku4Pricing,
- premiumPricing: nil,
- },
- {
- pattern: regexp.MustCompile(`^claude-3-5-haiku(?:-|$)`),
- standardPricing: haiku35Pricing,
- premiumPricing: nil,
- },
- {
- pattern: regexp.MustCompile(`^claude-3-haiku(?:-|$)`),
- standardPricing: haiku3Pricing,
- premiumPricing: nil,
- },
- }
- )
- func getPricing(model string, contextWindow int) ModelPricing {
- isPremium := contextWindow >= contextWindowPremium
- for _, family := range modelFamilies {
- if family.pattern.MatchString(model) {
- if isPremium && family.premiumPricing != nil {
- return *family.premiumPricing
- }
- return family.standardPricing
- }
- }
- return sonnet4StandardPricing
- }
- func calculateCost(stats UsageStats, model string, contextWindow int) float64 {
- pricing := getPricing(model, contextWindow)
- cacheCreationCost := 0.0
- if stats.CacheCreation5MinuteInputTokens > 0 || stats.CacheCreation1HourInputTokens > 0 {
- cacheCreationCost = float64(stats.CacheCreation5MinuteInputTokens)*pricing.CacheWritePrice5Minute +
- float64(stats.CacheCreation1HourInputTokens)*pricing.CacheWritePrice1Hour
- } else {
- // Backward compatibility for usage files generated before TTL split tracking.
- cacheCreationCost = float64(stats.CacheCreationInputTokens) * pricing.CacheWritePrice5Minute
- }
- cost := (float64(stats.InputTokens)*pricing.InputPrice +
- float64(stats.OutputTokens)*pricing.OutputPrice +
- float64(stats.CacheReadInputTokens)*pricing.CacheReadPrice +
- cacheCreationCost) / 1_000_000
- return math.Round(cost*100) / 100
- }
- func roundCost(cost float64) float64 {
- return math.Round(cost*100) / 100
- }
- func normalizeCombinations(combinations []CostCombination) {
- for index := range combinations {
- if combinations[index].ByUser == nil {
- combinations[index].ByUser = make(map[string]UsageStats)
- }
- }
- }
- func addUsageToCombinations(
- combinations *[]CostCombination,
- model string,
- contextWindow int,
- weekStartUnix int64,
- messagesCount int,
- inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64,
- user string,
- ) {
- var matchedCombination *CostCombination
- for index := range *combinations {
- combination := &(*combinations)[index]
- if combination.Model == model && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix {
- matchedCombination = combination
- break
- }
- }
- if matchedCombination == nil {
- newCombination := CostCombination{
- Model: model,
- ContextWindow: contextWindow,
- WeekStartUnix: weekStartUnix,
- Total: UsageStats{},
- ByUser: make(map[string]UsageStats),
- }
- *combinations = append(*combinations, newCombination)
- matchedCombination = &(*combinations)[len(*combinations)-1]
- }
- if cacheCreationTokens == 0 {
- cacheCreationTokens = cacheCreation5MinuteTokens + cacheCreation1HourTokens
- }
- matchedCombination.Total.RequestCount++
- matchedCombination.Total.MessagesCount += messagesCount
- matchedCombination.Total.InputTokens += inputTokens
- matchedCombination.Total.OutputTokens += outputTokens
- matchedCombination.Total.CacheReadInputTokens += cacheReadTokens
- matchedCombination.Total.CacheCreationInputTokens += cacheCreationTokens
- matchedCombination.Total.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens
- matchedCombination.Total.CacheCreation1HourInputTokens += cacheCreation1HourTokens
- if user != "" {
- userStats := matchedCombination.ByUser[user]
- userStats.RequestCount++
- userStats.MessagesCount += messagesCount
- userStats.InputTokens += inputTokens
- userStats.OutputTokens += outputTokens
- userStats.CacheReadInputTokens += cacheReadTokens
- userStats.CacheCreationInputTokens += cacheCreationTokens
- userStats.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens
- userStats.CacheCreation1HourInputTokens += cacheCreation1HourTokens
- matchedCombination.ByUser[user] = userStats
- }
- }
- func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) {
- result := make([]CostCombinationJSON, len(combinations))
- var totalCost float64
- for index, combination := range combinations {
- combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ContextWindow)
- totalCost += combinationTotalCost
- combinationJSON := CostCombinationJSON{
- Model: combination.Model,
- ContextWindow: combination.ContextWindow,
- WeekStartUnix: combination.WeekStartUnix,
- Total: UsageStatsJSON{
- RequestCount: combination.Total.RequestCount,
- MessagesCount: combination.Total.MessagesCount,
- InputTokens: combination.Total.InputTokens,
- OutputTokens: combination.Total.OutputTokens,
- CacheReadInputTokens: combination.Total.CacheReadInputTokens,
- CacheCreationInputTokens: combination.Total.CacheCreationInputTokens,
- CacheCreation5MinuteInputTokens: combination.Total.CacheCreation5MinuteInputTokens,
- CacheCreation1HourInputTokens: combination.Total.CacheCreation1HourInputTokens,
- CostUSD: combinationTotalCost,
- },
- ByUser: make(map[string]UsageStatsJSON),
- }
- for user, userStats := range combination.ByUser {
- userCost := calculateCost(userStats, combination.Model, combination.ContextWindow)
- if aggregateUserCosts != nil {
- aggregateUserCosts[user] += userCost
- }
- combinationJSON.ByUser[user] = UsageStatsJSON{
- RequestCount: userStats.RequestCount,
- MessagesCount: userStats.MessagesCount,
- InputTokens: userStats.InputTokens,
- OutputTokens: userStats.OutputTokens,
- CacheReadInputTokens: userStats.CacheReadInputTokens,
- CacheCreationInputTokens: userStats.CacheCreationInputTokens,
- CacheCreation5MinuteInputTokens: userStats.CacheCreation5MinuteInputTokens,
- CacheCreation1HourInputTokens: userStats.CacheCreation1HourInputTokens,
- CostUSD: userCost,
- }
- }
- result[index] = combinationJSON
- }
- return result, roundCost(totalCost)
- }
- func formatUTCOffsetLabel(timestamp time.Time) string {
- _, offsetSeconds := timestamp.Zone()
- sign := "+"
- if offsetSeconds < 0 {
- sign = "-"
- offsetSeconds = -offsetSeconds
- }
- offsetHours := offsetSeconds / 3600
- offsetMinutes := (offsetSeconds % 3600) / 60
- if offsetMinutes == 0 {
- return fmt.Sprintf("UTC%s%d", sign, offsetHours)
- }
- return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes)
- }
- func formatWeekStartKey(cycleStartAt time.Time) string {
- localCycleStart := cycleStartAt.In(time.Local)
- return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart))
- }
- func buildByWeekCost(combinations []CostCombination) map[string]float64 {
- byWeek := make(map[string]float64)
- for _, combination := range combinations {
- if combination.WeekStartUnix <= 0 {
- continue
- }
- weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
- weekKey := formatWeekStartKey(weekStartAt)
- byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ContextWindow)
- }
- for weekKey, weekCost := range byWeek {
- byWeek[weekKey] = roundCost(weekCost)
- }
- return byWeek
- }
- func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 {
- byUserAndWeek := make(map[string]map[string]float64)
- for _, combination := range combinations {
- if combination.WeekStartUnix <= 0 {
- continue
- }
- weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
- weekKey := formatWeekStartKey(weekStartAt)
- for user, userStats := range combination.ByUser {
- userWeeks, exists := byUserAndWeek[user]
- if !exists {
- userWeeks = make(map[string]float64)
- byUserAndWeek[user] = userWeeks
- }
- userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ContextWindow)
- }
- }
- for _, weekCosts := range byUserAndWeek {
- for weekKey, cost := range weekCosts {
- weekCosts[weekKey] = roundCost(cost)
- }
- }
- return byUserAndWeek
- }
- func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 {
- if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() {
- return 0
- }
- windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute
- return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix()
- }
- func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
- u.mutex.Lock()
- defer u.mutex.Unlock()
- result := &AggregatedUsageJSON{
- LastUpdated: u.LastUpdated,
- Costs: CostsSummaryJSON{
- TotalUSD: 0,
- ByUser: make(map[string]float64),
- ByWeek: make(map[string]float64),
- },
- }
- globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser)
- result.Combinations = globalCombinationsJSON
- result.Costs.TotalUSD = totalCost
- result.Costs.ByWeek = buildByWeekCost(u.Combinations)
- if len(result.Costs.ByWeek) == 0 {
- result.Costs.ByWeek = nil
- }
- result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations)
- if len(result.Costs.ByUserAndWeek) == 0 {
- result.Costs.ByUserAndWeek = nil
- }
- for user, cost := range result.Costs.ByUser {
- result.Costs.ByUser[user] = roundCost(cost)
- }
- return result
- }
- func (u *AggregatedUsage) Load() error {
- u.mutex.Lock()
- defer u.mutex.Unlock()
- u.LastUpdated = time.Time{}
- u.Combinations = nil
- 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
- normalizeCombinations(u.Combinations)
- 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,
- contextWindow int,
- messagesCount int,
- inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64,
- user string,
- ) error {
- return u.AddUsageWithCycleHint(model, contextWindow, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user, time.Now(), nil)
- }
- func (u *AggregatedUsage) AddUsageWithCycleHint(
- model string,
- contextWindow int,
- messagesCount int,
- inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64,
- user string,
- observedAt time.Time,
- cycleHint *WeeklyCycleHint,
- ) error {
- if model == "" {
- return E.New("model cannot be empty")
- }
- if contextWindow <= 0 {
- return E.New("contextWindow must be positive")
- }
- if observedAt.IsZero() {
- observedAt = time.Now()
- }
- u.mutex.Lock()
- defer u.mutex.Unlock()
- u.LastUpdated = observedAt
- weekStartUnix := deriveWeekStartUnix(cycleHint)
- addUsageToCombinations(&u.Combinations, model, contextWindow, weekStartUnix, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user)
- 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
- }
|