Skip to content

Commit 62f9d9c

Browse files
authored
refactor: Use sync for browser feature availabilities (#1905)
* refactor: Use sync for browser feature availabilities This commit refactors the code to use a new sync method for updating browser feature availabilities. The new method, SyncBrowserFeatureAvailabilities, allows for batch updates, which improves performance by reducing the number of database calls. And also helps in the cases when a feature's availability regresses for a browser. The following changes were made: - Replaced UpsertBrowserFeatureAvailability with SyncBrowserFeatureAvailabilities in all the test files. - Updated the main logic to use the new sync method. - Updated the data loading scripts to use the new sync method. * simplify
1 parent 4502e76 commit 62f9d9c

12 files changed

Lines changed: 375 additions & 638 deletions

lib/gcpspanner/browser_availabilities.go

Lines changed: 86 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,87 +17,122 @@ package gcpspanner
1717
import (
1818
"context"
1919
"fmt"
20+
"log/slog"
2021

2122
"cloud.google.com/go/spanner"
2223
)
2324

2425
const browserFeatureAvailabilitiesTable = "BrowserFeatureAvailabilities"
2526

26-
// Implements the entityMapper interface for BrowserFeatureAvailability and SpannerBrowserFeatureAvailability.
27-
type browserFeatureAvailabilityMapper struct{}
27+
// spannerBrowserFeatureAvailability is a wrapper for the browser availability
28+
// information for a feature stored in spanner.
29+
type spannerBrowserFeatureAvailability struct {
30+
WebFeatureID string `spanner:"WebFeatureID"`
31+
BrowserName string `spanner:"BrowserName"`
32+
BrowserVersion string `spanner:"BrowserVersion"`
33+
}
2834

29-
func (m browserFeatureAvailabilityMapper) Table() string {
30-
return browserFeatureAvailabilitiesTable
35+
// BrowserFeatureAvailability contains availability information for a particular
36+
// feature in a browser.
37+
type BrowserFeatureAvailability struct {
38+
BrowserName string
39+
BrowserVersion string
3140
}
3241

33-
type browserFeatureAvailabilityKey struct {
34-
WebFeatureID string
35-
BrowserName string
42+
// Implements the syncableEntityMapper interface for BrowserFeatureAvailability and spannerBrowserFeatureAvailability.
43+
type browserFeatureAvailabilitySpannerMapper struct{}
44+
45+
// PreDeleteHook is a no-op for browser feature availabilities.
46+
func (m browserFeatureAvailabilitySpannerMapper) PreDeleteHook(
47+
_ context.Context,
48+
_ *Client,
49+
_ []spannerBrowserFeatureAvailability,
50+
) ([]ExtraMutationsGroup, error) {
51+
return nil, nil
3652
}
3753

38-
func (m browserFeatureAvailabilityMapper) SelectOne(key browserFeatureAvailabilityKey) spanner.Statement {
39-
stmt := spanner.NewStatement(fmt.Sprintf(`
54+
// GetChildDeleteKeyMutations returns nil as there are no child delete mutations for browser feature availabilities.
55+
func (m browserFeatureAvailabilitySpannerMapper) GetChildDeleteKeyMutations(
56+
_ context.Context,
57+
_ *Client,
58+
_ []spannerBrowserFeatureAvailability,
59+
) ([]ExtraMutationsGroup, error) {
60+
return nil, nil
61+
}
62+
63+
func (m browserFeatureAvailabilitySpannerMapper) Table() string {
64+
return browserFeatureAvailabilitiesTable
65+
}
66+
67+
func (m browserFeatureAvailabilitySpannerMapper) SelectAll() spanner.Statement {
68+
return spanner.NewStatement(fmt.Sprintf(`
4069
SELECT
4170
WebFeatureID, BrowserName, BrowserVersion
42-
FROM %s
43-
WHERE WebFeatureID = @webFeatureID AND BrowserName = @browserName
44-
LIMIT 1`, m.Table()))
45-
parameters := map[string]interface{}{
46-
"webFeatureID": key.WebFeatureID,
47-
"browserName": key.BrowserName,
48-
}
49-
stmt.Params = parameters
50-
51-
return stmt
71+
FROM %s`, m.Table()))
5272
}
5373

54-
func (m browserFeatureAvailabilityMapper) GetKeyFromExternal(
55-
in spannerBrowserFeatureAvailability) browserFeatureAvailabilityKey {
56-
return browserFeatureAvailabilityKey{
57-
WebFeatureID: in.WebFeatureID,
58-
BrowserName: in.BrowserName,
59-
}
74+
func (m browserFeatureAvailabilitySpannerMapper) GetKeyFromExternal(in spannerBrowserFeatureAvailability) string {
75+
return fmt.Sprintf("%s-%s", in.WebFeatureID, in.BrowserName)
6076
}
6177

62-
func (m browserFeatureAvailabilityMapper) DeleteKey(key browserFeatureAvailabilityKey) spanner.Key {
63-
return spanner.Key{key.WebFeatureID, key.BrowserName}
78+
func (m browserFeatureAvailabilitySpannerMapper) GetKeyFromInternal(in spannerBrowserFeatureAvailability) string {
79+
return fmt.Sprintf("%s-%s", in.WebFeatureID, in.BrowserName)
6480
}
6581

66-
// spannerBrowserFeatureAvailability is a wrapper for the browser availability
67-
// information for a feature stored in spanner.
68-
type spannerBrowserFeatureAvailability struct {
69-
WebFeatureID string
70-
BrowserFeatureAvailability
82+
func (m browserFeatureAvailabilitySpannerMapper) MergeAndCheckChanged(
83+
in spannerBrowserFeatureAvailability, existing spannerBrowserFeatureAvailability) (
84+
spannerBrowserFeatureAvailability, bool) {
85+
merged := spannerBrowserFeatureAvailability{
86+
WebFeatureID: existing.WebFeatureID,
87+
BrowserName: existing.BrowserName,
88+
BrowserVersion: in.BrowserVersion,
89+
}
90+
hasChanged := merged.BrowserVersion != existing.BrowserVersion
91+
92+
return merged, hasChanged
7193
}
7294

73-
// BrowserFeatureAvailability contains availability information for a particular
74-
// feature in a browser.
75-
type BrowserFeatureAvailability struct {
76-
BrowserName string
77-
BrowserVersion string
95+
func (m browserFeatureAvailabilitySpannerMapper) DeleteMutation(
96+
in spannerBrowserFeatureAvailability) *spanner.Mutation {
97+
return spanner.Delete(browserFeatureAvailabilitiesTable, spanner.Key{in.WebFeatureID, in.BrowserName})
7898
}
7999

80-
// UpsertBrowserFeatureAvailability will upsert the given browser feature availability.
81-
func (c *Client) UpsertBrowserFeatureAvailability(
100+
// SyncBrowserFeatureAvailabilities reconciles the BrowserFeatureAvailabilities table with the provided
101+
// list of availabilities.
102+
func (c *Client) SyncBrowserFeatureAvailabilities(
82103
ctx context.Context,
83-
webFeatureID string,
84-
input BrowserFeatureAvailability) error {
85-
id, err := c.GetIDFromFeatureKey(ctx, NewFeatureKeyFilter(webFeatureID))
104+
availabilities map[string][]BrowserFeatureAvailability,
105+
) error {
106+
featureIDandKeys, err := c.FetchAllWebFeatureIDsAndKeys(ctx)
86107
if err != nil {
87108
return err
88109
}
89-
if id == nil {
90-
return ErrInternalQueryFailure
110+
111+
featureKeyToID := make(map[string]string, len(featureIDandKeys))
112+
for _, item := range featureIDandKeys {
113+
featureKeyToID[item.FeatureKey] = item.ID
91114
}
92-
featureAvailability := spannerBrowserFeatureAvailability{
93-
WebFeatureID: *id,
94-
BrowserFeatureAvailability: input,
115+
116+
var spannerAvailabilities []spannerBrowserFeatureAvailability
117+
for featureKey, featureAvailabilities := range availabilities {
118+
featureID, ok := featureKeyToID[featureKey]
119+
if !ok {
120+
slog.WarnContext(ctx, "unable to find feature id for feature key", "featureKey", featureKey)
121+
122+
continue
123+
}
124+
for _, availability := range featureAvailabilities {
125+
spannerAvailabilities = append(spannerAvailabilities, spannerBrowserFeatureAvailability{
126+
WebFeatureID: featureID,
127+
BrowserName: availability.BrowserName,
128+
BrowserVersion: availability.BrowserVersion,
129+
})
130+
}
95131
}
96132

97-
return newUniqueEntityWriter[
98-
browserFeatureAvailabilityMapper,
99-
spannerBrowserFeatureAvailability,
100-
spannerBrowserFeatureAvailability](c).upsertUniqueKey(ctx, featureAvailability)
133+
synchronizer := newEntitySynchronizer[browserFeatureAvailabilitySpannerMapper](c)
134+
135+
return synchronizer.Sync(ctx, spannerAvailabilities)
101136
}
102137

103138
func (c *Client) fetchAllBrowserAvailabilitiesWithTransaction(

lib/gcpspanner/browser_availabilities_test.go

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,49 +17,35 @@ package gcpspanner
1717
import (
1818
"context"
1919
"errors"
20+
"maps"
2021
"slices"
2122
"testing"
2223

2324
"cloud.google.com/go/spanner"
2425
"google.golang.org/api/iterator"
2526
)
2627

27-
func getSampleBrowserAvailabilities() []struct {
28-
FeatureKey string
29-
BrowserFeatureAvailability
30-
} {
31-
return []struct {
32-
FeatureKey string
33-
BrowserFeatureAvailability
34-
}{
35-
{
36-
BrowserFeatureAvailability: BrowserFeatureAvailability{
28+
func getSampleBrowserAvailabilities() map[string][]BrowserFeatureAvailability {
29+
return map[string][]BrowserFeatureAvailability{
30+
"feature1": {
31+
{
3732
BrowserName: "fooBrowser",
3833
BrowserVersion: "0.0.0",
3934
},
40-
FeatureKey: "feature1",
41-
},
42-
{
43-
BrowserFeatureAvailability: BrowserFeatureAvailability{
35+
{
4436
BrowserName: "barBrowser",
4537
BrowserVersion: "0.0.0",
4638
},
47-
FeatureKey: "feature1",
4839
},
49-
{
50-
BrowserFeatureAvailability: BrowserFeatureAvailability{
40+
"feature2": {
41+
{
5142
BrowserName: "barBrowser",
5243
BrowserVersion: "2.0.0",
5344
},
54-
FeatureKey: "feature2",
55-
},
56-
{
57-
BrowserFeatureAvailability: BrowserFeatureAvailability{
45+
{
5846
BrowserName: "fooBrowser",
5947
BrowserVersion: "1.0.0",
6048
},
61-
62-
FeatureKey: "feature2",
6349
},
6450
}
6551
}
@@ -86,7 +72,7 @@ func setupRequiredTablesForBrowserFeatureAvailability(
8672
// Helper method to get all the Availabilities in a stable order.
8773
func (c *Client) ReadAllAvailabilities(ctx context.Context, _ *testing.T) ([]BrowserFeatureAvailability, error) {
8874
stmt := spanner.NewStatement(
89-
"SELECT * FROM BrowserFeatureAvailabilities ORDER BY BrowserVersion ASC, BrowserName ASC")
75+
"SELECT BrowserName, BrowserVersion FROM BrowserFeatureAvailabilities ORDER BY BrowserVersion ASC, BrowserName ASC")
9076
iter := c.Single().Query(ctx, stmt)
9177
defer iter.Stop()
9278

@@ -99,27 +85,25 @@ func (c *Client) ReadAllAvailabilities(ctx context.Context, _ *testing.T) ([]Bro
9985
if err != nil {
10086
return nil, errors.Join(ErrInternalQueryFailure, err)
10187
}
102-
var availability spannerBrowserFeatureAvailability
88+
var availability BrowserFeatureAvailability
10389
if err := row.ToStruct(&availability); err != nil {
10490
return nil, errors.Join(ErrInternalQueryFailure, err)
10591
}
106-
ret = append(ret, availability.BrowserFeatureAvailability)
92+
ret = append(ret, availability)
10793
}
10894

10995
return ret, nil
11096
}
11197

112-
func TestUpsertBrowserFeatureAvailability(t *testing.T) {
98+
func TestSyncBrowserFeatureAvailabilities(t *testing.T) {
11399
restartDatabaseContainer(t)
114100
ctx := context.Background()
115101
setupRequiredTablesForBrowserFeatureAvailability(ctx, spannerClient, t)
116102
sampleAvailabilities := getSampleBrowserAvailabilities()
117-
for _, availability := range sampleAvailabilities {
118-
err := spannerClient.UpsertBrowserFeatureAvailability(
119-
ctx, availability.FeatureKey, availability.BrowserFeatureAvailability)
120-
if err != nil {
121-
t.Errorf("unexpected error during insert. %s", err.Error())
122-
}
103+
err := spannerClient.SyncBrowserFeatureAvailabilities(
104+
ctx, sampleAvailabilities)
105+
if err != nil {
106+
t.Errorf("unexpected error during insert. %s", err.Error())
123107
}
124108

125109
expectedPage := []BrowserFeatureAvailability{
@@ -151,10 +135,19 @@ func TestUpsertBrowserFeatureAvailability(t *testing.T) {
151135
}
152136

153137
// Update the availability info for feature1 on barBrowser to a later version
154-
err = spannerClient.UpsertBrowserFeatureAvailability(ctx, "feature1", BrowserFeatureAvailability{
155-
BrowserName: "barBrowser",
156-
BrowserVersion: "1.0.0",
157-
})
138+
updatedAvailabilities := maps.Clone(sampleAvailabilities)
139+
updatedAvailabilities["feature1"] = []BrowserFeatureAvailability{
140+
{
141+
BrowserName: "fooBrowser",
142+
BrowserVersion: "0.0.0",
143+
},
144+
{
145+
BrowserName: "barBrowser",
146+
BrowserVersion: "1.0.0",
147+
},
148+
}
149+
150+
err = spannerClient.SyncBrowserFeatureAvailabilities(ctx, updatedAvailabilities)
158151
if err != nil {
159152
t.Errorf("unexpected error during update. %s", err.Error())
160153
}
@@ -186,4 +179,31 @@ func TestUpsertBrowserFeatureAvailability(t *testing.T) {
186179
if !slices.Equal(expectedPage, availabilities) {
187180
t.Errorf("unequal availabilities.\nexpected %+v\nreceived %+v", expectedPage, availabilities)
188181
}
182+
183+
// Remove the availability info for feature1 on barBrowser
184+
regressedAvailabilities := maps.Clone(updatedAvailabilities)
185+
delete(regressedAvailabilities, "feature1")
186+
187+
err = spannerClient.SyncBrowserFeatureAvailabilities(ctx, regressedAvailabilities)
188+
if err != nil {
189+
t.Errorf("unexpected error during update. %s", err.Error())
190+
}
191+
192+
expectedPage = []BrowserFeatureAvailability{
193+
{
194+
BrowserName: "fooBrowser",
195+
BrowserVersion: "1.0.0",
196+
},
197+
{
198+
BrowserName: "barBrowser",
199+
BrowserVersion: "2.0.0",
200+
},
201+
}
202+
availabilities, err = spannerClient.ReadAllAvailabilities(ctx, t)
203+
if err != nil {
204+
t.Errorf("unexpected error during read all. %s", err.Error())
205+
}
206+
if !slices.Equal(expectedPage, availabilities) {
207+
t.Errorf("unequal availabilities.\nexpected %+v\nreceived %+v", expectedPage, availabilities)
208+
}
189209
}

0 commit comments

Comments
 (0)