Skip to content

Commit a709fb7

Browse files
rusabdRuslan AbdulkhalikovCopilotalexottnkvuong
authored
[Internal] Caching of group membership in databricks_group_member (#4581)
## Changes On the installations with large number of users the provider creates a large number of API calls to the Databricks API leading to slow deployments. The provider also requests all members for each group membership check. In this change group member resource use expiring caches - in our case we halved the default deployment times on existing installation and eliminated Databricks API throttling. I am also looking for some guidance on how to make this change more generic. ## Tests We run this provider in production with about 800 users and ~100 groups. - [x] `make test` run locally - [ ] relevant change in `docs/` folder - [ ] covered with integration tests in `internal/acceptance` - [x] using Go SDK - [x] using TF Plugin Framework --------- Co-authored-by: Ruslan Abdulkhalikov <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Alex Ott <[email protected]> Co-authored-by: vuong-nguyen <[email protected]> Co-authored-by: Alex Ott <[email protected]> Co-authored-by: Miles Yucht <[email protected]>
1 parent 12f27e0 commit a709fb7

File tree

4 files changed

+160
-11
lines changed

4 files changed

+160
-11
lines changed

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313
### Exporter
1414

1515
### Internal Changes
16+
17+
* Caching group membership in `databricks_group_member` to improve performance ([#4581](https://github.com/databricks/terraform-provider-databricks/pull/4581)).

scim/groups.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,9 @@ func (a GroupsAPI) Create(scimGroupRequest Group) (group Group, err error) {
3131

3232
// Read reads and returns a Group object via SCIM api
3333
func (a GroupsAPI) Read(groupID, attributes string) (group Group, err error) {
34-
err = a.client.Scim(a.context, http.MethodGet, fmt.Sprintf(
35-
"/preview/scim/v2/Groups/%v?attributes=%s", groupID, attributes), nil, &group)
36-
if err != nil {
37-
return
38-
}
34+
key := fmt.Sprintf(
35+
"/preview/scim/v2/Groups/%v?attributes=%s", groupID, attributes)
36+
err = a.client.Scim(a.context, http.MethodGet, key, nil, &group)
3937
return
4038
}
4139

scim/resource_group_member.go

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,128 @@ package scim
33
import (
44
"context"
55
"fmt"
6+
"sync"
67

78
"github.com/databricks/databricks-sdk-go/apierr"
89
"github.com/databricks/terraform-provider-databricks/common"
10+
"github.com/hashicorp/terraform-plugin-log/tflog"
911
)
1012

13+
// groupMembersInfo is the set of members that belong to a group. Read and write access
14+
// to the underlying members map must be protected by the lock.
15+
type groupMembersInfo struct {
16+
initialized bool
17+
members map[string]struct{}
18+
lock sync.Mutex
19+
}
20+
21+
// groupCache is a cache of groups to their members by their ID. Access to the cache
22+
// must be protected by the lock.
23+
type groupCache struct {
24+
// The mapping of cached members for this group. The cache key is the group ID.
25+
// TODO: add workspace ID to the cache key when account-level and workspace-level providers are unified.
26+
cache map[string]*groupMembersInfo
27+
lock sync.Mutex
28+
}
29+
30+
func newGroupCache() *groupCache {
31+
return &groupCache{
32+
cache: make(map[string]*groupMembersInfo),
33+
lock: sync.Mutex{},
34+
}
35+
}
36+
37+
func (gc *groupCache) getOrCreateGroupInfo(groupID string) *groupMembersInfo {
38+
gc.lock.Lock()
39+
defer gc.lock.Unlock()
40+
41+
groupInfo, exists := gc.cache[groupID]
42+
if !exists {
43+
groupInfo = &groupMembersInfo{
44+
initialized: false,
45+
members: make(map[string]struct{}),
46+
lock: sync.Mutex{},
47+
}
48+
gc.cache[groupID] = groupInfo
49+
}
50+
return groupInfo
51+
}
52+
53+
func (gc *groupCache) getMembers(api GroupsAPI, groupID string) (map[string]struct{}, error) {
54+
groupInfo := gc.getOrCreateGroupInfo(groupID)
55+
groupInfo.lock.Lock()
56+
defer groupInfo.lock.Unlock()
57+
58+
if !groupInfo.initialized {
59+
tflog.Debug(api.context, fmt.Sprintf("Getting members for group %s", groupID))
60+
group, err := api.Read(groupID, "members")
61+
if err != nil {
62+
return nil, err
63+
}
64+
tflog.Debug(api.context, fmt.Sprintf("Group %s has %d members", groupID, len(group.Members)))
65+
for _, member := range group.Members {
66+
groupInfo.members[member.Value] = struct{}{}
67+
}
68+
groupInfo.initialized = true
69+
tflog.Debug(api.context, fmt.Sprintf("Group %s has %d members (initialized)", groupID, len(groupInfo.members)))
70+
} else {
71+
tflog.Debug(api.context, fmt.Sprintf("Group %s has %d members (cached)", groupID, len(groupInfo.members)))
72+
}
73+
membersCopy := make(map[string]struct{}, len(groupInfo.members))
74+
for k, v := range groupInfo.members {
75+
membersCopy[k] = v
76+
}
77+
return membersCopy, nil
78+
}
79+
80+
func (gc *groupCache) removeMember(api GroupsAPI, groupID string, memberID string) error {
81+
groupInfo := gc.getOrCreateGroupInfo(groupID)
82+
groupInfo.lock.Lock()
83+
defer groupInfo.lock.Unlock()
84+
85+
err := api.Patch(groupID, PatchRequest(
86+
"remove", fmt.Sprintf(`members[value eq "%s"]`, memberID)))
87+
if err != nil {
88+
return err
89+
}
90+
91+
if groupInfo.initialized {
92+
delete(groupInfo.members, memberID)
93+
}
94+
return nil
95+
}
96+
97+
func (gc *groupCache) addMember(api GroupsAPI, groupID string, memberID string) error {
98+
groupInfo := gc.getOrCreateGroupInfo(groupID)
99+
groupInfo.lock.Lock()
100+
defer groupInfo.lock.Unlock()
101+
102+
err := api.Patch(groupID, PatchRequestWithValue("add", "members", memberID))
103+
if err != nil {
104+
return err
105+
}
106+
if groupInfo.initialized {
107+
groupInfo.members[memberID] = struct{}{}
108+
}
109+
return nil
110+
}
111+
112+
func hasMember(members map[string]struct{}, memberID string) bool {
113+
_, ok := members[memberID]
114+
return ok
115+
}
116+
117+
var globalGroupsCache = newGroupCache()
118+
11119
// ResourceGroupMember bind group with member
12120
func ResourceGroupMember() common.Resource {
13121
return common.NewPairID("group_id", "member_id").BindResource(common.BindResource{
14122
CreateContext: func(ctx context.Context, groupID, memberID string, c *common.DatabricksClient) error {
15-
return NewGroupsAPI(ctx, c).Patch(groupID, PatchRequestWithValue("add", "members", memberID))
123+
return globalGroupsCache.addMember(NewGroupsAPI(ctx, c), groupID, memberID)
16124
},
17125
ReadContext: func(ctx context.Context, groupID, memberID string, c *common.DatabricksClient) error {
18-
group, err := NewGroupsAPI(ctx, c).Read(groupID, "members")
19-
hasMember := ComplexValues(group.Members).HasValue(memberID)
20-
if err == nil && !hasMember {
126+
members, err := globalGroupsCache.getMembers(NewGroupsAPI(ctx, c), groupID)
127+
if err == nil && !hasMember(members, memberID) {
21128
return &apierr.APIError{
22129
ErrorCode: "NOT_FOUND",
23130
StatusCode: 404,
@@ -27,8 +134,7 @@ func ResourceGroupMember() common.Resource {
27134
return err
28135
},
29136
DeleteContext: func(ctx context.Context, groupID, memberID string, c *common.DatabricksClient) error {
30-
return NewGroupsAPI(ctx, c).Patch(groupID, PatchRequest(
31-
"remove", fmt.Sprintf(`members[value eq "%s"]`, memberID)))
137+
return globalGroupsCache.removeMember(NewGroupsAPI(ctx, c), groupID, memberID)
32138
},
33139
})
34140
}

scim/resource_group_member_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
)
1010

1111
func TestResourceGroupMemberCreate(t *testing.T) {
12+
globalGroupsCache = newGroupCache()
13+
1214
d, err := qa.ResourceFixture{
1315
Fixtures: []qa.HTTPFixture{
1416
{
@@ -43,6 +45,15 @@ func TestResourceGroupMemberCreate(t *testing.T) {
4345
}.Apply(t)
4446
assert.NoError(t, err)
4547
assert.Equal(t, "abc|bcd", d.Id())
48+
49+
assert.Equal(t, 1, len(globalGroupsCache.cache), "Cache should contain one entry after create")
50+
groupInfo, exists := globalGroupsCache.cache["abc"]
51+
assert.True(t, exists, "Cache should contain group 'abc'")
52+
53+
assert.True(t, groupInfo.initialized, "Group info should be initialized")
54+
assert.Equal(t, 1, len(groupInfo.members), "Should have one member cached")
55+
_, memberExists := groupInfo.members["bcd"]
56+
assert.True(t, memberExists, "Member 'bcd' should be in cache")
4657
}
4758

4859
func TestResourceGroupMemberCreate_Error(t *testing.T) {
@@ -70,6 +81,8 @@ func TestResourceGroupMemberCreate_Error(t *testing.T) {
7081
}
7182

7283
func TestResourceGroupMemberRead(t *testing.T) {
84+
globalGroupsCache = newGroupCache()
85+
7386
d, err := qa.ResourceFixture{
7487
Fixtures: []qa.HTTPFixture{
7588
{
@@ -93,6 +106,15 @@ func TestResourceGroupMemberRead(t *testing.T) {
93106
}.Apply(t)
94107
assert.NoError(t, err)
95108
assert.Equal(t, "abc|bcd", d.Id(), "Id should not be empty")
109+
110+
assert.Equal(t, 1, len(globalGroupsCache.cache), "Cache should contain one entry after read")
111+
groupInfo, exists := globalGroupsCache.cache["abc"]
112+
assert.True(t, exists, "Cache should contain group 'abc'")
113+
114+
assert.True(t, groupInfo.initialized, "Group info should be initialized")
115+
assert.Equal(t, 1, len(groupInfo.members), "Should have one member cached")
116+
_, memberExists := groupInfo.members["bcd"]
117+
assert.True(t, memberExists, "Member 'bcd' should be in cache")
96118
}
97119

98120
func TestResourceGroupMemberRead_NoMember(t *testing.T) {
@@ -157,6 +179,20 @@ func TestResourceGroupMemberRead_Error(t *testing.T) {
157179
}
158180

159181
func TestResourceGroupMemberDelete(t *testing.T) {
182+
globalGroupsCache = newGroupCache()
183+
184+
groupInfo := globalGroupsCache.getOrCreateGroupInfo("abc")
185+
groupInfo.initialized = true
186+
groupInfo.members["bcd"] = struct{}{}
187+
188+
assert.Equal(t, 1, len(globalGroupsCache.cache), "Cache should contain one entry initially")
189+
groupInfo, exists := globalGroupsCache.cache["abc"]
190+
assert.True(t, exists, "Cache should contain group 'abc'")
191+
192+
assert.Equal(t, 1, len(groupInfo.members), "Should have one member initially")
193+
_, memberExists := groupInfo.members["bcd"]
194+
assert.True(t, memberExists, "Member 'bcd' should be in cache initially")
195+
160196
d, err := qa.ResourceFixture{
161197
Fixtures: []qa.HTTPFixture{
162198
{
@@ -173,6 +209,13 @@ func TestResourceGroupMemberDelete(t *testing.T) {
173209
}.Apply(t)
174210
assert.NoError(t, err)
175211
assert.Equal(t, "abc|bcd", d.Id())
212+
213+
groupInfo, exists = globalGroupsCache.cache["abc"]
214+
assert.True(t, exists, "Cache should still contain group 'abc'")
215+
216+
assert.Equal(t, 0, len(groupInfo.members), "Should have no members after delete")
217+
_, memberExists = groupInfo.members["bcd"]
218+
assert.False(t, memberExists, "Member 'bcd' should not be in cache after delete")
176219
}
177220

178221
func TestResourceGroupMemberDelete_Error(t *testing.T) {

0 commit comments

Comments
 (0)