mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
@@ -0,0 +1,426 @@
|
||||
package workspaceapps_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/workspaceapps"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
type fakeReporter struct {
|
||||
mu sync.Mutex
|
||||
s []workspaceapps.StatsReport
|
||||
err error
|
||||
errN int
|
||||
}
|
||||
|
||||
func (r *fakeReporter) stats() []workspaceapps.StatsReport {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.s
|
||||
}
|
||||
|
||||
func (r *fakeReporter) errors() int {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.errN
|
||||
}
|
||||
|
||||
func (r *fakeReporter) setError(err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.err = err
|
||||
}
|
||||
|
||||
func (r *fakeReporter) Report(_ context.Context, stats []workspaceapps.StatsReport) error {
|
||||
r.mu.Lock()
|
||||
if r.err != nil {
|
||||
r.errN++
|
||||
r.mu.Unlock()
|
||||
return r.err
|
||||
}
|
||||
r.s = append(r.s, stats...)
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestStatsCollector(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rollupUUID := uuid.New()
|
||||
rollupUUID2 := uuid.New()
|
||||
someUUID := uuid.New()
|
||||
|
||||
rollupWindow := time.Minute
|
||||
start := database.Now().Truncate(time.Minute).UTC()
|
||||
end := start.Add(10 * time.Second)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
flushIncrement time.Duration
|
||||
flushCount int
|
||||
stats []workspaceapps.StatsReport
|
||||
want []workspaceapps.StatsReport
|
||||
}{
|
||||
{
|
||||
name: "Single stat rolled up and reported once",
|
||||
flushIncrement: 2*rollupWindow + time.Second,
|
||||
flushCount: 10, // Only reported once.
|
||||
stats: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: end,
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
want: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow),
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Two unique stat rolled up",
|
||||
flushIncrement: 2*rollupWindow + time.Second,
|
||||
flushCount: 10, // Only reported once.
|
||||
stats: []workspaceapps.StatsReport{
|
||||
{
|
||||
AccessMethod: workspaceapps.AccessMethodPath,
|
||||
SlugOrPort: "code-server",
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: end,
|
||||
Requests: 1,
|
||||
},
|
||||
{
|
||||
AccessMethod: workspaceapps.AccessMethodTerminal,
|
||||
SessionID: rollupUUID2,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: end,
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
want: []workspaceapps.StatsReport{
|
||||
{
|
||||
AccessMethod: workspaceapps.AccessMethodPath,
|
||||
SlugOrPort: "code-server",
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow),
|
||||
Requests: 1,
|
||||
},
|
||||
{
|
||||
AccessMethod: workspaceapps.AccessMethodTerminal,
|
||||
SessionID: rollupUUID2,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow),
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple stats rolled up",
|
||||
flushIncrement: 2*rollupWindow + time.Second,
|
||||
flushCount: 2,
|
||||
stats: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: end,
|
||||
Requests: 1,
|
||||
},
|
||||
{
|
||||
SessionID: uuid.New(),
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: end,
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
want: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow),
|
||||
Requests: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Long sessions not rolled up but reported multiple times",
|
||||
flushIncrement: rollupWindow + time.Second,
|
||||
flushCount: 4,
|
||||
stats: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
want: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow + time.Second),
|
||||
Requests: 1,
|
||||
},
|
||||
{
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(2 * (rollupWindow + time.Second)),
|
||||
Requests: 1,
|
||||
},
|
||||
{
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(3 * (rollupWindow + time.Second)),
|
||||
Requests: 1,
|
||||
},
|
||||
{
|
||||
SessionID: rollupUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(4 * (rollupWindow + time.Second)),
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Incomplete stats not reported until it exceeds rollup window",
|
||||
flushIncrement: rollupWindow / 4,
|
||||
flushCount: 6,
|
||||
stats: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: someUUID,
|
||||
SessionStartedAt: start,
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
want: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: someUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow / 4 * 5),
|
||||
Requests: 1,
|
||||
},
|
||||
{
|
||||
SessionID: someUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow / 4 * 6),
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Same stat reported without and with end time and rolled up",
|
||||
flushIncrement: rollupWindow + time.Second,
|
||||
flushCount: 1,
|
||||
stats: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: someUUID,
|
||||
SessionStartedAt: start,
|
||||
Requests: 1,
|
||||
},
|
||||
{
|
||||
SessionID: someUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(10 * time.Second),
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
want: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: someUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow),
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Same non-rolled up stat reported without and with end time",
|
||||
flushIncrement: rollupWindow * 2,
|
||||
flushCount: 1,
|
||||
stats: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: someUUID,
|
||||
SessionStartedAt: start,
|
||||
Requests: 1,
|
||||
},
|
||||
{
|
||||
SessionID: someUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow * 2),
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
want: []workspaceapps.StatsReport{
|
||||
{
|
||||
SessionID: someUUID,
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(rollupWindow * 2),
|
||||
Requests: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests.
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
flush := make(chan chan<- struct{}, 1)
|
||||
var now atomic.Pointer[time.Time]
|
||||
now.Store(&start)
|
||||
|
||||
reporter := &fakeReporter{}
|
||||
collector := workspaceapps.NewStatsCollector(workspaceapps.StatsCollectorOptions{
|
||||
Reporter: reporter,
|
||||
ReportInterval: time.Hour,
|
||||
RollupWindow: rollupWindow,
|
||||
|
||||
Flush: flush,
|
||||
Now: func() time.Time { return *now.Load() },
|
||||
})
|
||||
|
||||
// Collect reports.
|
||||
for _, report := range tt.stats {
|
||||
collector.Collect(report)
|
||||
}
|
||||
|
||||
// Advance time.
|
||||
flushTime := start.Add(tt.flushIncrement)
|
||||
for i := 0; i < tt.flushCount; i++ {
|
||||
now.Store(&flushTime)
|
||||
flushDone := make(chan struct{}, 1)
|
||||
flush <- flushDone
|
||||
<-flushDone
|
||||
flushTime = flushTime.Add(tt.flushIncrement)
|
||||
}
|
||||
|
||||
var gotStats []workspaceapps.StatsReport
|
||||
require.Eventually(t, func() bool {
|
||||
gotStats = reporter.stats()
|
||||
return len(gotStats) == len(tt.want)
|
||||
}, testutil.WaitMedium, testutil.IntervalFast)
|
||||
|
||||
// Order is not guaranteed.
|
||||
sortBySessionID := func(a, b workspaceapps.StatsReport) int {
|
||||
if a.SessionID == b.SessionID {
|
||||
return int(a.SessionEndedAt.Sub(b.SessionEndedAt))
|
||||
}
|
||||
if a.SessionID.String() < b.SessionID.String() {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
slices.SortFunc(tt.want, sortBySessionID)
|
||||
slices.SortFunc(gotStats, sortBySessionID)
|
||||
|
||||
// Verify reported stats.
|
||||
for i, got := range gotStats {
|
||||
want := tt.want[i]
|
||||
|
||||
assert.Equal(t, want.SessionID, got.SessionID, "session ID; i = %d", i)
|
||||
assert.Equal(t, want.SessionStartedAt, got.SessionStartedAt, "session started at; i = %d", i)
|
||||
assert.Equal(t, want.SessionEndedAt, got.SessionEndedAt, "session ended at; i = %d", i)
|
||||
assert.Equal(t, want.Requests, got.Requests, "requests; i = %d", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsCollector_backlog(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rollupWindow := time.Minute
|
||||
flush := make(chan chan<- struct{}, 1)
|
||||
|
||||
start := database.Now().Truncate(time.Minute).UTC()
|
||||
var now atomic.Pointer[time.Time]
|
||||
now.Store(&start)
|
||||
|
||||
reporter := &fakeReporter{}
|
||||
collector := workspaceapps.NewStatsCollector(workspaceapps.StatsCollectorOptions{
|
||||
Reporter: reporter,
|
||||
ReportInterval: time.Hour,
|
||||
RollupWindow: rollupWindow,
|
||||
|
||||
Flush: flush,
|
||||
Now: func() time.Time { return *now.Load() },
|
||||
})
|
||||
|
||||
reporter.setError(xerrors.New("some error"))
|
||||
|
||||
// The first collected stat is "rolled up" and moved into the
|
||||
// backlog during the first flush. On the second flush nothing is
|
||||
// rolled up due to being unable to report the backlog.
|
||||
for i := 0; i < 2; i++ {
|
||||
collector.Collect(workspaceapps.StatsReport{
|
||||
SessionID: uuid.New(),
|
||||
SessionStartedAt: start,
|
||||
SessionEndedAt: start.Add(10 * time.Second),
|
||||
Requests: 1,
|
||||
})
|
||||
start = start.Add(time.Minute)
|
||||
now.Store(&start)
|
||||
|
||||
flushDone := make(chan struct{}, 1)
|
||||
flush <- flushDone
|
||||
<-flushDone
|
||||
}
|
||||
|
||||
// Flush was performed 2 times, 2 reports should have failed.
|
||||
wantErrors := 2
|
||||
assert.Equal(t, wantErrors, reporter.errors())
|
||||
assert.Empty(t, reporter.stats())
|
||||
|
||||
reporter.setError(nil)
|
||||
|
||||
// Flush again, this time the backlog should be reported in addition
|
||||
// to the second collected stat being rolled up and reported.
|
||||
flushDone := make(chan struct{}, 1)
|
||||
flush <- flushDone
|
||||
<-flushDone
|
||||
|
||||
assert.Equal(t, wantErrors, reporter.errors())
|
||||
assert.Len(t, reporter.stats(), 2)
|
||||
}
|
||||
|
||||
func TestStatsCollector_Close(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
reporter := &fakeReporter{}
|
||||
collector := workspaceapps.NewStatsCollector(workspaceapps.StatsCollectorOptions{
|
||||
Reporter: reporter,
|
||||
ReportInterval: time.Hour,
|
||||
RollupWindow: time.Minute,
|
||||
})
|
||||
|
||||
collector.Collect(workspaceapps.StatsReport{
|
||||
SessionID: uuid.New(),
|
||||
SessionStartedAt: database.Now(),
|
||||
SessionEndedAt: database.Now(),
|
||||
Requests: 1,
|
||||
})
|
||||
|
||||
collector.Close()
|
||||
|
||||
// Verify that stats are reported after close.
|
||||
assert.NotEmpty(t, reporter.stats())
|
||||
}
|
||||
Reference in New Issue
Block a user