Skip to content

Commit 8388eae

Browse files
Sayan-995kawpii
andauthored
add Worker pools exercise templates (zhravan#157)
Signed-off-by: Kaushalya Pradeep <24698778+kaushalyap@users.noreply.github.com> Co-authored-by: kaushalyap <24698778+kaushalyap@users.noreply.github.com>
1 parent 75ca66c commit 8388eae

4 files changed

Lines changed: 344 additions & 1 deletion

File tree

internal/exercises/catalog.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,19 @@ concepts:
217217
- "Call wg.Add(1) before starting a goroutine and wg.Done() when it finishes."
218218
- "Close result channels after wg.Wait() so collectors can range over them."
219219
- "Capture loop variables correctly inside goroutines (e.g., `n := n`)."
220-
- "Return results as a slice — order does not need to match input order
220+
- "Return results as a slice — order does not need to match input order"
221+
222+
- slug: 43_worker_pools
223+
title: Worker Pools
224+
test_regex: ".*"
225+
hints:
226+
- Create separate channels for jobs (send work) and results (receive processed data).
227+
- Launch multiple worker goroutines that read from jobs channel and write to results channel.
228+
- Use channel directions (<-chan for receive-only, chan<- for send-only) in worker function signature.
229+
- Close the jobs channel after sending all work to signal workers to finish.
230+
- Use strings.TrimSpace() to clean message strings by removing leading/trailing whitespace.
231+
- Collect exactly len(logs) results to ensure all work is processed.
232+
221233
projects:
222234
- slug: 101_text_analyzer
223235
title: Text Analyzer (Easy)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package worker_pools
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
)
8+
9+
// Use case: Log Processing System
10+
// You're building a log processing system that needs to analyze logs from
11+
// multiple servers. Each log entry needs to be parsed, validated, and
12+
// transformed before being stored. Using a worker pool pattern ensures
13+
// efficient processing of thousands of log entries without overwhelming
14+
// system resources.
15+
16+
// LogEntry represents a single log entry to process
17+
type LogEntry struct {
18+
ID int
19+
Timestamp string
20+
Level string
21+
Message string
22+
}
23+
24+
// ProcessedLog represents a log after processing
25+
type ProcessedLog struct {
26+
ID int
27+
Severity int // 1=INFO, 2=WARNING, 3=ERROR
28+
CleanedMessage string
29+
}
30+
31+
// processLog processes a single log entry
32+
func processLog(log LogEntry) ProcessedLog {
33+
fmt.Printf("Processing log ID: %d\n", log.ID)
34+
time.Sleep(5 * time.Microsecond) // Simulate processing time
35+
36+
// Convert level to severity
37+
severity := 1 // Default INFO
38+
switch log.Level {
39+
case "INFO":
40+
severity = 1
41+
case "WARNING":
42+
severity = 2
43+
case "ERROR":
44+
severity = 3
45+
}
46+
47+
// Clean message
48+
cleanedMessage := strings.TrimSpace(log.Message)
49+
50+
return ProcessedLog{
51+
ID: log.ID,
52+
Severity: severity,
53+
CleanedMessage: cleanedMessage,
54+
}
55+
}
56+
57+
// worker receives jobs from a channel, processes them, and sends results
58+
func worker(id int, jobs <-chan LogEntry, results chan<- ProcessedLog) {
59+
for job := range jobs {
60+
fmt.Printf("Worker %d processing log %d\n", id, job.ID)
61+
processed := processLog(job)
62+
results <- processed
63+
}
64+
}
65+
66+
// LogProcessor creates a worker pool and processes all logs
67+
func LogProcessor(logs []LogEntry, numWorkers int) []ProcessedLog {
68+
// Create channels
69+
jobs := make(chan LogEntry, len(logs))
70+
results := make(chan ProcessedLog, len(logs))
71+
72+
// Start worker pool
73+
for w := 1; w <= numWorkers; w++ {
74+
go worker(w, jobs, results)
75+
}
76+
77+
// Send jobs
78+
go func() {
79+
for _, log := range logs {
80+
jobs <- log
81+
}
82+
close(jobs)
83+
}()
84+
85+
// Collect results
86+
processed := make([]ProcessedLog, 0, len(logs))
87+
for i := 0; i < len(logs); i++ {
88+
result := <-results
89+
processed = append(processed, result)
90+
}
91+
92+
return processed
93+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package worker_pools
2+
3+
import (
4+
"fmt"
5+
"time"
6+
)
7+
8+
// Use case: Log Processing System
9+
// You're building a log processing system that needs to analyze logs from
10+
// multiple servers. Each log entry needs to be parsed, validated, and
11+
// transformed before being stored. Using a worker pool pattern ensures
12+
// efficient processing of thousands of log entries without overwhelming
13+
// system resources.
14+
15+
// LogEntry represents a single log entry to process
16+
type LogEntry struct {
17+
ID int
18+
Timestamp string
19+
Level string
20+
Message string
21+
}
22+
23+
// ProcessedLog represents a log after processing
24+
type ProcessedLog struct {
25+
ID int
26+
Severity int // 1=INFO, 2=WARNING, 3=ERROR
27+
CleanedMessage string
28+
}
29+
30+
// TODO: 1. Implement processLog function that processes a single log entry
31+
// It should:
32+
// - Convert log level string to severity int (INFO=1, WARNING=2, ERROR=3)
33+
// - Clean the message by trimming spaces
34+
// - Return a ProcessedLog
35+
func processLog(log LogEntry) ProcessedLog {
36+
fmt.Printf("Processing log ID: %d\n", log.ID)
37+
time.Sleep(5 * time.Millisecond) // Simulate processing time
38+
39+
// TODO: Implement log processing logic
40+
return ProcessedLog{}
41+
}
42+
43+
// TODO: 2. Implement worker function that:
44+
// - Takes an ID, jobs channel (receive-only), and results channel (send-only)
45+
// - Continuously receives LogEntry from jobs channel
46+
// - Processes each log using processLog
47+
// - Sends ProcessedLog to results channel
48+
// - Stops when jobs channel is closed
49+
func worker(id int, jobs <-chan LogEntry, results chan<- ProcessedLog) {
50+
// TODO: Implement worker logic with proper channel directions
51+
fmt.Println("Worker started")
52+
}
53+
54+
// TODO: 3. Implement LogProcessor that:
55+
// - Creates jobs and results channels
56+
// - Spawns numWorkers worker goroutines
57+
// - Sends all logs to the jobs channel in a separate goroutine
58+
// - Collects all processed results
59+
// - Returns slice of ProcessedLog
60+
func LogProcessor(logs []LogEntry, numWorkers int) []ProcessedLog {
61+
// TODO: Create channels
62+
63+
// TODO: Start worker pool
64+
65+
// TODO: Send jobs
66+
67+
// TODO: Collect results
68+
69+
processed := []ProcessedLog{}
70+
return processed
71+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package worker_pools
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestProcessLog(t *testing.T) {
10+
log := LogEntry{
11+
ID: 1,
12+
Timestamp: "2025-10-15T10:30:00Z",
13+
Level: "ERROR",
14+
Message: " Database connection failed ",
15+
}
16+
17+
result := processLog(log)
18+
19+
if result.ID != 1 {
20+
t.Errorf("Expected ID 1, got %d", result.ID)
21+
}
22+
23+
if result.Severity != 3 {
24+
t.Errorf("Expected severity 3 for ERROR, got %d", result.Severity)
25+
}
26+
27+
if result.CleanedMessage != "Database connection failed" {
28+
t.Errorf("Expected cleaned message 'Database connection failed', got '%s'", result.CleanedMessage)
29+
}
30+
}
31+
32+
func TestLogProcessor(t *testing.T) {
33+
t.Run("Process all logs successfully", func(t *testing.T) {
34+
logs := []LogEntry{
35+
{ID: 1, Timestamp: "2025-10-15T10:30:00Z", Level: "INFO", Message: " Server started "},
36+
{ID: 2, Timestamp: "2025-10-15T10:31:00Z", Level: "WARNING", Message: "High memory usage"},
37+
{ID: 3, Timestamp: "2025-10-15T10:32:00Z", Level: "ERROR", Message: "Connection timeout"},
38+
{ID: 4, Timestamp: "2025-10-15T10:33:00Z", Level: "INFO", Message: "Request completed"},
39+
{ID: 5, Timestamp: "2025-10-15T10:34:00Z", Level: "ERROR", Message: " Failed to save "},
40+
}
41+
42+
results := LogProcessor(logs, 3)
43+
44+
if len(results) != 5 {
45+
t.Fatalf("Expected 5 processed logs, got %d", len(results))
46+
}
47+
48+
// Verify all IDs are present
49+
ids := make(map[int]bool)
50+
for _, r := range results {
51+
ids[r.ID] = true
52+
}
53+
54+
for i := 1; i <= 5; i++ {
55+
if !ids[i] {
56+
t.Errorf("Missing log ID %d in results", i)
57+
}
58+
}
59+
})
60+
61+
t.Run("Processing is concurrent and faster than sequential", func(t *testing.T) {
62+
// Create 20 logs
63+
logs := make([]LogEntry, 20)
64+
for i := 0; i < 20; i++ {
65+
logs[i] = LogEntry{
66+
ID: i + 1,
67+
Timestamp: "2025-10-15T10:30:00Z",
68+
Level: "INFO",
69+
Message: "Test message",
70+
}
71+
}
72+
73+
start := time.Now()
74+
results := LogProcessor(logs, 5)
75+
elapsed := time.Since(start)
76+
77+
if len(results) != 20 {
78+
t.Fatalf("Expected 20 processed logs, got %d", len(results))
79+
}
80+
81+
// With 5 workers processing 20 logs at ~5 microseconds each:
82+
// Sequential would take ~100 microseconds
83+
// Concurrent should take ~20-30 microseconds (4 batches of 5)
84+
if elapsed.Microseconds() > 50 {
85+
t.Errorf("Processing took too long (%v), worker pool may not be working correctly", elapsed)
86+
}
87+
})
88+
89+
t.Run("Verify correct severity mapping", func(t *testing.T) {
90+
logs := []LogEntry{
91+
{ID: 1, Timestamp: "2025-10-15T10:30:00Z", Level: "INFO", Message: "info"},
92+
{ID: 2, Timestamp: "2025-10-15T10:30:00Z", Level: "WARNING", Message: "warn"},
93+
{ID: 3, Timestamp: "2025-10-15T10:30:00Z", Level: "ERROR", Message: "error"},
94+
}
95+
96+
results := LogProcessor(logs, 2)
97+
98+
severities := make(map[int]int)
99+
for _, r := range results {
100+
severities[r.ID] = r.Severity
101+
}
102+
103+
if severities[1] != 1 {
104+
t.Errorf("Expected INFO severity 1, got %d", severities[1])
105+
}
106+
if severities[2] != 2 {
107+
t.Errorf("Expected WARNING severity 2, got %d", severities[2])
108+
}
109+
if severities[3] != 3 {
110+
t.Errorf("Expected ERROR severity 3, got %d", severities[3])
111+
}
112+
})
113+
}
114+
115+
func TestWorkerChannelDirections(t *testing.T) {
116+
// This test ensures channels are properly typed with directions
117+
// by checking if the implementation compiles and runs correctly
118+
logs := []LogEntry{
119+
{ID: 1, Timestamp: "2025-10-15T10:30:00Z", Level: "INFO", Message: "test"},
120+
}
121+
122+
results := LogProcessor(logs, 1)
123+
124+
if len(results) != 1 {
125+
t.Errorf("Expected 1 result, got %d", len(results))
126+
}
127+
}
128+
129+
func BenchmarkLogProcessor(b *testing.B) {
130+
logs := make([]LogEntry, 100)
131+
for i := 0; i < 100; i++ {
132+
logs[i] = LogEntry{
133+
ID: i + 1,
134+
Timestamp: "2025-10-15T10:30:00Z",
135+
Level: "INFO",
136+
Message: "Test message",
137+
}
138+
}
139+
140+
b.ResetTimer()
141+
for i := 0; i < b.N; i++ {
142+
LogProcessor(logs, 10)
143+
}
144+
}
145+
146+
func BenchmarkLogProcessorWorkerComparison(b *testing.B) {
147+
logs := make([]LogEntry, 100)
148+
for i := 0; i < 100; i++ {
149+
logs[i] = LogEntry{
150+
ID: i + 1,
151+
Timestamp: "2025-10-15T10:30:00Z",
152+
Level: "INFO",
153+
Message: "Test message",
154+
}
155+
}
156+
157+
workerCounts := []int{1, 2, 5, 10, 20}
158+
159+
for _, numWorkers := range workerCounts {
160+
b.Run(fmt.Sprintf("Workers_%d", numWorkers), func(b *testing.B) {
161+
b.ResetTimer()
162+
for i := 0; i < b.N; i++ {
163+
LogProcessor(logs, numWorkers)
164+
}
165+
})
166+
}
167+
}

0 commit comments

Comments
 (0)