Skip to content

Commit 707abd4

Browse files
committed
fix: address copilot review comments
- Guard GetRange/GetRangeStale against quantity=0 (false cache hit) - Use shared coilOn/coilOff slices in decomposeResponse to reduce per-coil allocations - Extract cleanupOnce so tests exercise the real keepStale guard instead of manually simulating cleanup
1 parent f5344ec commit 707abd4

File tree

3 files changed

+38
-23
lines changed

3 files changed

+38
-23
lines changed

internal/cache/cache.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ func (c *Cache) Delete(key string) {
132132
// GetRange retrieves all values for a contiguous register range.
133133
// Returns the per-register/coil values and true only if ALL are cached and fresh.
134134
func (c *Cache) GetRange(slaveID byte, functionCode byte, startAddr uint16, quantity uint16) ([][]byte, bool) {
135+
if quantity == 0 {
136+
return nil, false
137+
}
138+
135139
c.mu.RLock()
136140
defer c.mu.RUnlock()
137141

@@ -152,6 +156,10 @@ func (c *Cache) GetRange(slaveID byte, functionCode byte, startAddr uint16, quan
152156
// GetRangeStale retrieves all values for a contiguous register range, ignoring TTL.
153157
// Returns the per-register/coil values and true only if ALL are present (even if expired).
154158
func (c *Cache) GetRangeStale(slaveID byte, functionCode byte, startAddr uint16, quantity uint16) ([][]byte, bool) {
159+
if quantity == 0 {
160+
return nil, false
161+
}
162+
155163
c.mu.RLock()
156164
defer c.mu.RUnlock()
157165

@@ -250,6 +258,21 @@ func (c *Cache) Coalesce(ctx context.Context, key string, fetch func(context.Con
250258
return result, nil
251259
}
252260

261+
// cleanupOnce runs a single cleanup pass, removing expired entries.
262+
// Skips deletion when keepStale is true.
263+
func (c *Cache) cleanupOnce() {
264+
if c.keepStale {
265+
return
266+
}
267+
c.mu.Lock()
268+
for key, entry := range c.entries {
269+
if entry.IsExpired() {
270+
delete(c.entries, key)
271+
}
272+
}
273+
c.mu.Unlock()
274+
}
275+
253276
// cleanup periodically removes expired entries.
254277
func (c *Cache) cleanup() {
255278
ticker := time.NewTicker(time.Minute)
@@ -260,16 +283,7 @@ func (c *Cache) cleanup() {
260283
case <-c.done:
261284
return
262285
case <-ticker.C:
263-
if c.keepStale {
264-
continue
265-
}
266-
c.mu.Lock()
267-
for key, entry := range c.entries {
268-
if entry.IsExpired() {
269-
delete(c.entries, key)
270-
}
271-
}
272-
c.mu.Unlock()
286+
c.cleanupOnce()
273287
}
274288
}
275289
}

internal/cache/cache_test.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -374,29 +374,24 @@ func TestCache_KeepStale(t *testing.T) {
374374
c.Set("key1", []byte("value1"))
375375
time.Sleep(100 * time.Millisecond)
376376

377-
// Simulate cleanup
378-
c.mu.Lock()
379-
for key, entry := range c.entries {
380-
if entry.IsExpired() {
381-
delete(c.entries, key)
382-
}
383-
}
384-
c.mu.Unlock()
377+
c.cleanupOnce()
385378

386379
if _, ok := c.GetStale("key1"); ok {
387380
t.Error("expected stale data to be gone after cleanup with keepStale=false")
388381
}
389382
c.Close()
390383

391-
// With keepStale=true, expired entries survive cleanup
384+
// With keepStale=true, cleanup skips deletion
392385
c2 := New(50*time.Millisecond, true)
393386
c2.Set("key1", []byte("value1"))
394387
time.Sleep(100 * time.Millisecond)
395388

396-
// Entry should still be accessible via GetStale
389+
c2.cleanupOnce()
390+
391+
// Entry should still be accessible via GetStale after cleanup
397392
data, ok := c2.GetStale("key1")
398393
if !ok {
399-
t.Error("expected stale data to survive with keepStale=true")
394+
t.Error("expected stale data to survive cleanup with keepStale=true")
400395
}
401396
if string(data) != "value1" {
402397
t.Errorf("expected value1, got %s", string(data))

internal/proxy/proxy.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,12 @@ func (p *Proxy) invalidateCache(req *modbus.Request) {
202202
}
203203
}
204204

205+
// Shared byte slices for coil values — safe to reuse since SetRange copies.
206+
var (
207+
coilOn = []byte{1}
208+
coilOff = []byte{0}
209+
)
210+
205211
// decomposeResponse extracts per-register/coil values from a Modbus read response.
206212
// Response format: [funcCode, byteCount, data...]
207213
// For registers (FC 0x03, 0x04): each register is 2 bytes.
@@ -236,9 +242,9 @@ func decomposeResponse(functionCode byte, quantity uint16, data []byte) [][]byte
236242
return nil
237243
}
238244
if payload[byteIdx]&(1<<bitIdx) != 0 {
239-
values[i] = []byte{1}
245+
values[i] = coilOn
240246
} else {
241-
values[i] = []byte{0}
247+
values[i] = coilOff
242248
}
243249
}
244250
return values

0 commit comments

Comments
 (0)