carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package usage
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "time"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// ScanRow is one account's on-demand scanning work for a UTC day.
15type ScanRow struct {
16 Objects int64 `json:"objects"`
17 Bytes int64 `json:"bytes"`
18}
19
20// ScanDay is usage/scan/<date>.json, keyed by account.
21type ScanDay struct {
22 V int `json:"v"` // 1
23 Date string `json:"date"`
24 Accounts map[string]ScanRow `json:"accounts"`
25}
26
27func ScanDayKey(date string) string { return "usage/scan/" + date + ".json" }
28
29const ScanPrefix = "usage/scan/"
30
31func (d *ScanDay) AddDeltas(deltas map[string]ScanRow) {
32 if d.Accounts == nil {
33 d.Accounts = map[string]ScanRow{}
34 }
35 for account, delta := range deltas {
36 row := d.Accounts[account]
37 row.Objects += delta.Objects
38 row.Bytes += delta.Bytes
39 d.Accounts[account] = row
40 }
41}
42
43// UpsertScanDay folds one drain tick into its UTC day. Empty ticks write
44// nothing; a compare-and-swap loss re-reads before adding so concurrent
45// consoles cannot silently replace each other's counts.
46func UpsertScanDay(ctx context.Context, s release.Store, now time.Time, deltas map[string]ScanRow) error {
47 if len(deltas) == 0 {
48 return nil
49 }
50 date := DayOf(now)
51 key := ScanDayKey(date)
52 for attempt := 0; attempt < 3; attempt++ {
53 day := ScanDay{V: 1, Date: date}
54 raw, etag, err := s.Get(ctx, key)
55 switch {
56 case err == nil:
57 if err := json.Unmarshal(raw, &day); err != nil {
58 return fmt.Errorf("usage: parse %s: %w", key, err)
59 }
60 case errors.Is(err, os.ErrNotExist):
61 etag = ""
62 default:
63 return err
64 }
65 day.AddDeltas(deltas)
66 out, err := json.Marshal(day)
67 if err != nil {
68 return err
69 }
70 if _, err := s.Put(ctx, key, out, etag); err == nil {
71 return nil
72 } else if !errors.Is(err, release.ErrCASConflict) {
73 return err
74 }
75 }
76 return fmt.Errorf("usage: %s: lost CAS race three times", key)
77}
78