Skip to content

Commit fa6a57d

Browse files
authored
Use copy on write strategy for device metadata (#489)
* Use copy on write strategy for metadata * let client code sync writers to avoid stale data * add unit tests * No need to deep copy metadata values on write * Make default values more consistent with map data * update context method * Abandon constructor approach * consistent use of named return vars in short funcs * Benchmark example usage of metadata in parallel * update changelog * add new release
1 parent 64a391c commit fa6a57d

File tree

9 files changed

+235
-187
lines changed

9 files changed

+235
-187
lines changed

CHANGELOG.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8-
- fixed `ConsulWatch` in xresolver by storing and watching the correct part of the url [#490](https://github.com/xmidt-org/webpa-common/pull/490)
9-
- fixed consul service discovery to pass QueryOptions [#490](https://github.com/xmidt-org/webpa-common/pull/490)
8+
9+
## [v1.10.2]
10+
### Fixed
11+
- Fixed `ConsulWatch` in xresolver by storing and watching the correct part of the url. [#490](https://github.com/xmidt-org/webpa-common/pull/490)
12+
- Fixed consul service discovery to pass QueryOptions. [#490](https://github.com/xmidt-org/webpa-common/pull/490)
13+
14+
### Changed
15+
- Device metadata implementation is now thread-safe and optimized for reads. [#489](https://github.com/xmidt-org/webpa-common/pull/489)
16+
1017

1118
## [v1.10.1]
1219
### Fixed
13-
- Device metadata didn't return a read-only view of its map claims resulting in data races [#483](https://github.com/xmidt-org/webpa-common/pull/483)
20+
- Device metadata didn't return a read-only view of its map claims resulting in data races. [#483](https://github.com/xmidt-org/webpa-common/pull/483)
1421

1522

1623
## [v1.10.0]
@@ -99,7 +106,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99106
- The first official release. We will be better about documenting changes
100107
moving forward.
101108

102-
[Unreleased]: https://github.com/xmidt-org/webpa-common/compare/v1.10.1...HEAD
109+
[Unreleased]: https://github.com/xmidt-org/webpa-common/compare/v1.10.2...HEAD
110+
[v1.10.2]: https://github.com/xmidt-org/webpa-common/compare/v1.10.1...v1.10.2
103111
[v1.10.1]: https://github.com/xmidt-org/webpa-common/compare/v1.10.0...v1.10.1
104112
[v1.10.0]: https://github.com/xmidt-org/webpa-common/compare/v1.9.0...v1.10.0
105113
[v1.9.0]: https://github.com/xmidt-org/webpa-common/compare/v1.8.1...v1.9.0

device/context.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ func WithIDRequest(id ID, original *http.Request) *http.Request {
3131
}
3232

3333
// WithDeviceMetadata returns a new context with the given metadata as a value.
34-
func WithDeviceMetadata(parent context.Context, metadata Metadata) context.Context {
34+
func WithDeviceMetadata(parent context.Context, metadata *Metadata) context.Context {
3535
return context.WithValue(parent, metadataKey, metadata)
3636
}
3737

3838
// GetDeviceMetadata returns the device metadata from the context if any.
39-
func GetDeviceMetadata(ctx context.Context) (metadata Metadata, ok bool) {
40-
metadata, ok = ctx.Value(metadataKey).(Metadata)
39+
func GetDeviceMetadata(ctx context.Context) (metadata *Metadata, ok bool) {
40+
metadata, ok = ctx.Value(metadataKey).(*Metadata)
4141
return
4242
}

device/device.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type Interface interface {
9191

9292
// Metadata returns a key value store object for information that's useful to guide interactions
9393
// with a device such as security credentials.
94-
Metadata() Metadata
94+
Metadata() *Metadata
9595

9696
// CloseReason returns the metadata explaining why a device was closed. If this device
9797
// is not closed, this method's return is undefined.
@@ -119,7 +119,7 @@ type device struct {
119119
compliance convey.Compliance
120120
conveyClosure conveymetric.Closure
121121

122-
metadata Metadata
122+
metadata *Metadata
123123

124124
closeReason atomic.Value
125125
}
@@ -131,7 +131,7 @@ type deviceOptions struct {
131131
QueueSize int
132132
ConnectedAt time.Time
133133
Logger log.Logger
134-
Metadata Metadata
134+
Metadata *Metadata
135135
}
136136

137137
// newDevice is an internal factory function for devices
@@ -309,7 +309,7 @@ func (d *device) ConveyCompliance() convey.Compliance {
309309
return d.compliance
310310
}
311311

312-
func (d *device) Metadata() Metadata {
312+
func (d *device) Metadata() *Metadata {
313313
return d.metadata
314314
}
315315

device/device_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestDevice(t *testing.T) {
5353
QueueSize: record.expectedQueueSize,
5454
ConnectedAt: expectedConnectedAt,
5555
Logger: logging.NewTestLogger(nil, t),
56-
Metadata: NewDeviceMetadata(),
56+
Metadata: new(Metadata),
5757
})
5858
)
5959

device/manager.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,8 @@ func (m *manager) Connect(response http.ResponseWriter, request *http.Request, r
149149
}
150150

151151
metadata, ok := GetDeviceMetadata(ctx)
152-
153152
if !ok {
154-
metadata = NewDeviceMetadata()
153+
metadata = new(Metadata)
155154
}
156155

157156
cvy, cvyErr := m.conveyTranslator.FromHeader(request.Header)
@@ -164,7 +163,7 @@ func (m *manager) Connect(response http.ResponseWriter, request *http.Request, r
164163
Logger: m.logger,
165164
})
166165

167-
if len(metadata.JWTClaims()) < 1 {
166+
if len(metadata.Claims()) < 1 {
168167
d.errorLog.Log(logging.MessageKey(), "missing security information")
169168
}
170169

@@ -311,7 +310,7 @@ func (m *manager) readPump(d *device, r ReadCloser, closeOnce *sync.Once) {
311310
}
312311

313312
deviceMetadata := event.Device.Metadata()
314-
message.PartnerIDs = []string{deviceMetadata.JWTClaims().PartnerID()}
313+
message.PartnerIDs = []string{deviceMetadata.PartnerIDClaim()}
315314

316315
if message.Type == wrp.SimpleEventMessageType {
317316
message.SessionID = deviceMetadata.SessionID()

device/metadata.go

Lines changed: 69 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package device
22

33
import (
4-
"encoding/json"
4+
"sync"
5+
"sync/atomic"
56

67
"github.com/segmentio/ksuid"
78
"github.com/spf13/cast"
@@ -28,107 +29,101 @@ func init() {
2829
}
2930

3031
// Metadata contains information such as security credentials
31-
// related to a device.
32-
type Metadata map[string]interface{}
33-
34-
// JWTClaims returns the JWT claims attached to a device. If no claims exist,
35-
// they are initialized appropiately.
36-
func (m Metadata) JWTClaims() JWTClaims { // returns the type and such type has getter/setter
37-
if jwtClaims, ok := m[JWTClaimsKey].(JWTClaims); ok {
38-
return deepCopyMap(jwtClaims)
39-
}
40-
return deepCopyMap(m.initJWTClaims())
41-
}
42-
43-
// SetJWTClaims sets the JWT claims attached to a device.
44-
func (m Metadata) SetJWTClaims(jwtClaims JWTClaims) {
45-
m[JWTClaimsKey] = jwtClaims
32+
// related to a device. Read operations are optimized with a
33+
// copy-on-write strategy. Client code must further synchronize concurrent
34+
// writers to avoid stale data.
35+
// Metadata uses an atomic.Value internally and thus it should not be copied
36+
// after creation.
37+
type Metadata struct {
38+
v atomic.Value
39+
once sync.Once
4640
}
4741

4842
// SessionID returns the UUID associated with a device's current connection
49-
// to the cluster.
50-
func (m Metadata) SessionID() string {
51-
if sessionID, ok := m[SessionIDKey].(string); ok {
52-
return sessionID
53-
}
54-
55-
return m.initSessionID()
56-
}
57-
58-
func (m Metadata) initSessionID() string {
59-
sessionID := ksuid.New().String()
60-
m[SessionIDKey] = sessionID
61-
return sessionID
43+
// to the cluster if one has been set. The zero value is returned as default.
44+
func (m *Metadata) SessionID() (sessionID string) {
45+
sessionID, _ = m.loadData()[SessionIDKey].(string)
46+
return
6247
}
6348

64-
func (m Metadata) initJWTClaims() JWTClaims {
65-
jwtClaims := JWTClaims(make(map[string]interface{}))
66-
m.SetJWTClaims(jwtClaims)
67-
return jwtClaims
49+
// SetSessionID sets the UUID associated the device's current connection to the cluster.
50+
// It uses sync.Once to ensure the sessionID is unchanged through the metadata's lifecycle.
51+
func (m *Metadata) SetSessionID(sessionID string) {
52+
m.once.Do(func() {
53+
m.copyAndStore(SessionIDKey, sessionID)
54+
})
6855
}
6956

70-
// Load allows retrieving values from a device's metadata
71-
func (m Metadata) Load(key string) interface{} {
72-
return m[key]
57+
// Load returns the value associated with the given key in the metadata map.
58+
// It is not recommended modifying values returned by reference.
59+
func (m *Metadata) Load(key string) interface{} {
60+
return m.loadData()[key]
7361
}
7462

75-
// Store allows writing values into the device's metadata given
76-
// a key. Boolean results indicates whether the operation was successful.
77-
// Note: operations will fail for reserved keys.
78-
func (m Metadata) Store(key string, value interface{}) bool {
63+
// Store updates the key value mapping in the device metadata map.
64+
// A boolean result is given indicating whether the operation was successful.
65+
// Operations will fail for reserved keys.
66+
// To avoid updating keys with stale data/value, client code will need to
67+
// synchronize the entire transaction of reading, copying, modifying and
68+
// writing back the value.
69+
func (m *Metadata) Store(key string, value interface{}) bool {
7970
if reservedMetadataKeys[key] {
8071
return false
8172
}
82-
m[key] = value
73+
m.copyAndStore(key, value)
8374
return true
8475
}
8576

86-
// NewDeviceMetadata returns a metadata object ready for use.
87-
func NewDeviceMetadata() Metadata {
88-
return NewDeviceMetadataWithClaims(make(map[string]interface{}))
77+
// SetClaims updates the claims associated with the device that's
78+
// owner of the metadata.
79+
// To avoid updating the claims with stale data, client code will need to
80+
// synchronize the entire transaction of reading, copying, modifying and
81+
// writing back the value.
82+
func (m *Metadata) SetClaims(claims map[string]interface{}) {
83+
m.copyAndStore(JWTClaimsKey, deepCopyMap(claims))
8984
}
9085

91-
// NewDeviceMetadataWithClaims returns a metadata object ready for use with the
92-
// given claims.
93-
func NewDeviceMetadataWithClaims(claims map[string]interface{}) Metadata {
94-
m := make(Metadata)
95-
m.SetJWTClaims(deepCopyMap(claims))
96-
m.initSessionID()
97-
return m
86+
// Claims returns the claims attached to a device. The returned map
87+
// should not be modified to avoid any race conditions. To update the claims,
88+
// take a look at the ClaimsCopy() function
89+
func (m *Metadata) Claims() (claims map[string]interface{}) {
90+
claims, _ = m.loadData()[JWTClaimsKey].(map[string]interface{})
91+
return
9892
}
9993

100-
// JWTClaims defines the interface of a device's security claims.
101-
// One current use case is providing security credentials the device
102-
// presented at registration time.
103-
type JWTClaims map[string]interface{}
94+
// ClaimsCopy returns a deep copy of the claims. Use this, along with the
95+
// SetClaims() method to update the claims.
96+
func (m *Metadata) ClaimsCopy() map[string]interface{} {
97+
return deepCopyMap(m.Claims())
98+
}
10499

105-
// Trust returns the device's trust level claim
100+
// TrustClaim returns the device's trust level claim.
106101
// By Default, a device is untrusted (trust = 0).
107-
func (c JWTClaims) Trust() int {
108-
if trust, ok := c[TrustClaimKey].(int); ok {
109-
return trust
110-
}
111-
return 0
102+
func (m *Metadata) TrustClaim() (trust int) {
103+
trust, _ = m.Claims()[TrustClaimKey].(int)
104+
return
112105
}
113106

114-
// PartnerID returns the partner ID claim.
107+
// PartnerIDClaim returns the partner ID claim.
115108
// If no claim is found, the zero value is returned.
116-
func (c JWTClaims) PartnerID() string {
117-
if partnerID, ok := c[PartnerIDClaimKey].(string); ok {
118-
return partnerID
119-
}
120-
return "" // no partner by default
109+
func (m *Metadata) PartnerIDClaim() (partnerID string) {
110+
partnerID, _ = m.Claims()[PartnerIDClaimKey].(string)
111+
return
112+
}
113+
114+
func (m *Metadata) loadData() (data map[string]interface{}) {
115+
data, _ = m.v.Load().(map[string]interface{})
116+
return
121117
}
122118

123-
// SetTrust modifies the trust level of the device which owns these
124-
// claims.
125-
func (c JWTClaims) SetTrust(trust int) {
126-
c[TrustClaimKey] = trust
119+
func (m *Metadata) storeData(data map[string]interface{}) {
120+
m.v.Store(data)
127121
}
128122

129-
// MarshalJSON allows easy JSON representation of the JWTClaims underlying claims map.
130-
func (c JWTClaims) MarshalJSON() ([]byte, error) {
131-
return json.Marshal(c)
123+
func (m *Metadata) copyAndStore(key string, val interface{}) {
124+
data := deepCopyMap(m.loadData())
125+
data[key] = val
126+
m.storeData(data)
132127
}
133128

134129
func deepCopyMap(m map[string]interface{}) map[string]interface{} {

0 commit comments

Comments
 (0)