Skip to content

Commit dde5090

Browse files
Michael Gaschdougm
Michael Gasch
authored andcommitted
fix: Ignore concurrent deletes in GetCategories
GetCategories() would fail if a category was deleted between the initial ListCategories() and GetCategory() details loop which is not atomic. Closes: #2710 Signed-off-by: Michael Gasch <[email protected]>
1 parent fc1fce6 commit dde5090

File tree

2 files changed

+99
-7
lines changed

2 files changed

+99
-7
lines changed

vapi/tags/categories.go

+12-7
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ import (
2020
"context"
2121
"fmt"
2222
"net/http"
23+
"strings"
2324

2425
"github.com/vmware/govmomi/vapi/internal"
2526
)
2627

27-
// Category provides methods to create, read, update, delete, and enumerate categories.
28+
// Category provides methods to create, read, update, delete, and enumerate
29+
// categories.
2830
type Category struct {
2931
ID string `json:"id,omitempty"`
3032
Name string `json:"name,omitempty"`
@@ -92,7 +94,8 @@ func (c *Manager) CreateCategory(ctx context.Context, category *Category) (strin
9294
return res, c.Do(ctx, url.Request(http.MethodPost, spec), &res)
9395
}
9496

95-
// UpdateCategory can update one or more of the AssociableTypes, Cardinality, Description and Name fields.
97+
// UpdateCategory updates one or more of the AssociableTypes, Cardinality,
98+
// Description and Name fields.
9699
func (c *Manager) UpdateCategory(ctx context.Context, category *Category) error {
97100
spec := struct {
98101
Category Category `json:"update_spec"`
@@ -108,7 +111,7 @@ func (c *Manager) UpdateCategory(ctx context.Context, category *Category) error
108111
return c.Do(ctx, url.Request(http.MethodPatch, spec), nil)
109112
}
110113

111-
// DeleteCategory deletes an existing category.
114+
// DeleteCategory deletes a category.
112115
func (c *Manager) DeleteCategory(ctx context.Context, category *Category) error {
113116
url := c.Resource(internal.CategoryPath).WithID(category.ID)
114117
return c.Do(ctx, url.Request(http.MethodDelete), nil)
@@ -141,7 +144,7 @@ func (c *Manager) ListCategories(ctx context.Context) ([]string, error) {
141144
return res, c.Do(ctx, url.Request(http.MethodGet), &res)
142145
}
143146

144-
// GetCategories fetches an array of category information in the system.
147+
// GetCategories fetches a list of category information in the system.
145148
func (c *Manager) GetCategories(ctx context.Context) ([]Category, error) {
146149
ids, err := c.ListCategories(ctx)
147150
if err != nil {
@@ -152,11 +155,13 @@ func (c *Manager) GetCategories(ctx context.Context) ([]Category, error) {
152155
for _, id := range ids {
153156
category, err := c.GetCategory(ctx, id)
154157
if err != nil {
155-
return nil, fmt.Errorf("get category %s: %s", id, err)
158+
if strings.Contains(err.Error(), http.StatusText(http.StatusNotFound)) {
159+
continue // deleted since last fetch
160+
}
161+
return nil, fmt.Errorf("get category %s: %v", id, err)
156162
}
157-
158163
categories = append(categories, *category)
159-
160164
}
165+
161166
return categories, nil
162167
}

vapi/tags/categories_test.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package tags
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
"testing"
9+
10+
"github.com/vmware/govmomi/simulator"
11+
"github.com/vmware/govmomi/vapi/rest"
12+
"github.com/vmware/govmomi/vim25"
13+
)
14+
15+
func TestManager_GetCategories(t *testing.T) {
16+
simulator.Run(func(ctx context.Context, client *vim25.Client) error {
17+
rc := rest.NewClient(client)
18+
19+
tr := testRoundTripper{
20+
t: t,
21+
transport: rc.Client.Client.Transport,
22+
}
23+
rc.Client.Client.Transport = &tr
24+
25+
err := rc.Login(ctx, simulator.DefaultLogin)
26+
if err != nil {
27+
t.Fatalf("VAPI login: %v", err)
28+
}
29+
30+
tm := NewManager(rc)
31+
32+
// categories to createCount and (concurrently) delete while retrieving categories
33+
const (
34+
createCount = 5
35+
deleteCount = 2
36+
)
37+
38+
var created []string
39+
for i := 1; i <= createCount; i++ {
40+
cat, err := tm.CreateCategory(ctx, &Category{
41+
Name: fmt.Sprintf("testcat-%d", i),
42+
})
43+
if err != nil {
44+
t.Fatalf("createCount category: %v", err)
45+
}
46+
created = append(created, cat)
47+
}
48+
49+
for i := 0; i < deleteCount; i++ {
50+
tr.deleted = append(tr.deleted, created[i])
51+
}
52+
53+
got, err := tm.GetCategories(ctx)
54+
if err != nil {
55+
t.Fatalf("get categories: %v", err)
56+
}
57+
58+
if len(got) != createCount-deleteCount {
59+
t.Errorf("category count mismatch: got=%d, want=%d", len(got), createCount-deleteCount)
60+
}
61+
62+
return nil
63+
})
64+
}
65+
66+
// testRoundTripper returns 404 for all categories in deleted
67+
type testRoundTripper struct {
68+
t *testing.T
69+
70+
deleted []string
71+
transport http.RoundTripper
72+
}
73+
74+
func (tr *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
75+
for _, id := range tr.deleted {
76+
if strings.Contains(req.URL.Path, id) {
77+
return &http.Response{
78+
StatusCode: http.StatusNotFound,
79+
Status: http.StatusText(http.StatusNotFound),
80+
Body: nil,
81+
Request: req.Clone(context.Background()),
82+
}, nil
83+
}
84+
}
85+
86+
return tr.transport.RoundTrip(req)
87+
}

0 commit comments

Comments
 (0)