Skip to content

Commit 9c36aef

Browse files
clintonkreederc42
andauthored
Concurrent metrics update in cache
Co-authored-by: Chris Reeder <[email protected]>
1 parent 4d3b2b1 commit 9c36aef

18 files changed

+2651
-114
lines changed

core/common.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/netapp/trident/config"
12+
"github.com/netapp/trident/core/metrics"
1213
. "github.com/netapp/trident/logging"
1314
"github.com/netapp/trident/storage"
1415
"github.com/netapp/trident/utils/models"
@@ -28,7 +29,7 @@ func recordTiming(operation string, err *error) func() {
2829
if *err != nil {
2930
success = "false"
3031
}
31-
operationDurationInMsSummary.WithLabelValues(operation, success).Observe(endTimeMS)
32+
metrics.OperationDurationInMsSummary.WithLabelValues(operation, success).Observe(endTimeMS)
3233
}
3334
}
3435

@@ -47,7 +48,7 @@ func recordTransactionTiming(txn *storage.VolumeTransaction, err *error) {
4748
if *err != nil {
4849
success = "false"
4950
}
50-
operationDurationInMsSummary.WithLabelValues(operation, success).Observe(endTimeMS)
51+
metrics.OperationDurationInMsSummary.WithLabelValues(operation, success).Observe(endTimeMS)
5152
}
5253

5354
// getProtocol returns the appropriate protocol based on a specified volume mode, access mode and protocol, or

core/concurrent_cache/backend.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,25 @@ func UpsertBackend(id, name, newName string) Subquery {
102102
newKey: newName,
103103
setResults: func(s *Subquery, r *Result) error {
104104
backends.rlock()
105-
if i, ok := backends.data[s.id]; ok {
106-
r.Backend.Read = i.SmartCopy().(storage.Backend)
105+
oldBackendData, ok := backends.data[s.id]
106+
if ok {
107+
r.Backend.Read = oldBackendData.SmartCopy().(storage.Backend)
107108
}
108109
backends.runlock()
110+
109111
r.Backend.Upsert = func(b storage.Backend) {
110-
backends.lock()
111112
if s.id == "" {
112113
s.id = b.BackendUUID()
113114
}
115+
116+
// Update metrics
117+
if oldBackendData != nil {
118+
deleteBackendFromMetrics(oldBackendData.(storage.Backend))
119+
}
120+
addBackendToMetrics(b)
121+
122+
backends.lock()
123+
114124
// This is the canonical method for upserting with a unique key.
115125
// There are 4 cases: key and newKey are equal, key is empty, newKey is empty,
116126
// and key and newKey are different. In all cases expect #3,
@@ -131,6 +141,7 @@ func UpsertBackend(id, name, newName string) Subquery {
131141
delete(backends.key.data, s.key)
132142
backends.key.data[s.newKey] = s.id
133143
}
144+
134145
backends.data[s.id] = b
135146
backends.unlock()
136147
}
@@ -146,11 +157,18 @@ func DeleteBackend(id string) Subquery {
146157
id: id,
147158
setResults: func(s *Subquery, r *Result) error {
148159
backends.rlock()
149-
if i, ok := backends.data[s.id]; ok {
150-
r.Backend.Read = i.SmartCopy().(storage.Backend)
160+
backendData, ok := backends.data[s.id]
161+
if ok {
162+
r.Backend.Read = backendData.SmartCopy().(storage.Backend)
151163
}
152164
backends.runlock()
165+
153166
r.Backend.Delete = func() {
167+
// Update metrics
168+
if backendData != nil {
169+
deleteBackendFromMetrics(backendData.(storage.Backend))
170+
}
171+
154172
backends.lock()
155173
delete(backends.data, s.id)
156174
// delete from the uniqueKey cache as well
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package concurrent_cache
2+
3+
import (
4+
"testing"
5+
6+
"github.com/prometheus/client_golang/prometheus/testutil"
7+
"github.com/stretchr/testify/assert"
8+
"go.uber.org/mock/gomock"
9+
10+
"github.com/netapp/trident/core/metrics"
11+
"github.com/netapp/trident/storage"
12+
)
13+
14+
func TestUpsertBackend_Metrics(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
backendExists bool
18+
initialBackend storage.Backend
19+
upsertBackend storage.Backend
20+
}{
21+
{
22+
name: "insert new backend",
23+
backendExists: false,
24+
},
25+
{
26+
name: "update existing backend",
27+
backendExists: true,
28+
},
29+
}
30+
31+
for _, tt := range tests {
32+
t.Run(tt.name, func(t *testing.T) {
33+
mockCtrl := gomock.NewController(t)
34+
defer mockCtrl.Finish()
35+
36+
// Reset metrics before each test
37+
metrics.BackendsGauge.Reset()
38+
metrics.TridentBackendInfo.Reset()
39+
40+
// Set up initial state if backend exists
41+
if tt.backendExists {
42+
tt.initialBackend = getMockBackendWithMap(mockCtrl, map[string]string{
43+
"name": "existing-backend",
44+
"driverName": "test-driver",
45+
"state": string(storage.Online),
46+
"uuid": "test-backend-uuid",
47+
})
48+
backends.lock()
49+
backends.data["test-backend-uuid"] = tt.initialBackend
50+
backends.unlock()
51+
// Add the existing backend to metrics to simulate realistic state
52+
addBackendToMetrics(tt.initialBackend)
53+
}
54+
55+
// Get initial metric values
56+
initialBackendGauge := testutil.ToFloat64(metrics.BackendsGauge.WithLabelValues("test-driver", string(storage.Online)))
57+
initialTridentBackendInfo := testutil.ToFloat64(metrics.TridentBackendInfo.WithLabelValues("test-driver", "existing-backend", "test-backend-uuid"))
58+
59+
// Create upsert backend
60+
tt.upsertBackend = getMockBackendWithMap(mockCtrl, map[string]string{
61+
"name": "updated-backend",
62+
"driverName": "test-driver",
63+
"state": string(storage.Online),
64+
"uuid": "test-backend-uuid",
65+
})
66+
67+
// Execute upsert operation
68+
subquery := UpsertBackend("test-backend-uuid", "test-backend", "updated-backend")
69+
result := &Result{}
70+
err := subquery.setResults(&subquery, result)
71+
assert.NoError(t, err, "UpsertBackend setResults should not error")
72+
73+
// Verify the upsert function was created
74+
assert.NotNil(t, result.Backend.Upsert, "Upsert function should be created")
75+
76+
// Call the upsert function
77+
result.Backend.Upsert(tt.upsertBackend)
78+
79+
// Verify metrics were updated correctly
80+
afterUpsertBackendGauge := testutil.ToFloat64(metrics.BackendsGauge.WithLabelValues("test-driver", string(storage.Online)))
81+
afterUpsertTridentBackendInfo := testutil.ToFloat64(metrics.TridentBackendInfo.WithLabelValues("test-driver", "updated-backend", "test-backend-uuid"))
82+
83+
if tt.backendExists {
84+
// For existing backend: delete old (dec) + add new (inc) = no net change
85+
assert.Equal(t, initialBackendGauge, afterUpsertBackendGauge, "BackendGauge should remain unchanged when updating existing backend")
86+
// TridentBackendInfo should be set to 1 for the new backend name/labels
87+
assert.Equal(t, initialTridentBackendInfo, afterUpsertTridentBackendInfo, "TridentBackendInfo should be set to 1 for updated backend")
88+
89+
// Verify the old TridentBackendInfo metric is removed
90+
oldTridentBackendInfo := testutil.ToFloat64(metrics.TridentBackendInfo.WithLabelValues("test-driver", "existing-backend", "test-backend-uuid"))
91+
92+
assert.Equal(t, float64(0), oldTridentBackendInfo, "Old TridentBackendInfo should be removed")
93+
} else {
94+
// For new backend: only add (inc) = increment by 1
95+
assert.Equal(t, initialBackendGauge+1, afterUpsertBackendGauge, "BackendGauge should be incremented by 1 when adding new backend")
96+
assert.Equal(t, initialTridentBackendInfo+1, afterUpsertTridentBackendInfo, "TridentBackendInfo should be set to 1 for new backend")
97+
}
98+
99+
// Verify the backend was actually stored
100+
backends.rlock()
101+
storedBackend, exists := backends.data["test-backend-uuid"]
102+
backends.runlock()
103+
assert.True(t, exists, "Backend should exist in storage after upsert")
104+
assert.Equal(t, tt.upsertBackend, storedBackend, "Stored backend should match upserted backend")
105+
106+
// Clean up
107+
backends.lock()
108+
delete(backends.data, "test-backend-uuid")
109+
delete(backends.key.data, "test-backend")
110+
delete(backends.key.data, "updated-backend")
111+
backends.unlock()
112+
})
113+
}
114+
}
115+
116+
func TestDeleteBackend_Metrics(t *testing.T) {
117+
tests := []struct {
118+
name string
119+
backendExists bool
120+
backendToDelete storage.Backend
121+
}{
122+
{
123+
name: "delete existing backend",
124+
backendExists: true,
125+
},
126+
{
127+
name: "delete non-existing backend",
128+
backendExists: false,
129+
},
130+
}
131+
132+
for _, tt := range tests {
133+
t.Run(tt.name, func(t *testing.T) {
134+
mockCtrl := gomock.NewController(t)
135+
defer mockCtrl.Finish()
136+
137+
// Reset metrics before each test
138+
metrics.BackendsGauge.Reset()
139+
metrics.TridentBackendInfo.Reset()
140+
141+
// Set up initial state if backend exists
142+
if tt.backendExists {
143+
tt.backendToDelete = getMockBackendWithMap(mockCtrl, map[string]string{
144+
"name": "existing-backend",
145+
"driverName": "test-driver",
146+
"state": string(storage.Online),
147+
"uuid": "test-backend-uuid",
148+
})
149+
backends.lock()
150+
backends.data["test-backend-uuid"] = tt.backendToDelete
151+
backends.unlock()
152+
// Add the existing backend to metrics to simulate realistic state
153+
addBackendToMetrics(tt.backendToDelete)
154+
}
155+
156+
// Get initial metric value
157+
initialBackendGauge := testutil.ToFloat64(metrics.BackendsGauge.WithLabelValues("test-driver", string(storage.Online)))
158+
initialTridentBackendInfo := testutil.ToFloat64(metrics.TridentBackendInfo.WithLabelValues("test-driver", "existing-backend", "test-backend-uuid"))
159+
160+
// Execute delete operation
161+
subquery := DeleteBackend("test-backend-uuid")
162+
result := &Result{}
163+
err := subquery.setResults(&subquery, result)
164+
assert.NoError(t, err, "DeleteBackend setResults should not error")
165+
166+
if tt.backendExists {
167+
// Verify the delete function was created and the backend was read
168+
assert.NotNil(t, result.Backend.Delete, "Delete function should be created")
169+
assert.Equal(t, tt.backendToDelete, result.Backend.Read, "Read backend should match the backend that exists")
170+
171+
// Call the delete function
172+
result.Backend.Delete()
173+
174+
// Verify metrics were updated correctly (decremented by 1)
175+
afterDeleteBackendGauge := testutil.ToFloat64(metrics.BackendsGauge.WithLabelValues("test-driver", string(storage.Online)))
176+
afterDeleteTridentBackendInfo := testutil.ToFloat64(metrics.TridentBackendInfo.WithLabelValues("test-driver", "existing-backend", "test-backend-uuid"))
177+
178+
assert.Equal(t, initialBackendGauge-1, afterDeleteBackendGauge, "BackendGauge should be decremented by 1 when deleting existing backend")
179+
assert.Equal(t, initialTridentBackendInfo-1, afterDeleteTridentBackendInfo, "TridentBackendInfo should be set to 0 when deleting existing backend")
180+
181+
// Verify the backend was actually removed from storage
182+
backends.rlock()
183+
_, exists := backends.data["test-backend-uuid"]
184+
backends.runlock()
185+
assert.False(t, exists, "Backend should not exist in storage after delete")
186+
} else {
187+
// For non-existing backend, delete function should still be created but Read should be nil
188+
assert.NotNil(t, result.Backend.Delete, "Delete function should be created even for non-existing backend")
189+
assert.Nil(t, result.Backend.Read, "Read backend should be nil for non-existing backend")
190+
191+
// Call the delete function
192+
result.Backend.Delete()
193+
194+
// Verify metrics were NOT updated (no change since backend didn't exist)
195+
afterDeleteBackendGauge := testutil.ToFloat64(metrics.BackendsGauge.WithLabelValues("test-driver", string(storage.Online)))
196+
afterDeleteTridentBackendInfo := testutil.ToFloat64(metrics.TridentBackendInfo.WithLabelValues("test-driver", "existing-backend", "test-backend-uuid"))
197+
198+
assert.Equal(t, initialBackendGauge, afterDeleteBackendGauge, "BackendGauge should remain unchanged when deleting non-existing backend")
199+
assert.Equal(t, initialTridentBackendInfo, afterDeleteTridentBackendInfo, "TridentBackendInfo should remain unchanged when deleting non-existing backend")
200+
}
201+
})
202+
}
203+
}

core/concurrent_cache/concurrent_cache.go

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,30 @@ func assembleQueries(queries [][]Subquery, roots [][]int, cachesPresent map[reso
232232
}
233233

234234
func lockCachesAndFillInIDs(queries [][]Subquery, roots [][]int, cachesPresent map[resource]struct{}) error {
235-
sortedResources := make([]resource, 0, len(cachesPresent))
235+
resources := make([]resource, 0, len(cachesPresent))
236236
for r := range cachesPresent {
237-
sortedResources = append(sortedResources, r)
237+
resources = append(resources, r)
238238
}
239-
slices.SortFunc(sortedResources, func(i, j resource) int {
239+
unlocker, err := rLockCaches(resources)
240+
defer unlocker()
241+
if err != nil {
242+
return err
243+
}
244+
245+
for i := range queries {
246+
for _, r := range roots[i] {
247+
if err := fillInIDs(r, queries[i]); err != nil {
248+
return err
249+
}
250+
}
251+
}
252+
return nil
253+
}
254+
255+
// rLockCaches sorts and acquires read locks on all caches for the given resources. It returns a function that releases
256+
// the locks, and error if a cache does not exist.
257+
func rLockCaches(resources []resource) (func(), error) {
258+
slices.SortFunc(resources, func(i, j resource) int {
240259
switch {
241260
case resourceRanks[i] > resourceRanks[j]:
242261
return -1
@@ -253,24 +272,18 @@ func lockCachesAndFillInIDs(queries [][]Subquery, roots [][]int, cachesPresent m
253272

254273
return 0
255274
})
256-
257-
for _, r := range sortedResources {
275+
unlocks := make([]func(), 0, len(caches))
276+
for _, r := range resources {
258277
c, ok := caches[r]
259278
if !ok {
260-
return fmt.Errorf("no cache found for %s", r)
279+
return func() {}, fmt.Errorf("no cache found for resource %s", r)
261280
}
262281
c.rlock()
263-
defer c.runlock()
282+
unlocks = append(unlocks, c.runlock)
264283
}
265-
266-
for i := range queries {
267-
for _, r := range roots[i] {
268-
if err := fillInIDs(r, queries[i]); err != nil {
269-
return err
270-
}
271-
}
272-
}
273-
return nil
284+
return func() {
285+
doReverse(unlocks)
286+
}, nil
274287
}
275288

276289
func checkError(queries []Subquery) error {

0 commit comments

Comments
 (0)