Skip to content

Commit f6a290c

Browse files
Add support for handling duplicate read messages (#28)
If different use case implementations trigger the same read messages, currently they are all sent through and result in multiple responses. This change will cache up to the last 20 read messages for each feature and not resend duplicates to the same remote feature. Instead it will return the message counter of the previous read request, so all requestors will get the same responses. Fixes #27
2 parents 354299c + 8116edb commit f6a290c

File tree

2 files changed

+155
-1
lines changed

2 files changed

+155
-1
lines changed

spine/feature_local.go

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package spine
22

33
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"encoding/json"
47
"errors"
58
"fmt"
69
"reflect"
10+
"sort"
711
"sync"
812
"time"
913

@@ -31,7 +35,10 @@ type FeatureLocal struct {
3135
bindings []*model.FeatureAddressType // bindings to remote features
3236
subscriptions []*model.FeatureAddressType // subscriptions to remote features
3337

34-
mux sync.Mutex
38+
readMsgCache map[model.MsgCounterType]string // cache for unanswered read messages, so we can filter duplicates and not send them
39+
40+
mux sync.Mutex
41+
muxReadCache sync.RWMutex
3542
}
3643

3744
func NewFeatureLocal(id uint, entity api.EntityLocalInterface, ftype model.FeatureTypeType, role model.RoleType) *FeatureLocal {
@@ -46,6 +53,7 @@ func NewFeatureLocal(id uint, entity api.EntityLocalInterface, ftype model.Featu
4653
writeApprovalReceived: make(map[model.MsgCounterType]int),
4754
pendingWriteApprovals: make(map[model.MsgCounterType]*time.Timer),
4855
writeTimeout: defaultMaxResponseDelay,
56+
readMsgCache: make(map[model.MsgCounterType]string),
4957
}
5058

5159
for _, fd := range CreateFunctionData[api.FunctionDataCmdInterface](ftype) {
@@ -58,6 +66,68 @@ func NewFeatureLocal(id uint, entity api.EntityLocalInterface, ftype model.Featu
5866

5967
var _ api.FeatureLocalInterface = (*FeatureLocal)(nil)
6068

69+
/* Read Msg Cache */
70+
71+
func (r *FeatureLocal) hashForMessage(destinationAddress *model.FeatureAddressType, cmd model.CmdType) string {
72+
cmdString, err := json.Marshal(cmd)
73+
if err != nil {
74+
return ""
75+
}
76+
77+
sig := fmt.Sprintf("%s-%s", destinationAddress.String(), cmdString)
78+
shaBytes := sha256.Sum256([]byte(sig))
79+
return hex.EncodeToString(shaBytes[:])
80+
}
81+
82+
func (r *FeatureLocal) msgCounterForHashFromCache(hash string) *model.MsgCounterType {
83+
r.muxReadCache.RLock()
84+
defer r.muxReadCache.RUnlock()
85+
86+
for msgCounter, h := range r.readMsgCache {
87+
if h == hash {
88+
return &msgCounter
89+
}
90+
}
91+
92+
return nil
93+
}
94+
95+
func (r *FeatureLocal) hasMsgCounterInCache(msgCounter model.MsgCounterType) bool {
96+
r.muxReadCache.RLock()
97+
defer r.muxReadCache.RUnlock()
98+
99+
_, ok := r.readMsgCache[msgCounter]
100+
101+
return ok
102+
}
103+
104+
func (r *FeatureLocal) addMsgCounterHashToCache(msgCounter model.MsgCounterType, hash string) {
105+
r.muxReadCache.Lock()
106+
defer r.muxReadCache.Unlock()
107+
108+
// cleanup cache, keep only the last 20 messages
109+
if len(r.readMsgCache) > 20 {
110+
keys := make([]uint64, 0, len(r.readMsgCache))
111+
for k := range r.readMsgCache {
112+
keys = append(keys, uint64(k))
113+
}
114+
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
115+
116+
// oldest key is the one with the lowest msgCounterValue
117+
oldestKey := keys[0]
118+
delete(r.readMsgCache, model.MsgCounterType(oldestKey))
119+
}
120+
121+
r.readMsgCache[msgCounter] = hash
122+
}
123+
124+
func (r *FeatureLocal) removeMsgCounterFromCache(msgCounter model.MsgCounterType) {
125+
r.muxReadCache.Lock()
126+
defer r.muxReadCache.Unlock()
127+
128+
delete(r.readMsgCache, msgCounter)
129+
}
130+
61131
/* FeatureLocalInterface */
62132

63133
func (r *FeatureLocal) Device() api.DeviceLocalInterface {
@@ -336,8 +406,20 @@ func (r *FeatureLocal) RequestRemoteDataBySenderAddress(
336406
deviceSki string,
337407
destinationAddress *model.FeatureAddressType,
338408
maxDelay time.Duration) (*model.MsgCounterType, *model.ErrorType) {
409+
// check if there is an unanswered read message for this destination and cmd and return that msgCounter
410+
hash := r.hashForMessage(destinationAddress, cmd)
411+
if len(hash) > 0 {
412+
if msgCounterCache := r.msgCounterForHashFromCache(hash); msgCounterCache != nil {
413+
return msgCounterCache, nil
414+
}
415+
}
416+
339417
msgCounter, err := sender.Request(model.CmdClassifierTypeRead, r.Address(), destinationAddress, false, []model.CmdType{cmd})
340418
if err == nil {
419+
if len(hash) > 0 {
420+
r.addMsgCounterHashToCache(*msgCounter, hash)
421+
}
422+
341423
return msgCounter, nil
342424
}
343425

@@ -513,6 +595,12 @@ func (r *FeatureLocal) HandleMessage(message *api.Message) *model.ErrorType {
513595
return model.NewErrorType(model.ErrorNumberTypeCommandNotSupported, "No function found for cmd data")
514596
}
515597

598+
if message.RequestHeader != nil &&
599+
message.RequestHeader.MsgCounterReference != nil &&
600+
r.hasMsgCounterInCache(*message.RequestHeader.MsgCounterReference) {
601+
r.removeMsgCounterFromCache(*message.RequestHeader.MsgCounterReference)
602+
}
603+
516604
switch message.CmdClassifier {
517605
case model.CmdClassifierTypeResult:
518606
if err := r.processResult(message); err != nil {

spine/feature_local_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,72 @@ func (s *LocalFeatureTestSuite) BeforeTest(suiteName, testName string) {
4848
s.remoteSubFeature, _ = createRemoteEntityAndFeature(remoteDevice, 2, s.subFeatureType, s.serverWriteFunction)
4949
}
5050

51+
func (s *LocalFeatureTestSuite) TestDeviceClassification_Cache() {
52+
expMsgCounter1 := model.MsgCounterType(1)
53+
54+
s.senderMock.EXPECT().Request(
55+
model.CmdClassifierTypeRead,
56+
s.localFeature.Address(),
57+
s.remoteServerFeature.Address(),
58+
false,
59+
mock.AnythingOfType("[]model.CmdType")).Return(&expMsgCounter1, nil).Once()
60+
61+
msgCounter, err := s.localFeature.RequestRemoteDataBySenderAddress(model.CmdType{}, s.senderMock, "dummy", s.remoteServerFeature.Address(), 0)
62+
assert.Nil(s.T(), err)
63+
assert.NotNil(s.T(), msgCounter)
64+
65+
msgCounter2, err := s.localFeature.RequestRemoteDataBySenderAddress(model.CmdType{}, s.senderMock, "dummy", s.remoteServerFeature.Address(), 0)
66+
assert.Nil(s.T(), err)
67+
assert.NotNil(s.T(), msgCounter2)
68+
assert.Equal(s.T(), *msgCounter, *msgCounter2)
69+
70+
replyMsg := api.Message{
71+
Cmd: model.CmdType{
72+
DeviceClassificationManufacturerData: &model.DeviceClassificationManufacturerDataType{},
73+
},
74+
CmdClassifier: model.CmdClassifierTypeReply,
75+
RequestHeader: &model.HeaderType{
76+
MsgCounter: util.Ptr(model.MsgCounterType(1)),
77+
MsgCounterReference: msgCounter,
78+
},
79+
FeatureRemote: s.remoteFeature,
80+
}
81+
s.localFeature.HandleMessage(&replyMsg)
82+
83+
expMsgCounter3 := model.MsgCounterType(3)
84+
s.senderMock.EXPECT().Request(
85+
model.CmdClassifierTypeRead,
86+
s.localFeature.Address(),
87+
s.remoteServerFeature.Address(),
88+
false,
89+
mock.AnythingOfType("[]model.CmdType")).Return(&expMsgCounter3, nil).Once()
90+
91+
msgCounter3, err := s.localFeature.RequestRemoteDataBySenderAddress(model.CmdType{}, s.senderMock, "dummy", s.remoteServerFeature.Address(), 0)
92+
assert.Nil(s.T(), err)
93+
assert.NotNil(s.T(), msgCounter3)
94+
assert.NotEqual(s.T(), *msgCounter, *msgCounter3)
95+
96+
for i := 0; i < 50; i++ {
97+
expMsgCounter4 := model.MsgCounterType(i + 3)
98+
remoteAddress := &model.FeatureAddressType{
99+
Device: s.remoteServerFeature.Device().Address(),
100+
Entity: []model.AddressEntityType{1},
101+
Feature: util.Ptr(model.AddressFeatureType(i)),
102+
}
103+
s.senderMock.EXPECT().Request(
104+
model.CmdClassifierTypeRead,
105+
s.localFeature.Address(),
106+
mock.Anything,
107+
false,
108+
mock.AnythingOfType("[]model.CmdType")).Return(&expMsgCounter4, nil).Once()
109+
110+
msgCounter4, err := s.localFeature.RequestRemoteDataBySenderAddress(model.CmdType{}, s.senderMock, "dummy", remoteAddress, 0)
111+
assert.Nil(s.T(), err)
112+
assert.NotNil(s.T(), expMsgCounter4)
113+
assert.NotEqual(s.T(), *msgCounter, *msgCounter4)
114+
}
115+
}
116+
51117
func (s *LocalFeatureTestSuite) TestDeviceClassification_Functions() {
52118
fcts := s.localServerFeatureWrite.Functions()
53119
assert.NotNil(s.T(), fcts)

0 commit comments

Comments
 (0)