Introduction
Garden Night's entrance checks now classify responses correctly, but the first draft processes assignments one at a time. A slow entrance delays when other useful results become available. You will share the work among a small fixed number of Go workers and still return every assigned result.
This independent unit already includes a completed single-target checker. You should know Go functions, structs, slices, modules, basic channel syntax, and the HTTP outcomes from the previous unit or equivalent experience. Goroutines, channel closing, and worker completion are explained before use. Only prepared local targets are requested; this is a one-time tool, not a benchmark or monitoring platform. Allow about 60 minutes including reading and ordinary mistakes; this is an author estimate.
Observe Real Work Before Sharing It
In this step, run the current serial checker and inspect actual service observations. Start in Workspace. cd chooses the project; export adds the prepared Go directory to this shell's command search path. Repeat both lines in a new terminal session.
cd /home/labex/project/site-checker
export PATH=/usr/local/go/bin:$PATH
Read the checking function, the work loop, and the non-timeout assignment list. cat only displays these files:
cat check.go run.go overlap.json
Check already returns ok, http_error, content_mismatch, connection_error, or timeout, retains task identity and received status, and closes each body. The serial Run ignores its workers parameter. That is the missing behavior you will change.
The overlap.json entrances each do a finite 250ms of prepared work. Use a two-second per-target timeout so this observation concerns concurrency, not client timeouts. go run . runs this package; -input selects assignments, -workers requests a maximum number of active checks, and -timeout 2s sets the request budget. > writes standard output to a file.
go run . -input overlap.json -workers 2 -timeout 2s > normal-report.json
Read it:
cat normal-report.json
All six checks should be ok. This report alone does not show whether they overlapped. The prepared observer records enter and exit events when a real server handler starts and finishes; active counts handlers actually processing requests. The small Python reader retrieves these existing observations without adding target requests:
python3 observe.py
On this fresh serial scene, expect Actual handler peak: 1; entries and exits alternate with at most one active handler. The observer was ready before these requests. The finite 250ms work gives concurrent clients an opportunity to overlap; a delay or total runtime alone does not prove that overlap happened. The trace is the evidence.
For now the draft can produce a complete report, but its maximum active work is still one. Keep that report as the normal sample for this step. The next step replaces only run.go; the event service, target definitions, and checking function stay available.
Give a Fixed Number of Workers the Jobs
In this step, design how jobs and results move, then edit Run in Workspace. Keep the completed Check and the prepared inputs unchanged. Your result is a working pool, not just several started goroutines.
Decide who owns each channel
A goroutine is a function running concurrently with its caller; go func() { ... }() starts an anonymous function immediately. Starting it does not prove an HTTP request is active. A channel transfers typed values: make(chan Target) creates a jobs channel, and make(chan Result) creates a results channel. On these unbuffered channels, jobs <- target sends a value and waits for a receiver. for target := range jobs receives until the channel is closed and drained. close(jobs) means no more values will be sent; already received values remain valid.
Before editing, decide which part of your program will send jobs, receive jobs, send results, receive results, and close each channel. There should be one job producer and one result collector. Workers receive jobs and send results. Only the producer knows when no more jobs will be sent, so it closes jobs. A worker cannot safely close results while another worker might still send.
sync.WaitGroup tracks completion. A declaration such as var finished sync.WaitGroup starts at zero. Call finished.Add(1) before starting each worker; inside that worker, defer finished.Done() marks its exit. finished.Wait() waits until every registered worker has called Done. The results channel can close only after that wait finishes. Give that waiting-and-closing responsibility to a separate goroutine: if the main goroutine waited before receiving, workers could remain blocked on their result sends. The producer also runs separately so the main goroutine can receive results while work is being assigned.
To start the requested number of workers, a counting loop such as for i := 0; i < workers; i++ { ... } initializes i to zero, checks the upper bound before each iteration, and increments i after it. Its body starts one worker each time. Each worker must call Check synchronously, send its returned result, and only then receive another job. That bounds active client checks by the number of workers. Do not start a new goroutine for each target inside the worker. Share one http.Client with Timeout: budget; that client supports concurrent requests. Only the collector appends to the report slice, so workers do not race over a shared slice or map. Different completion orders are valid.
For an empty target list, the producer still closes jobs. Workers then finish, the closer closes results, and the collector can return an initialized empty slice. Trace this sequence before writing code. Also explain why one target with three workers must finish, and why several targets with one worker must be serial.
Edit one coherent work loop
Back up the file with a non-.go extension, so Go does not compile a second definition of Run. cp copies the file without changing the original:
cp run.go run.go.bak
Open the existing file:
nano run.go
Use arrow keys to move, type to insert text, and use Backspace or Delete to remove characters. Ctrl means hold Control while pressing the other key. Ctrl+W, func Run, Enter searches for the function; Ctrl+C cancels a search. Ctrl+K cuts the current line, so use it only when you intend to remove that line. Save with Ctrl+O, confirm run.go, then Enter; Ctrl+X exits. To discard unintended changes, press Ctrl+X, then N when nano asks whether to save. At an unwanted filename prompt, Ctrl+C cancels that prompt so you can correct the edit or save to the original filename.
Keep the function signature and the existing HTTP client. Add sync to the import group when you use its WaitGroup. Replace the serial target loop with your design, working through these responsibilities in order:
First, create typed jobs and results channels and a worker-completion WaitGroup. Register and start exactly workers workers. Each worker receives until jobs is closed, sends every result from Check, and marks its completion on return.
Next, start a separate producer that sends every target, including repeated URLs with different IDs. Close jobs after the final send, including when there were no targets.
Also start a separate completion goroutine that waits for all workers, then closes results. It must not close results while a worker can still send.
Finally, in the main goroutine, initialize an empty result slice, receive every result until the results channel closes, append each one, and return the slice.
Write all four parts before running the program. An incomplete pool with no sender, no closer, or no collector is not a useful intermediate experiment. You are deciding how the parts connect; there is no supplied finished Run to paste. The names of your local variables or equivalent synchronization choices are not graded.
Save and exit, then inspect your actual file. cat displays it without changing it:
cat run.go
gofmt -w formats source in place. A syntax error identifies the file and line: reopen run.go, repair that area, save, and repeat formatting. Do not overwrite your completed parts with a new whole-file answer.
gofmt -w run.go
Check real overlapping work
Now run the complete pool on the non-timeout assignments with two workers. The two-second budget lets their finite 250ms work finish normally:
go run . -input overlap.json -workers 2 -timeout 2s
Every assignment should return an ok result. Read the observer's existing records:
python3 observe.py
On this scene, the cumulative trace should now show Actual handler peak: 2, with an enter while another handler is still active. If the peak remains one, inspect how workers receive and call Check; elapsed time cannot prove success. An unusually delayed scheduler can make an observation inconclusive. Once the latest trace ends with exit and active=0, the finite requests have actually finished; you may repeat the harmless batch and read its new events.
A cumulative peak above two means over-limit work occurred at some point. An earlier incorrect implementation leaves that peak behind after you fix the source. Read the latest entry/exit events together and use Verify, which runs your original source in an independent fixture, to assess the current implementation. Do not clear history or reset the VM to erase an earlier peak.
If the command remains stuck instead of returning, Ctrl+C interrupts this foreground invocation; the separately prepared event service stays available. Reopen run.go and check the channel ownership chain: were all jobs sent, did the producer close jobs, do workers call Done, can the collector receive while they send, and does the closer eventually close results? Repair locally and format. Before starting another concurrency observation, run python3 observe.py and confirm its latest recorded event is an exit with active=0. If finite requests are still active, read again until the recorded condition is true; do not use a guessed sleep or delete observations. Client timeouts do not immediately stop server work.
If a file is missing, use pwd and the first step's cd to recover the directory. If go is unavailable in a new shell, repeat that step's session export. Neither an ordinary edit nor a path mistake requires changing global shell files or resetting the VM.
Finish Every Assignment Within Its Budget
In this step, use the pool for the event report and exercise its boundaries. Read the actual assignments before running them:
cat targets.json
The expected result kinds are two ok, one http_error with status 403, one content_mismatch with status 200, one timeout with status zero, and one connection_error with status zero. The ticket service's delay is a finite 600ms; the 200ms client budget intentionally ends that check sooner. Both IDs for the repeated check-in URL must remain in the report.
Save a new report using two workers. > replaces only report.json; errors remain visible in the terminal.
go run . -input targets.json -workers 2 -timeout 200ms > report.json
Read your real saved file:
cat report.json
Row order may differ from the input list and between runs. Match by task ID: a slow request may finish after a later fast request. A single worker never starts its next target until Check returns, but a timed-out server handler can continue finite work after its client has left. Therefore the course measures the worker upper bound on separate non-timeout requests; it does not mislabel lingering timed-out handlers as a pool bug.
A pool must also handle less work than it has workers and no work at all. These commands use prepared lists and show output directly, leaving report.json intact:
go run . -input single.json -workers 3 -timeout 2s
go run . -input empty.json -workers 3 -timeout 2s
Expect one ok result, then [], with both commands finishing. Finally request an upper bound of one on several non-timeout targets:
go run . -input overlap.json -workers 1 -timeout 2s
All results remain ok. This last invocation must be serial even though your code can start several workers. The observer's cumulative peak retains earlier activity, so it cannot isolate this invocation's upper bound. The course's check creates a separate fresh fixture for each worker budget, including one, two, and three. For multi-target budgets of two or three it requires real overlap of at least two and no more than the requested upper bound; it does not require a peak exactly equal to three. Empty and single-target batches have no overlap requirement.
Keep your complete event report and the original inputs and service. You now have a reusable command that combines bounded active work with complete results, including honest failures.
Summary
You used job and result channels with explicit close ownership, a fixed worker count, and completion synchronization. A single collector kept every task result without shared-slice races. Real server events distinguished serial, bounded concurrent, and excessive work, while separate timeout tests avoided mistaking lingering server work for a worker-limit violation. Your saved report remains useful even when some entrances fail.



