Skip to content

Commit 26e0f02

Browse files
committed
Merge branch 'main' into mafredri/feat-coderd-add-user-latency-and-insights-endpoints
2 parents ddc8606 + 1cb39fc commit 26e0f02

File tree

188 files changed

+11965
-1488
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

188 files changed

+11965
-1488
lines changed

.github/actions/setup-node/action.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ runs:
1313
cache-dependency-path: "site/yarn.lock"
1414
- name: Install node_modules
1515
shell: bash
16-
run: ./scripts/yarn_install.sh
16+
run: ../scripts/yarn_install.sh
17+
working-directory: site

.github/workflows/ci.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ jobs:
240240
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
241241
timeout-minutes: 20
242242
strategy:
243+
fail-fast: false
243244
matrix:
244245
os:
245246
- ubuntu-latest
@@ -543,7 +544,7 @@ jobs:
543544
# REMARK: this is only used to build storybook and deploy it to Chromatic.
544545
runs-on: ubuntu-latest
545546
needs: changes
546-
if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
547+
if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true'
547548
steps:
548549
- name: Checkout
549550
uses: actions/checkout@v3

.github/workflows/pr-deploy.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ concurrency:
2222

2323
jobs:
2424
pr_commented:
25-
if: github.event_name == 'issue_comment' && contains(github.event.comment.body, '/deploy-pr') && github.event.comment.author_association == 'MEMBER' || github.event_name == 'workflow_dispatch'
25+
if: (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/deploy-pr') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'OWNER')) || github.event_name == 'workflow_dispatch'
2626
outputs:
2727
PR_NUMBER: ${{ steps.pr_number.outputs.PR_NUMBER }}
2828
PR_TITLE: ${{ steps.pr_number.outputs.PR_TITLE }}

.github/workflows/release.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ jobs:
259259
env:
260260
CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
261261

262+
- name: Generate offline docs
263+
run: |
264+
version="$(./scripts/version.sh)"
265+
make -j build/coder_docs_"$version".tgz
266+
262267
- name: ls build
263268
run: ls -lh build
264269

.prettierrc.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
printWidth: 80
55
semi: false
66
trailingComma: all
7+
useTabs: false
8+
tabWidth: 2
79
overrides:
810
- files:
911
- README.md

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,10 +356,18 @@ build/coder_helm_$(VERSION).tgz:
356356
--output "$@"
357357

358358
site/out/index.html: site/package.json $(shell find ./site $(FIND_EXCLUSIONS) -type f \( -name '*.ts' -o -name '*.tsx' \))
359-
./scripts/yarn_install.sh
360359
cd site
360+
../scripts/yarn_install.sh
361361
yarn build
362362

363+
offlinedocs/out/index.html: $(shell find ./offlinedocs $(FIND_EXCLUSIONS) -type f) $(shell find ./docs $(FIND_EXCLUSIONS) -type f | sed 's: :\\ :g')
364+
cd offlinedocs
365+
../scripts/yarn_install.sh
366+
yarn export
367+
368+
build/coder_docs_$(VERSION).tgz: offlinedocs/out/index.html
369+
tar -czf "$@" -C offlinedocs/out .
370+
363371
install: build/coder_$(VERSION)_$(GOOS)_$(GOARCH)$(GOOS_BIN_EXT)
364372
install_dir="$$(go env GOPATH)/bin"
365373
output_file="$${install_dir}/coder$(GOOS_BIN_EXT)"

agent/agent.go

Lines changed: 80 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func New(options Options) Agent {
108108
}
109109
}
110110
if options.ReportMetadataInterval == 0 {
111-
options.ReportMetadataInterval = 1 * time.Minute
111+
options.ReportMetadataInterval = time.Second
112112
}
113113
if options.ServiceBannerRefreshInterval == 0 {
114114
options.ServiceBannerRefreshInterval = 2 * time.Minute
@@ -242,15 +242,15 @@ func (a *agent) runLoop(ctx context.Context) {
242242
}
243243
}
244244

245-
func (a *agent) collectMetadata(ctx context.Context, md codersdk.WorkspaceAgentMetadataDescription) *codersdk.WorkspaceAgentMetadataResult {
245+
func (a *agent) collectMetadata(ctx context.Context, md codersdk.WorkspaceAgentMetadataDescription, now time.Time) *codersdk.WorkspaceAgentMetadataResult {
246246
var out bytes.Buffer
247247
result := &codersdk.WorkspaceAgentMetadataResult{
248248
// CollectedAt is set here for testing purposes and overrode by
249249
// coderd to the time of server receipt to solve clock skew.
250250
//
251251
// In the future, the server may accept the timestamp from the agent
252252
// if it can guarantee the clocks are synchronized.
253-
CollectedAt: time.Now(),
253+
CollectedAt: now,
254254
}
255255
cmdPty, err := a.sshServer.CreateCommand(ctx, md.Script, nil)
256256
if err != nil {
@@ -298,54 +298,68 @@ type metadataResultAndKey struct {
298298
}
299299

300300
type trySingleflight struct {
301-
m sync.Map
301+
mu sync.Mutex
302+
m map[string]struct{}
302303
}
303304

304305
func (t *trySingleflight) Do(key string, fn func()) {
305-
_, loaded := t.m.LoadOrStore(key, struct{}{})
306-
if !loaded {
307-
// There is already a goroutine running for this key.
306+
t.mu.Lock()
307+
_, ok := t.m[key]
308+
if ok {
309+
t.mu.Unlock()
308310
return
309311
}
310312

311-
defer t.m.Delete(key)
313+
t.m[key] = struct{}{}
314+
t.mu.Unlock()
315+
defer func() {
316+
t.mu.Lock()
317+
delete(t.m, key)
318+
t.mu.Unlock()
319+
}()
320+
312321
fn()
313322
}
314323

315324
func (a *agent) reportMetadataLoop(ctx context.Context) {
316325
const metadataLimit = 128
317326

318327
var (
319-
baseTicker = time.NewTicker(a.reportMetadataInterval)
320-
lastCollectedAts = make(map[string]time.Time)
321-
metadataResults = make(chan metadataResultAndKey, metadataLimit)
328+
baseTicker = time.NewTicker(a.reportMetadataInterval)
329+
lastCollectedAtMu sync.RWMutex
330+
lastCollectedAts = make(map[string]time.Time)
331+
metadataResults = make(chan metadataResultAndKey, metadataLimit)
332+
logger = a.logger.Named("metadata")
322333
)
323334
defer baseTicker.Stop()
324335

325336
// We use a custom singleflight that immediately returns if there is already
326337
// a goroutine running for a given key. This is to prevent a build-up of
327338
// goroutines waiting on Do when the script takes many multiples of
328339
// baseInterval to run.
329-
var flight trySingleflight
340+
flight := trySingleflight{m: map[string]struct{}{}}
341+
342+
postMetadata := func(mr metadataResultAndKey) {
343+
err := a.client.PostMetadata(ctx, mr.key, *mr.result)
344+
if err != nil {
345+
a.logger.Error(ctx, "agent failed to report metadata", slog.Error(err))
346+
}
347+
}
330348

331349
for {
332350
select {
333351
case <-ctx.Done():
334352
return
335353
case mr := <-metadataResults:
336-
lastCollectedAts[mr.key] = mr.result.CollectedAt
337-
err := a.client.PostMetadata(ctx, mr.key, *mr.result)
338-
if err != nil {
339-
a.logger.Error(ctx, "agent failed to report metadata", slog.Error(err))
340-
}
354+
postMetadata(mr)
355+
continue
341356
case <-baseTicker.C:
342357
}
343358

344359
if len(metadataResults) > 0 {
345360
// The inner collection loop expects the channel is empty before spinning up
346361
// all the collection goroutines.
347-
a.logger.Debug(
348-
ctx, "metadata collection backpressured",
362+
logger.Debug(ctx, "metadata collection backpressured",
349363
slog.F("queue_len", len(metadataResults)),
350364
)
351365
continue
@@ -357,7 +371,7 @@ func (a *agent) reportMetadataLoop(ctx context.Context) {
357371
}
358372

359373
if len(manifest.Metadata) > metadataLimit {
360-
a.logger.Error(
374+
logger.Error(
361375
ctx, "metadata limit exceeded",
362376
slog.F("limit", metadataLimit), slog.F("got", len(manifest.Metadata)),
363377
)
@@ -367,51 +381,79 @@ func (a *agent) reportMetadataLoop(ctx context.Context) {
367381
// If the manifest changes (e.g. on agent reconnect) we need to
368382
// purge old cache values to prevent lastCollectedAt from growing
369383
// boundlessly.
384+
lastCollectedAtMu.Lock()
370385
for key := range lastCollectedAts {
371386
if slices.IndexFunc(manifest.Metadata, func(md codersdk.WorkspaceAgentMetadataDescription) bool {
372387
return md.Key == key
373388
}) < 0 {
389+
logger.Debug(ctx, "deleting lastCollected key, missing from manifest",
390+
slog.F("key", key),
391+
)
374392
delete(lastCollectedAts, key)
375393
}
376394
}
395+
lastCollectedAtMu.Unlock()
377396

378397
// Spawn a goroutine for each metadata collection, and use a
379398
// channel to synchronize the results and avoid both messy
380399
// mutex logic and overloading the API.
381400
for _, md := range manifest.Metadata {
382-
collectedAt, ok := lastCollectedAts[md.Key]
383-
if ok {
384-
// If the interval is zero, we assume the user just wants
385-
// a single collection at startup, not a spinning loop.
386-
if md.Interval == 0 {
387-
continue
388-
}
389-
// The last collected value isn't quite stale yet, so we skip it.
390-
if collectedAt.Add(a.reportMetadataInterval).After(time.Now()) {
391-
continue
392-
}
393-
}
394-
395401
md := md
396402
// We send the result to the channel in the goroutine to avoid
397403
// sending the same result multiple times. So, we don't care about
398404
// the return values.
399405
go flight.Do(md.Key, func() {
406+
ctx := slog.With(ctx, slog.F("key", md.Key))
407+
lastCollectedAtMu.RLock()
408+
collectedAt, ok := lastCollectedAts[md.Key]
409+
lastCollectedAtMu.RUnlock()
410+
if ok {
411+
// If the interval is zero, we assume the user just wants
412+
// a single collection at startup, not a spinning loop.
413+
if md.Interval == 0 {
414+
return
415+
}
416+
intervalUnit := time.Second
417+
// reportMetadataInterval is only less than a second in tests,
418+
// so adjust the interval unit for them.
419+
if a.reportMetadataInterval < time.Second {
420+
intervalUnit = 100 * time.Millisecond
421+
}
422+
// The last collected value isn't quite stale yet, so we skip it.
423+
if collectedAt.Add(time.Duration(md.Interval) * intervalUnit).After(time.Now()) {
424+
return
425+
}
426+
}
427+
400428
timeout := md.Timeout
401429
if timeout == 0 {
402-
timeout = md.Interval
430+
if md.Interval != 0 {
431+
timeout = md.Interval
432+
} else if interval := int64(a.reportMetadataInterval.Seconds()); interval != 0 {
433+
// Fallback to the report interval
434+
timeout = interval * 3
435+
} else {
436+
// If the interval is still 0 (possible if the interval
437+
// is less than a second), default to 5. This was
438+
// randomly picked.
439+
timeout = 5
440+
}
403441
}
404-
ctx, cancel := context.WithTimeout(ctx,
405-
time.Duration(timeout)*time.Second,
406-
)
442+
ctxTimeout := time.Duration(timeout) * time.Second
443+
ctx, cancel := context.WithTimeout(ctx, ctxTimeout)
407444
defer cancel()
408445

446+
now := time.Now()
409447
select {
410448
case <-ctx.Done():
449+
logger.Warn(ctx, "metadata collection timed out", slog.F("timeout", ctxTimeout))
411450
case metadataResults <- metadataResultAndKey{
412451
key: md.Key,
413-
result: a.collectMetadata(ctx, md),
452+
result: a.collectMetadata(ctx, md, now),
414453
}:
454+
lastCollectedAtMu.Lock()
455+
lastCollectedAts[md.Key] = now
456+
lastCollectedAtMu.Unlock()
415457
}
416458
})
417459
}

agent/agent_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,7 @@ func TestAgent_StartupScript(t *testing.T) {
10661066
t.Parallel()
10671067
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
10681068
client := agenttest.NewClient(t,
1069+
logger,
10691070
uuid.New(),
10701071
agentsdk.Manifest{
10711072
StartupScript: command,
@@ -1097,6 +1098,7 @@ func TestAgent_StartupScript(t *testing.T) {
10971098
t.Parallel()
10981099
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
10991100
client := agenttest.NewClient(t,
1101+
logger,
11001102
uuid.New(),
11011103
agentsdk.Manifest{
11021104
StartupScript: command,
@@ -1470,6 +1472,7 @@ func TestAgent_Lifecycle(t *testing.T) {
14701472
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
14711473

14721474
client := agenttest.NewClient(t,
1475+
logger,
14731476
uuid.New(),
14741477
agentsdk.Manifest{
14751478
DERPMap: derpMap,
@@ -1742,6 +1745,7 @@ func TestAgent_Reconnect(t *testing.T) {
17421745
statsCh := make(chan *agentsdk.Stats, 50)
17431746
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
17441747
client := agenttest.NewClient(t,
1748+
logger,
17451749
agentID,
17461750
agentsdk.Manifest{
17471751
DERPMap: derpMap,
@@ -1776,6 +1780,7 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) {
17761780
defer coordinator.Close()
17771781

17781782
client := agenttest.NewClient(t,
1783+
logger,
17791784
uuid.New(),
17801785
agentsdk.Manifest{
17811786
GitAuthConfigs: 1,
@@ -1900,7 +1905,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Durati
19001905
})
19011906
statsCh := make(chan *agentsdk.Stats, 50)
19021907
fs := afero.NewMemMapFs()
1903-
c := agenttest.NewClient(t, metadata.AgentID, metadata, statsCh, coordinator)
1908+
c := agenttest.NewClient(t, logger.Named("agent"), metadata.AgentID, metadata, statsCh, coordinator)
19041909

19051910
options := agent.Options{
19061911
Client: c,

0 commit comments

Comments
 (0)