Skip to content

Commit df5d596

Browse files
committed
Minor Kyle updates
1 parent b5c1662 commit df5d596

File tree

8 files changed

+174
-178
lines changed

8 files changed

+174
-178
lines changed

coderd/coderd.go

-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package coderd
22

33
import (
4-
"context"
54
"crypto/x509"
65
"io"
76
"net/http"
@@ -116,8 +115,6 @@ func New(options *Options) *API {
116115
options.Logger.Named("metrics_cache"),
117116
)
118117

119-
metricsCache.Start(context.Background())
120-
121118
r := chi.NewRouter()
122119
api := &API{
123120
Options: options,

coderd/metrics.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func (api *API) daus(rw http.ResponseWriter, r *http.Request) {
2525
httpapi.Forbidden(rw)
2626
return
2727
}
28-
resp := api.metricsCache.GetDAUs()
28+
resp := api.metricsCache.DAUs()
2929
if resp.Entries == nil {
3030
resp.Entries = []codersdk.DAUEntry{}
3131
}

coderd/metrics_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func TestWorkspaceReportStats(t *testing.T) {
8080
daus, err := client.GetDAUsFromAgentStats(context.Background())
8181
require.NoError(t, err)
8282

83-
require.Equal(t, &codersdk.GetDAUsResponse{
83+
require.Equal(t, &codersdk.DAUsResponse{
8484
Entries: []codersdk.DAUEntry{},
8585
}, daus, "no DAUs when stats are empty")
8686

@@ -106,7 +106,7 @@ func TestWorkspaceReportStats(t *testing.T) {
106106
daus, err = client.GetDAUsFromAgentStats(context.Background())
107107
require.NoError(t, err)
108108

109-
require.Equal(t, &codersdk.GetDAUsResponse{
109+
require.Equal(t, &codersdk.DAUsResponse{
110110
Entries: []codersdk.DAUEntry{
111111
{
112112

coderd/metricscache/cache.go

-162
This file was deleted.

coderd/metricscache/doc.go

-5
This file was deleted.

coderd/metricscache/metricscache.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package metricscache
2+
3+
import (
4+
"context"
5+
"os"
6+
"strconv"
7+
"sync/atomic"
8+
"time"
9+
10+
"golang.org/x/xerrors"
11+
12+
"cdr.dev/slog"
13+
"github.com/coder/coder/coderd/database"
14+
"github.com/coder/coder/codersdk"
15+
"github.com/coder/retry"
16+
)
17+
18+
// Cache holds the DAU cache and, later, the
19+
// user activity cache. The aggregation queries responsible for these values
20+
// can take up to a minute on large deployments, but the cache has near zero
21+
// effect on most deployments.
22+
type Cache struct {
23+
database database.Store
24+
log slog.Logger
25+
26+
dausResponse atomic.Pointer[codersdk.DAUsResponse]
27+
28+
doneCh chan struct{}
29+
cancel func()
30+
}
31+
32+
func New(db database.Store, log slog.Logger) *Cache {
33+
ctx, cancel := context.WithCancel(context.Background())
34+
35+
c := &Cache{
36+
database: db,
37+
log: log,
38+
doneCh: make(chan struct{}),
39+
cancel: cancel,
40+
}
41+
go c.run(ctx)
42+
return c
43+
}
44+
45+
const CacheRefreshIntervalEnv = "CODER_METRICS_CACHE_INTERVAL_MS"
46+
47+
func fillEmptyDAUDays(rows []database.GetDAUsFromAgentStatsRow) []database.GetDAUsFromAgentStatsRow {
48+
var newRows []database.GetDAUsFromAgentStatsRow
49+
50+
for i, row := range rows {
51+
if i == 0 {
52+
newRows = append(newRows, row)
53+
continue
54+
}
55+
56+
last := rows[i-1]
57+
58+
const day = time.Hour * 24
59+
diff := row.Date.Sub(last.Date)
60+
for diff > day {
61+
if diff <= day {
62+
break
63+
}
64+
last.Date = last.Date.Add(day)
65+
last.Daus = 0
66+
newRows = append(newRows, last)
67+
diff -= day
68+
}
69+
70+
newRows = append(newRows, row)
71+
continue
72+
}
73+
74+
return newRows
75+
}
76+
77+
func (c *Cache) refresh(ctx context.Context) error {
78+
err := c.database.DeleteOldAgentStats(ctx)
79+
if err != nil {
80+
return xerrors.Errorf("delete old stats: %w", err)
81+
}
82+
83+
daus, err := c.database.GetDAUsFromAgentStats(ctx)
84+
if err != nil {
85+
return err
86+
}
87+
88+
var resp codersdk.DAUsResponse
89+
for _, ent := range fillEmptyDAUDays(daus) {
90+
resp.Entries = append(resp.Entries, codersdk.DAUEntry{
91+
Date: ent.Date,
92+
DAUs: int(ent.Daus),
93+
})
94+
}
95+
96+
c.dausResponse.Store(&resp)
97+
return nil
98+
}
99+
100+
func (c *Cache) run(ctx context.Context) {
101+
defer close(c.doneCh)
102+
103+
interval := time.Hour
104+
105+
intervalEnv, ok := os.LookupEnv(CacheRefreshIntervalEnv)
106+
if ok {
107+
intervalMs, err := strconv.Atoi(intervalEnv)
108+
if err != nil {
109+
c.log.Error(
110+
ctx,
111+
"could not parse interval from env",
112+
slog.F("interval", intervalEnv),
113+
)
114+
} else {
115+
interval = time.Duration(intervalMs) * time.Millisecond
116+
}
117+
}
118+
119+
ticker := time.NewTicker(interval)
120+
defer ticker.Stop()
121+
122+
for {
123+
for r := retry.New(time.Millisecond*100, time.Minute); r.Wait(ctx); {
124+
start := time.Now()
125+
err := c.refresh(ctx)
126+
if err != nil {
127+
c.log.Error(ctx, "refresh", slog.Error(err))
128+
continue
129+
}
130+
c.log.Debug(
131+
ctx,
132+
"metrics refreshed",
133+
slog.F("took", time.Since(start)),
134+
slog.F("interval", interval),
135+
)
136+
break
137+
}
138+
139+
select {
140+
case <-ticker.C:
141+
case <-c.doneCh:
142+
return
143+
case <-ctx.Done():
144+
return
145+
}
146+
}
147+
}
148+
149+
func (c *Cache) Close() error {
150+
c.cancel()
151+
<-c.doneCh
152+
return nil
153+
}
154+
155+
// DAUs returns the DAUs or nil if they aren't ready yet.
156+
func (c *Cache) DAUs() codersdk.DAUsResponse {
157+
r := c.dausResponse.Load()
158+
if r == nil {
159+
return codersdk.DAUsResponse{}
160+
}
161+
return *r
162+
}

0 commit comments

Comments
 (0)