Skip to content

Commit 5982971

Browse files
committed
Add non blocking channel operation exercise.
1 parent 057d954 commit 5982971

4 files changed

Lines changed: 184 additions & 0 deletions

File tree

internal/exercises/catalog.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ concepts:
241241
test_regex: ".*"
242242
hints:
243243
- Use `for i := range ch {...}` syntax to range over a channels.
244+
- slug: 46_non_blocking_channel_operations
245+
title: Non-blocking channel operations
246+
test_regex: ".*"
247+
hints:
248+
- Use `default` case in select to handle non-blocking channel operations.
249+
- Safely increment `dropped` using mutex `Lock()` and `Unlock()`.
244250

245251
projects:
246252
- slug: 101_text_analyzer
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package non_blocking_channel_operations
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
// Assume case where you have high throughput system
9+
// where you have to drop logs
10+
// if they cannot be processed in time
11+
// in order to keep system throughput high.
12+
13+
type Logger struct {
14+
ch chan string
15+
wg sync.WaitGroup
16+
processed int
17+
dropped int
18+
19+
mu sync.Mutex
20+
}
21+
22+
func NewLogger(bufferSize int) *Logger {
23+
l := &Logger{
24+
ch: make(chan string, bufferSize),
25+
}
26+
l.wg.Add(1)
27+
go l.run()
28+
return l
29+
}
30+
31+
var processingDelay = 200 * time.Microsecond
32+
33+
// slow log processor
34+
func (l *Logger) run() {
35+
defer l.wg.Done()
36+
for msg := range l.ch {
37+
_ = msg
38+
time.Sleep(processingDelay)
39+
40+
l.mu.Lock()
41+
l.processed++
42+
l.mu.Unlock()
43+
}
44+
}
45+
46+
func (l *Logger) Log(msg string) {
47+
select {
48+
case l.ch <- msg: // queued
49+
default:
50+
l.mu.Lock()
51+
l.dropped++
52+
l.mu.Unlock()
53+
}
54+
}
55+
56+
// graceful shutdown
57+
func (l *Logger) Close() {
58+
close(l.ch)
59+
l.wg.Wait()
60+
}
61+
62+
func (l *Logger) Stats() (processed, dropped int) {
63+
l.mu.Lock()
64+
defer l.mu.Unlock()
65+
return l.processed, l.dropped
66+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package non_blocking_channel_operations
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
// Assume case where you have high throughput system
9+
// where you have to drop logs
10+
// if they cannot be processed in time,
11+
// in order to keep system throughput high.
12+
13+
type Logger struct {
14+
ch chan string
15+
wg sync.WaitGroup
16+
processed int
17+
dropped int
18+
19+
mu sync.Mutex
20+
}
21+
22+
func NewLogger(bufferSize int) *Logger {
23+
l := &Logger{
24+
ch: make(chan string, bufferSize),
25+
}
26+
l.wg.Add(1)
27+
go l.run()
28+
return l
29+
}
30+
31+
var processingDelay = 200 * time.Microsecond
32+
33+
// slow log processor
34+
func (l *Logger) run() {
35+
defer l.wg.Done()
36+
for msg := range l.ch {
37+
_ = msg
38+
time.Sleep(processingDelay)
39+
40+
l.mu.Lock()
41+
l.processed++
42+
l.mu.Unlock()
43+
}
44+
}
45+
46+
// TODO: Implement logger function in a way
47+
// 1. incoming messages are queued to the channel
48+
// 2. if queue is full, increment dropped
49+
func (l *Logger) Log(msg string) {
50+
51+
}
52+
53+
// graceful shutdown
54+
func (l *Logger) Close() {
55+
close(l.ch)
56+
l.wg.Wait()
57+
}
58+
59+
func (l *Logger) Stats() (processed, dropped int) {
60+
l.mu.Lock()
61+
defer l.mu.Unlock()
62+
return l.processed, l.dropped
63+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package non_blocking_channel_operations
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestNonBlockingChannelOperations(t *testing.T) {
9+
10+
t.Run("Logs are processed without dropping", func(t *testing.T) {
11+
12+
processingDelay = 20 * time.Microsecond
13+
14+
logger := NewLogger(10)
15+
16+
for range 10 {
17+
logger.Log("msg")
18+
}
19+
logger.Close()
20+
21+
processed, dropped := logger.Stats()
22+
23+
if dropped != 0 {
24+
t.Fatalf("Expected no dropped logs, got %d", dropped)
25+
}
26+
if processed != 10 {
27+
t.Fatalf("Expected 10 processed logs, got %d", processed)
28+
}
29+
})
30+
31+
t.Run("Logs are not dropped", func(t *testing.T) {
32+
logger := NewLogger(5)
33+
34+
for range 10 {
35+
logger.Log("msg")
36+
}
37+
38+
logger.Close()
39+
40+
processed, dropped := logger.Stats()
41+
42+
if dropped != 5 {
43+
t.Fatalf("Expected no dropped logs, got %d", dropped)
44+
}
45+
if processed != 5 {
46+
t.Fatalf("Expected 10 processed logs, got %d", processed)
47+
}
48+
})
49+
}

0 commit comments

Comments
 (0)