carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "log"
7 "os"
8 "time"
9
10 "github.com/carlosframework/platform/internal/analytics"
11 "github.com/carlosframework/platform/internal/applog"
12 "github.com/carlosframework/platform/internal/release"
13 "github.com/carlosframework/platform/internal/usage"
14)
15
16// usageFlush drains the edge meter and CAS-merges the deltas into the box's
17// usage/edge day object, then (when am is non-nil) drains the analytics
18// meter's deltas into their app-day objects (spec §3.3: the same tick, a
19// second CAS write per app). Usage writes go to s (rebased under
20// CARLOS_STATUS_PREFIX on a fleet box); analytics writes go to raw — a day
21// file lives under apps/<sqid>/<app>/analytics/, the app's own data, and
22// must NOT be rebased (same reasoning as log shipping — logship.go:54-61).
23// A failed analytics batch is re-queued for the next tick; a failed usage
24// write is returned but does not block the analytics flush.
25func usageFlush(ctx context.Context, s, raw release.Store, box string, drain func() map[string]usage.EdgeRow, am *analytics.Meter, now time.Time) error {
26 err := usage.UpsertEdgeDay(ctx, s, box, now, drain())
27 if am == nil {
28 return err
29 }
30 failed := map[analytics.AppKey][]analytics.Delta{}
31 for k, ds := range am.Drain() {
32 for _, d := range ds {
33 uerr := analytics.UpsertDay(ctx, raw, k.Acct, k.App, d)
34 if errors.Is(uerr, applog.ErrBadKeySegment) {
35 // No key, so no retry can ever land it: drop, never re-queue.
36 unshippable(applog.FamilyAnalytics, k, uerr)
37 continue
38 }
39 if uerr != nil {
40 log.Printf("agent: analytics flush %s/%s %s: %v (re-queued)", k.Acct, k.App, d.Date, uerr)
41 failed[k] = append(failed[k], d)
42 }
43 }
44 }
45 if len(failed) > 0 {
46 am.Requeue(failed)
47 }
48 return err
49}
50
51// usageLoop flushes edge usage every 5 minutes, forever — the metricsLoop
52// conventions exactly: recover guard, disabled with a log line if the box
53// has no status store (mirroring metricsLoop's errNoStatusStore vs. other-error
54// distinction at metrics_loop.go:773-779, rather than the brief's single log
55// line, so the two loops read the same way in the logs), errors logged and
56// never fatal (the drained window is lost, the next one is not; modelling
57// grade tolerates the gap). am is the analytics meter (nil disables the
58// analytics half of the flush); its deltas are folded in on the same tick.
59func usageLoop(drain func() map[string]usage.EdgeRow, am *analytics.Meter) {
60 defer func() {
61 if r := recover(); r != nil {
62 log.Printf("agent: usage loop panic, edge metering stopped for this process: %v", r)
63 }
64 }()
65 ctx := context.Background()
66 // Same selection rule statusLoop/metricsLoop use (agent.go's
67 // openStatusStore): a status/deployment bucket wins and keeps direct S3;
68 // a fleet box with neither set publishes through the edge's store door.
69 raw, target, err := openStatusStore(ctx, os.Getenv)
70 if err != nil {
71 if errors.Is(err, errNoStatusStore) {
72 log.Printf("agent: usage loop disabled: neither CARLOS_STATUS_BUCKET nor CARLOS_DEPLOYMENT_BUCKET is set")
73 } else {
74 log.Printf("agent: usage loop disabled: %v", err)
75 }
76 return
77 }
78 s := release.NewPrefixStore(raw, resolveStatusPrefix(os.Getenv))
79 box := metricsBoxLabel(os.Getenv, readMachineID())
80 log.Printf("agent: usage loop armed (5min flush), publishing to %s", target)
81 for {
82 time.Sleep(5 * time.Minute)
83 if err := usageFlush(ctx, s, raw, box, drain, am, time.Now()); err != nil {
84 log.Printf("agent: usage flush: %v", err)
85 }
86 }
87}
88