Skip to content

Commit 21885c9

Browse files
authored
CBG-4735: Remove the _globalSync xattr with purge to avoid attachment metadata hanging around (#7667)
1 parent 2d2b42e commit 21885c9

File tree

4 files changed

+115
-3
lines changed

4 files changed

+115
-3
lines changed

base/util.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,3 +1814,14 @@ func SlicesEqualIgnoreOrder[T comparable](a, b []T) bool {
18141814
}
18151815
return true
18161816
}
1817+
1818+
// KeysPresent returns the subset of keys that are present in m, preserving input order of keys.
1819+
func KeysPresent[K comparable, V any](m map[K]V, keys []K) []K {
1820+
result := make([]K, 0, len(keys))
1821+
for _, k := range keys {
1822+
if _, ok := m[k]; ok {
1823+
result = append(result, k)
1824+
}
1825+
}
1826+
return result
1827+
}

base/util_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,3 +1807,50 @@ func TestSlicesEqualUnordered(t *testing.T) {
18071807
})
18081808
}
18091809
}
1810+
1811+
func TestKeysPresent(t *testing.T) {
1812+
tests := []struct {
1813+
name string
1814+
m map[string]int
1815+
keys []string
1816+
want []string
1817+
}{
1818+
{
1819+
name: "all present",
1820+
m: map[string]int{"a": 1, "b": 2, "c": 3},
1821+
keys: []string{"a", "b"},
1822+
want: []string{"a", "b"},
1823+
},
1824+
{
1825+
name: "some present",
1826+
m: map[string]int{"a": 1, "b": 2},
1827+
keys: []string{"a", "x", "b", "y"},
1828+
want: []string{"a", "b"},
1829+
},
1830+
{
1831+
name: "none present",
1832+
m: map[string]int{"a": 1},
1833+
keys: []string{"x", "y"},
1834+
want: []string{},
1835+
},
1836+
{
1837+
name: "empty keys",
1838+
m: map[string]int{"a": 1},
1839+
keys: nil,
1840+
want: []string{},
1841+
},
1842+
{
1843+
name: "empty map",
1844+
m: map[string]int{},
1845+
keys: []string{"a"},
1846+
want: []string{},
1847+
},
1848+
}
1849+
1850+
for _, tc := range tests {
1851+
t.Run(tc.name, func(t *testing.T) {
1852+
got := KeysPresent(tc.m, tc.keys)
1853+
assert.Equal(t, tc.want, got)
1854+
})
1855+
}
1856+
}

db/crud.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2867,7 +2867,7 @@ func (db *DatabaseCollectionWithUser) DeleteDoc(ctx context.Context, docid strin
28672867

28682868
// Purges a document from the bucket (no tombstone)
28692869
func (db *DatabaseCollectionWithUser) Purge(ctx context.Context, key string, needsAudit bool) error {
2870-
doc, err := db.GetDocument(ctx, key, DocUnmarshalAll)
2870+
doc, rawBucketDoc, err := db.GetDocumentWithRaw(ctx, key, DocUnmarshalAll)
28712871
if err != nil {
28722872
return err
28732873
}
@@ -2892,8 +2892,15 @@ func (db *DatabaseCollectionWithUser) Purge(ctx context.Context, key string, nee
28922892
}
28932893

28942894
if db.UseXattrs() {
2895-
err := db.dataStore.DeleteWithXattrs(ctx, key, []string{base.SyncXattrName})
2896-
if err != nil {
2895+
// Clean up _sync and _globalSync (if present). Leave _vv and _mou since they are also shared by XDCR/Eventing.
2896+
xattrsToDelete := []string{base.SyncXattrName, base.GlobalXattrName}
2897+
// TODO: CBG-4796 - we currently need to determine a list of present xattrs before we delete to avoid differences
2898+
// between Rosmar and Couchbase Server implementations of DeleteWithXattrs and GetWithXattrs.
2899+
var presentXattrsToDelete []string
2900+
if rawBucketDoc != nil && rawBucketDoc.Xattrs != nil {
2901+
presentXattrsToDelete = base.KeysPresent(rawBucketDoc.Xattrs, xattrsToDelete)
2902+
}
2903+
if err := db.dataStore.DeleteWithXattrs(ctx, key, presentXattrsToDelete); err != nil {
28972904
return err
28982905
}
28992906
} else {

rest/adminapitest/admin_api_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package adminapitest
1010

1111
import (
12+
"encoding/base64"
1213
"encoding/json"
1314
"errors"
1415
"fmt"
@@ -1789,6 +1790,52 @@ func TestPurgeWithSomeInvalidDocs(t *testing.T) {
17891790
rest.RequireStatus(t, rt.SendAdminRequest("PUT", "/{{.keyspace}}/doc2", `{"moo":"car"}`), 409)
17901791
}
17911792

1793+
// TestPurgeWithOldAttachment ensures that purging a document with an attachment actually removes it and a recreated document does not have the old attachment.
1794+
func TestPurgeWithOldAttachment(t *testing.T) {
1795+
rt := rest.NewRestTester(t, nil)
1796+
defer rt.Close()
1797+
1798+
const att1 = "first attachment"
1799+
att1Data := base64.StdEncoding.EncodeToString([]byte(att1))
1800+
_ = rt.PutDocWithAttachment("doc1", `{"foo":"doc1"}`, "att1", att1Data)
1801+
1802+
rawBody, rawXattrs, _, err := rt.GetSingleDataStore().GetWithXattrs(t.Context(), "doc1", []string{base.SyncXattrName, base.GlobalXattrName})
1803+
require.NoError(t, err)
1804+
assert.NotNil(t, rawBody)
1805+
assert.NotNil(t, rawXattrs)
1806+
var globalSync db.GlobalSyncData
1807+
require.NoError(t, json.Unmarshal(rawXattrs[base.GlobalXattrName], &globalSync))
1808+
assert.Equal(t, len(att1), int(globalSync.Attachments["att1"].(map[string]any)["length"].(float64)))
1809+
1810+
response := rt.SendAdminRequest("POST", "/{{.keyspace}}/_purge", `{"doc1":["*"]}`)
1811+
rest.RequireStatus(t, response, http.StatusOK)
1812+
var body db.Body
1813+
require.NoError(t, base.JSONUnmarshal(response.Body.Bytes(), &body))
1814+
assert.Equal(t, db.Body{"purged": map[string]any{"doc1": []interface{}{"*"}}}, body)
1815+
1816+
// inspect bucket doc to ensure SG's xattrs are gone
1817+
rawBody, rawXattrs, _, err = rt.GetSingleDataStore().GetWithXattrs(t.Context(), "doc1", []string{base.SyncXattrName, base.GlobalXattrName})
1818+
assert.Error(t, err)
1819+
assert.True(t, base.IsDocNotFoundError(err))
1820+
assert.Nil(t, rawBody)
1821+
assert.Empty(t, rawXattrs)
1822+
1823+
// Overwriting the document here is intentional: after purging, we want to verify that re-inserting the document does not resurrect any previous attachments or metadata.
1824+
// This ensures the purge operation fully removed all traces of the original document, and that the new insert starts from a clean state.
1825+
const att2 = "att two"
1826+
att2Data := base64.StdEncoding.EncodeToString([]byte(att2))
1827+
_ = rt.PutDocWithAttachment("doc1", `{"foo":"doc1"}`, "att2", att2Data)
1828+
1829+
rawBody, rawXattrs, _, err = rt.GetSingleDataStore().GetWithXattrs(t.Context(), "doc1", []string{base.SyncXattrName, base.GlobalXattrName})
1830+
require.NoError(t, err)
1831+
assert.NotNil(t, rawBody)
1832+
assert.NotNil(t, rawXattrs)
1833+
globalSync = db.GlobalSyncData{}
1834+
require.NoError(t, json.Unmarshal(rawXattrs[base.GlobalXattrName], &globalSync))
1835+
assert.NotContains(t, globalSync.Attachments, "att1")
1836+
assert.Equal(t, len(att2), int(globalSync.Attachments["att2"].(map[string]any)["length"].(float64)))
1837+
}
1838+
17921839
// TestRawRedaction tests the /_raw endpoint with and without redaction
17931840
// intentionally does string matching on redactable strings to avoid any regressions if we move around metadata without updating the test
17941841
func TestRawRedaction(t *testing.T) {

0 commit comments

Comments
 (0)