@@ -2,6 +2,7 @@ package dbrollup
2
2
3
3
import (
4
4
"context"
5
+ "flag"
5
6
"time"
6
7
7
8
"golang.org/x/sync/errgroup"
@@ -19,36 +20,68 @@ const (
19
20
DefaultInterval = 5 * time .Minute
20
21
)
21
22
23
+ type Event struct {
24
+ TemplateUsageStats bool
25
+ }
26
+
22
27
type Rolluper struct {
23
- cancel context.CancelFunc
24
- closed chan struct {}
25
- db database.Store
26
- logger slog.Logger
28
+ cancel context.CancelFunc
29
+ closed chan struct {}
30
+ db database.Store
31
+ logger slog.Logger
32
+ interval time.Duration
33
+ event chan <- Event
34
+ }
35
+
36
+ type Option func (* Rolluper )
37
+
38
+ // WithInterval sets the interval between rollups.
39
+ func WithInterval (interval time.Duration ) Option {
40
+ return func (r * Rolluper ) {
41
+ r .interval = interval
42
+ }
43
+ }
44
+
45
+ // WithEventChannel sets the event channel to use for rollup events.
46
+ //
47
+ // This is only used for testing.
48
+ func WithEventChannel (ch chan <- Event ) Option {
49
+ if flag .Lookup ("test.v" ) == nil {
50
+ panic ("developer error: WithEventChannel is not to be used outside of tests" )
51
+ }
52
+ return func (r * Rolluper ) {
53
+ r .event = ch
54
+ }
27
55
}
28
56
29
57
// New creates a new DB rollup service that periodically runs rollup queries.
30
58
// It is the caller's responsibility to call Close on the returned instance.
31
59
//
32
60
// This is for e.g. generating insights data (template_usage_stats) from
33
61
// raw data (workspace_agent_stats, workspace_app_stats).
34
- func New (logger slog.Logger , db database.Store , interval time. Duration ) * Rolluper {
62
+ func New (logger slog.Logger , db database.Store , opts ... Option ) * Rolluper {
35
63
ctx , cancel := context .WithCancel (context .Background ())
36
64
37
65
r := & Rolluper {
38
- cancel : cancel ,
39
- closed : make (chan struct {}),
40
- db : db ,
41
- logger : logger .Named ("dbrollup" ),
66
+ cancel : cancel ,
67
+ closed : make (chan struct {}),
68
+ db : db ,
69
+ logger : logger ,
70
+ interval : DefaultInterval ,
71
+ }
72
+
73
+ for _ , opt := range opts {
74
+ opt (r )
42
75
}
43
76
44
77
//nolint:gocritic // The system rolls up database tables without user input.
45
78
ctx = dbauthz .AsSystemRestricted (ctx )
46
- go r .start (ctx , interval )
79
+ go r .start (ctx )
47
80
48
81
return r
49
82
}
50
83
51
- func (r * Rolluper ) start (ctx context.Context , interval time. Duration ) {
84
+ func (r * Rolluper ) start (ctx context.Context ) {
52
85
defer close (r .closed )
53
86
54
87
do := func () {
@@ -58,7 +91,7 @@ func (r *Rolluper) start(ctx context.Context, interval time.Duration) {
58
91
now := time .Now ()
59
92
60
93
// Track whether or not we performed a rollup (we got the advisory lock).
61
- templateUsageStats := false
94
+ var ev Event
62
95
63
96
eg .Go (func () error {
64
97
return r .db .InTx (func (tx database.Store ) error {
@@ -72,7 +105,7 @@ func (r *Rolluper) start(ctx context.Context, interval time.Duration) {
72
105
return nil
73
106
}
74
107
75
- templateUsageStats = true
108
+ ev . TemplateUsageStats = true
76
109
return tx .UpsertTemplateUsageStats (ctx )
77
110
}, nil )
78
111
})
@@ -86,12 +119,22 @@ func (r *Rolluper) start(ctx context.Context, interval time.Duration) {
86
119
if ctx .Err () == nil {
87
120
r .logger .Error (ctx , "failed to rollup data" , slog .Error (err ))
88
121
}
89
- } else {
90
- r .logger .Debug (ctx ,
91
- "rolled up data" ,
92
- slog .F ("took" , time .Since (now )),
93
- slog .F ("template_usage_stats" , templateUsageStats ),
94
- )
122
+ return
123
+ }
124
+
125
+ r .logger .Debug (ctx ,
126
+ "rolled up data" ,
127
+ slog .F ("took" , time .Since (now )),
128
+ slog .F ("event" , ev ),
129
+ )
130
+
131
+ // For testing.
132
+ if r .event != nil {
133
+ select {
134
+ case <- ctx .Done ():
135
+ return
136
+ case r .event <- ev :
137
+ }
95
138
}
96
139
}
97
140
@@ -108,11 +151,11 @@ func (r *Rolluper) start(ctx context.Context, interval time.Duration) {
108
151
case <- t .C :
109
152
// Ensure we're on the interval.
110
153
now := time .Now ()
111
- next := now .Add (interval ).Truncate (interval ) // Ensure we're on the interval and synced with the clock.
154
+ next := now .Add (r . interval ).Truncate (r . interval ) // Ensure we're on the interval and synced with the clock.
112
155
d := next .Sub (now )
113
156
// Safety check (shouldn't be possible).
114
157
if d <= 0 {
115
- d = interval
158
+ d = r . interval
116
159
}
117
160
t .Reset (d )
118
161
0 commit comments