Skip to content

Commit 8640164

Browse files
authored
feat(lock): Add update CLI command (#92)
1 parent 4c5e7d3 commit 8640164

File tree

12 files changed

+499
-20
lines changed

12 files changed

+499
-20
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
applyTo: "**/*.go"
3+
description: "Concurrency patterns for parallel task execution. Follow these patterns when writing code that runs tasks concurrently."
4+
---
5+
6+
# Concurrency Patterns
7+
8+
When running tasks in parallel (e.g., per-component operations), follow these patterns:
9+
10+
## Concurrency Limits
11+
12+
Use `env.IOBoundConcurrency()` or `env.CPUBoundConcurrency()` from `azldev.Env` to determine the worker count:
13+
14+
- **I/O-bound** (network clones, file copies): `env.IOBoundConcurrency()` — 2× CPU count
15+
- **CPU-bound** (hashing, parsing): `env.CPUBoundConcurrency()` — 1× CPU count
16+
17+
**Never launch unbounded goroutines.** With 7k+ components, unbounded parallelism will overwhelm the system (loadavg 500+).
18+
19+
## Semaphore Pattern
20+
21+
Use a buffered channel as a semaphore to limit concurrency:
22+
23+
```go
24+
semaphore := make(chan struct{}, env.IOBoundConcurrency())
25+
26+
go func() {
27+
select {
28+
case semaphore <- struct{}{}:
29+
defer func() { <-semaphore }()
30+
case <-ctx.Done():
31+
// handle cancellation
32+
return
33+
}
34+
// do work
35+
}()
36+
```
37+
38+
## Cancellation (Ctrl+C)
39+
40+
Always support graceful cancellation via `env.WithCancel()`:
41+
42+
1. Create a cancellable child env: `workerEnv, cancel := env.WithCancel(); defer cancel()`
43+
2. Use context-aware semaphore acquisition (select on semaphore and `workerEnv.Done()`)
44+
3. Pass `workerEnv` (not `env`) to worker goroutines so they respect cancellation
45+
46+
Example from `render.go` and `update.go`:
47+
48+
```go
49+
workerEnv, cancel := env.WithCancel()
50+
defer cancel()
51+
52+
semaphore := make(chan struct{}, env.IOBoundConcurrency())
53+
54+
for idx, item := range items {
55+
waitGroup.Add(1)
56+
go func() {
57+
defer waitGroup.Done()
58+
59+
select {
60+
case semaphore <- struct{}{}:
61+
defer func() { <-semaphore }()
62+
case <-workerEnv.Done():
63+
results[idx].Error = "context cancelled"
64+
return
65+
}
66+
67+
// Do work using workerEnv (not env)
68+
doWork(workerEnv, item)
69+
}()
70+
}
71+
72+
waitGroup.Wait()
73+
```
74+
75+
## Limits
76+
77+
The `Env` type provides a set of methods to determine appropriate concurrency limits for different types of tasks: `IOBoundConcurrency()`, `CPUBoundConcurrency()`, and `FastConcurrency()`. Use these methods to set the size of worker pools or semaphores based on the nature of the tasks being performed.
78+
79+
## Thread Safety
80+
81+
- Events are currently not thread-safe; control long-running event handling outside of worker goroutines.

.github/workflows/devcontainer.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: "Check devcontainer build"
22
on:
33
push:
4-
branches: [ main ]
4+
branches: [main]
55
pull_request:
6-
branches: [ main ]
6+
branches: [main]
77
workflow_dispatch:
88
schedule:
99
# Run every night at 3:15am PST (11:15am UTC)
10-
- cron: '15 11 * * *'
10+
- cron: "15 11 * * *"
1111

1212
# Cancel in-progress runs of this workflow if a new run is triggered.
1313
concurrency:
@@ -32,6 +32,7 @@ jobs:
3232
# We disable the unix-chkpwd AppArmor profile to avoid errors building the container.
3333
# We also need to purge a few packages to avoid profile conflicts.
3434
set -euxo pipefail
35+
sudo apt-get update
3536
sudo apt-get purge firefox passt
3637
sudo systemctl reload apparmor.service
3738
sudo apt-get install apparmor-utils

.github/workflows/test.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: "Run tests"
22
on:
33
push:
4-
branches: [ main ]
4+
branches: [main]
55
pull_request:
6-
branches: [ main ]
6+
branches: [main]
77
workflow_dispatch:
88
schedule:
99
# Run every night at 3:15am PST (11:15am UTC)
10-
- cron: '15 11 * * *'
10+
- cron: "15 11 * * *"
1111

1212
# Cancel in-progress runs of this workflow if a new run is triggered.
1313
concurrency:
@@ -53,6 +53,7 @@ jobs:
5353
# containerized tests. We also need to purge a few packages to avoid
5454
# profile conflicts.
5555
set -euxo pipefail
56+
sudo apt-get update
5657
sudo apt-get purge firefox passt
5758
sudo systemctl reload apparmor.service
5859
sudo apt-get install apparmor-utils

docs/user/reference/cli/azldev_component.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/user/reference/cli/azldev_component_update.md

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/app/azldev/cmds/component/component.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ components defined in the project configuration.`,
3030
prepareOnAppInit(app, cmd)
3131
queryOnAppInit(app, cmd)
3232
renderOnAppInit(app, cmd)
33+
updateOnAppInit(app, cmd)
3334
}

internal/app/azldev/cmds/component/render.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"fmt"
99
"log/slog"
1010
"path/filepath"
11-
"runtime"
1211
"slices"
1312
"strings"
1413
"sync"
@@ -115,13 +114,6 @@ const (
115114
renderStatusCancelled = "cancelled"
116115
)
117116

118-
// concurrentRenderLimit returns the number of concurrent goroutines for phases 1 and 3.
119-
// Each goroutine involves git clone + overlay (phase 1) or file filtering + copy (phase 3),
120-
// so this bounds I/O parallelism. Uses 2x CPU count since these operations are I/O-bound.
121-
func concurrentRenderLimit() int {
122-
return max(1, 2*runtime.NumCPU()) //nolint:mnd // 2x CPU empirically chosen via benchmarking
123-
}
124-
125117
// RenderComponents renders the post-overlay spec and sidecar files for each
126118
// selected component into the output directory. Processing is done in three phases:
127119
// 1. Parallel source preparation (clone, overlay, synthetic git)
@@ -290,7 +282,7 @@ func parallelPrepare(
290282
defer cancel()
291283

292284
resultsChan := make(chan prepResult, len(comps))
293-
semaphore := make(chan struct{}, concurrentRenderLimit())
285+
semaphore := make(chan struct{}, env.IOBoundConcurrency())
294286

295287
var waitGroup sync.WaitGroup
296288

@@ -486,7 +478,7 @@ func batchMockProcess(
486478
}
487479
}
488480

489-
mockResults, err := mockProcessor.BatchProcess(env, env, stagingDir, inputs, env.FS())
481+
mockResults, err := mockProcessor.BatchProcess(env, env, stagingDir, inputs, env.FS(), env.CPUBoundConcurrency())
490482
if err != nil {
491483
slog.Error("Batch mock processing failed", "error", err)
492484
// Return empty map — all components will get reported as errors in phase 3.
@@ -532,7 +524,7 @@ func parallelFinish(
532524
}
533525

534526
resultsChan := make(chan finishResult, len(prepared))
535-
semaphore := make(chan struct{}, concurrentRenderLimit())
527+
semaphore := make(chan struct{}, env.IOBoundConcurrency())
536528

537529
var waitGroup sync.WaitGroup
538530

0 commit comments

Comments
 (0)