Skip to content

Commit 29db783

Browse files
Merge pull request #18 from Vedd-Patel/evict-stale-vehicles
evict stale vehicles from tracker to prevent memory leak
2 parents 277bf26 + 4121069 commit 29db783

3 files changed

Lines changed: 104 additions & 1 deletion

File tree

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func main() {
3434
defer store.Close()
3535

3636
tracker := NewTracker(maxAge)
37+
defer tracker.Stop()
3738

3839
cutoff := time.Now().Add(-maxAge)
3940
recentLocations, err := store.GetRecentLocations(ctx, cutoff)

tracker.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,42 @@ type Tracker struct {
2222
mu sync.RWMutex
2323
vehicles map[string]*VehicleState
2424
maxAge time.Duration
25+
done chan struct{}
26+
stopOnce sync.Once
2527
}
2628

2729
// NewTracker creates a Tracker with the given staleness threshold.
2830
func NewTracker(maxAge time.Duration) *Tracker {
2931
if maxAge <= 0 {
3032
panic("maxAge must be positive")
3133
}
32-
return &Tracker{
34+
35+
t := &Tracker{
3336
vehicles: make(map[string]*VehicleState),
3437
maxAge: maxAge,
38+
done: make(chan struct{}),
3539
}
40+
41+
go func() {
42+
ticker := time.NewTicker(maxAge)
43+
defer ticker.Stop()
44+
for {
45+
select {
46+
case <-ticker.C:
47+
t.cleanup()
48+
case <-t.done:
49+
return
50+
}
51+
}
52+
}()
53+
54+
return t
55+
}
56+
57+
func (t *Tracker) Stop() {
58+
t.stopOnce.Do(func() {
59+
close(t.done)
60+
})
3661
}
3762

3863
// Update stores or replaces the latest position for a vehicle.
@@ -65,3 +90,15 @@ func (t *Tracker) ActiveVehicles() []*VehicleState {
6590
}
6691
return active
6792
}
93+
94+
// cleanup removes old entries from the tracker to prevent unbounded memory growth.
95+
func (t *Tracker) cleanup() {
96+
t.mu.Lock()
97+
defer t.mu.Unlock()
98+
cutoff := time.Now().Add(-t.maxAge)
99+
for id, v := range t.vehicles {
100+
if v.UpdatedAt.Before(cutoff) {
101+
delete(t.vehicles, id)
102+
}
103+
}
104+
}

tracker_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"fmt"
45
"sync"
56
"testing"
67
"time"
@@ -11,6 +12,7 @@ import (
1112

1213
func TestTracker_Update(t *testing.T) {
1314
tracker := NewTracker(5 * time.Minute)
15+
defer tracker.Stop()
1416

1517
loc := &LocationReport{
1618
VehicleID: "bus-1",
@@ -36,6 +38,7 @@ func TestTracker_Update(t *testing.T) {
3638

3739
func TestTracker_UpdateOverwrites(t *testing.T) {
3840
tracker := NewTracker(5 * time.Minute)
41+
defer tracker.Stop()
3942

4043
tracker.Update(&LocationReport{VehicleID: "bus-1", Latitude: 1.0, Longitude: 2.0, Timestamp: 1})
4144
tracker.Update(&LocationReport{VehicleID: "bus-1", Latitude: 3.0, Longitude: 4.0, Timestamp: 2})
@@ -48,6 +51,7 @@ func TestTracker_UpdateOverwrites(t *testing.T) {
4851

4952
func TestTracker_MultipleVehicles(t *testing.T) {
5053
tracker := NewTracker(5 * time.Minute)
54+
defer tracker.Stop()
5155

5256
tracker.Update(&LocationReport{VehicleID: "bus-1", Latitude: 1, Longitude: 2, Timestamp: 1})
5357
tracker.Update(&LocationReport{VehicleID: "bus-2", Latitude: 3, Longitude: 4, Timestamp: 2})
@@ -58,6 +62,7 @@ func TestTracker_MultipleVehicles(t *testing.T) {
5862

5963
func TestTracker_Staleness(t *testing.T) {
6064
tracker := NewTracker(1 * time.Millisecond)
65+
defer tracker.Stop()
6166

6267
tracker.Update(&LocationReport{VehicleID: "bus-1", Latitude: 1, Longitude: 2, Timestamp: 1})
6368
time.Sleep(5 * time.Millisecond)
@@ -68,6 +73,7 @@ func TestTracker_Staleness(t *testing.T) {
6873

6974
func TestTracker_Concurrent(t *testing.T) {
7075
tracker := NewTracker(5 * time.Minute)
76+
defer tracker.Stop()
7177
var wg sync.WaitGroup
7278

7379
for i := 0; i < 100; i++ {
@@ -92,6 +98,25 @@ func TestTracker_Concurrent(t *testing.T) {
9298
assert.Len(t, active, 1)
9399
}
94100

101+
func TestTracker_ConcurrentWithCleanup(t *testing.T) {
102+
tracker := NewTracker(5 * time.Millisecond)
103+
defer tracker.Stop()
104+
105+
var wg sync.WaitGroup
106+
for i := 0; i < 100; i++ {
107+
wg.Add(1)
108+
go func(id int) {
109+
defer wg.Done()
110+
tracker.Update(&LocationReport{
111+
VehicleID: fmt.Sprintf("bus-%d", id),
112+
Timestamp: 1,
113+
})
114+
tracker.ActiveVehicles()
115+
}(i)
116+
}
117+
wg.Wait()
118+
}
119+
95120
func TestNewTracker_PanicsOnInvalidMaxAge(t *testing.T) {
96121
assert.PanicsWithValue(t, "maxAge must be positive", func() {
97122
NewTracker(0)
@@ -101,3 +126,43 @@ func TestNewTracker_PanicsOnInvalidMaxAge(t *testing.T) {
101126
NewTracker(-1 * time.Second)
102127
}, "NewTracker with negative duration should panic")
103128
}
129+
130+
func TestTracker_Cleanup(t *testing.T) {
131+
132+
maxAge := 10 * time.Millisecond
133+
tracker := NewTracker(maxAge)
134+
defer tracker.Stop()
135+
136+
tracker.Update(&LocationReport{VehicleID: "bus-1", Timestamp: 1})
137+
138+
assert.Eventually(t, func() bool {
139+
tracker.mu.RLock()
140+
defer tracker.mu.RUnlock()
141+
return len(tracker.vehicles) == 0
142+
}, 100*time.Millisecond, 5*time.Millisecond, "stale vehicle should be deleted from internal map")
143+
}
144+
145+
func TestTracker_StopIsIdempotent(t *testing.T) {
146+
tracker := NewTracker(5 * time.Minute)
147+
tracker.Stop()
148+
assert.NotPanics(t, func() { tracker.Stop() })
149+
}
150+
151+
func TestTracker_CleanupPreservesFreshVehicles(t *testing.T) {
152+
maxAge := 50 * time.Millisecond
153+
tracker := NewTracker(maxAge)
154+
defer tracker.Stop()
155+
156+
tracker.Update(&LocationReport{VehicleID: "old-bus", Timestamp: 1})
157+
time.Sleep(60 * time.Millisecond)
158+
tracker.Update(&LocationReport{VehicleID: "new-bus", Timestamp: 2})
159+
160+
assert.Eventually(t, func() bool {
161+
tracker.mu.RLock()
162+
defer tracker.mu.RUnlock()
163+
_, hasOld := tracker.vehicles["old-bus"]
164+
_, hasNew := tracker.vehicles["new-bus"]
165+
return !hasOld && hasNew
166+
}, 200*time.Millisecond, 5*time.Millisecond,
167+
"cleanup should remove old-bus but keep new-bus")
168+
}

0 commit comments

Comments
 (0)