Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CBG-3727: Diagnostic API: Get all doc channels #6701

Merged
merged 12 commits into from
Feb 27, 2024
14 changes: 10 additions & 4 deletions auth/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,19 @@ type GrantHistory struct {
// Struct is for ease of internal use
// Bucket store has each entry as a string "seq-endSeq"
type GrantHistorySequencePair struct {
StartSeq uint64 // Sequence at which a grant was performed to give access to a role / channel. Only populated once endSeq is available.
EndSeq uint64 // Sequence when access to a role / channel was revoked.
StartSeq uint64 // Sequence at which a grant was performed to give access to a role / channel. Only populated once endSeq is available.
EndSeq uint64 // Sequence when access to a role / channel was revoked.
Compacted bool
}

// MarshalJSON will handle conversion from having a seq / endSeq struct to the bucket format of "seq-endSeq"
// MarshalJSON will handle conversion from having a seq / endSeq struct to the bucket format of "seq-endSeq" and "seq~endSeq" if the pair was compacted
func (pair *GrantHistorySequencePair) MarshalJSON() ([]byte, error) {
stringPair := fmt.Sprintf("%d-%d", pair.StartSeq, pair.EndSeq)
var stringPair string
if pair.Compacted {
stringPair = fmt.Sprintf("%d~%d", pair.StartSeq, pair.EndSeq)
} else {
Comment on lines +70 to +72
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're doing this text representation in MarshalJSON, an equivalent should be put in UnmarshalJSON.

Right now the two aren't compatible, and will error when running the splitPair := strings.Split(stringPair, "-") step.

stringPair = fmt.Sprintf("%d-%d", pair.StartSeq, pair.EndSeq)
}
return base.JSONMarshal(stringPair)
}

Expand Down
8 changes: 5 additions & 3 deletions db/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ type UserAccessMap map[string]channels.TimedSet
type AttachmentsMeta map[string]interface{} // AttachmentsMeta metadata as included in sync metadata

type ChannelSetEntry struct {
Name string `json:"name"`
Start uint64 `json:"start"`
End uint64 `json:"end,omitempty"`
Name string `json:"name"`
Start uint64 `json:"start"`
End uint64 `json:"end,omitempty"`
Compacted bool `json:"compacted,omitempty"`
}

// The sync-gateway metadata stored in the "_sync" property of a Couchbase document.
Expand Down Expand Up @@ -930,6 +931,7 @@ func (doc *Document) addToChannelSetHistory(channelName string, historyEntry Cha

if entryCount >= DocumentHistoryMaxEntriesPerChannel {
doc.ChannelSetHistory[secondOldestEntryIdx].Start = oldestEntryStartSeq
doc.ChannelSetHistory[secondOldestEntryIdx].Compacted = true
doc.ChannelSetHistory = append(doc.ChannelSetHistory[:oldestEntryIdx], doc.ChannelSetHistory[oldestEntryIdx+1:]...)
}

Expand Down
2 changes: 2 additions & 0 deletions docs/api/diagnostic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ servers:
paths:
/_ping:
$ref: ./paths/common/_ping.yaml
'/{keyspace}/{docid}/_all_channels':
$ref: './paths/diagnostic/keyspace-docid-_all_channels.yaml'
externalDocs:
description: Sync Gateway Quickstart | Couchbase Docs
url: 'https://docs.couchbase.com/sync-gateway/current/index.html'
37 changes: 37 additions & 0 deletions docs/api/paths/diagnostic/keyspace-docid-_all_channels.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2022-Present Couchbase, Inc.
#
# Use of this software is governed by the Business Source License included
# in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
# in that file, in accordance with the Business Source License, use of this
# software will be governed by the Apache License, Version 2.0, included in
# the file licenses/APL2.txt.
parameters:
- $ref: ../../components/parameters.yaml#/keyspace
- $ref: ../../components/parameters.yaml#/docid
get:
summary: Get channel history for a document
description: |-
Retrieve all doc channels and the sequence spans showing when the doc was added to a channel and when it was removed.
Required Sync Gateway RBAC roles:
* Sync Gateway Application Read Only
responses:
'200':
description: Document found successfully
content:
application/json:
schema:
additionalProperties:
x-additionalPropertiesName: channel
description: The channels the document has been in.
type: array
items:
sequences:
description: The sequence number that document was added to the channel.
type: string
example: "28-48"

'404':
$ref: ../../components/responses.yaml#/Not-found
tags:
- Document
operationId: get_keyspace-docid-_all_channels
41 changes: 41 additions & 0 deletions rest/diagnostic_doc_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright 2024-Present Couchbase, Inc.

Use of this software is governed by the Business Source License included in
the file licenses/BSL-Couchbase.txt. As of the Change Date specified in that
file, in accordance with the Business Source License, use of this software will
be governed by the Apache License, Version 2.0, included in the file
licenses/APL2.txt.
*/

package rest

import (
"github.com/couchbase/sync_gateway/auth"
"github.com/couchbase/sync_gateway/db"
)

// HTTP handler for a GET of a document
func (h *handler) handleGetDocChannels() error {
docid := h.PathVar("docid")

doc, err := h.collection.GetDocument(h.ctx(), docid, db.DocUnmarshalSync)
if err != nil {
return err
}
if doc == nil {
return kNotFoundError
}
resp := make(map[string][]auth.GrantHistorySequencePair, len(doc.Channels))

for _, chanSetInfo := range doc.SyncData.ChannelSet {
resp[chanSetInfo.Name] = append(resp[chanSetInfo.Name], auth.GrantHistorySequencePair{StartSeq: chanSetInfo.Start, EndSeq: chanSetInfo.End})
}
for _, hist := range doc.SyncData.ChannelSetHistory {
resp[hist.Name] = append(resp[hist.Name], auth.GrantHistorySequencePair{StartSeq: hist.Start, EndSeq: hist.End, Compacted: hist.Compacted})
continue
}

h.writeJSON(resp)
return nil
}
59 changes: 59 additions & 0 deletions rest/diagnostic_doc_api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Copyright 2024-Present Couchbase, Inc.

Use of this software is governed by the Business Source License included in
the file licenses/BSL-Couchbase.txt. As of the Change Date specified in that
file, in accordance with the Business Source License, use of this software will
be governed by the Apache License, Version 2.0, included in the file
licenses/APL2.txt.
*/

package rest

import (
"encoding/json"
"net/http"
"testing"

"github.com/couchbase/sync_gateway/db"

"github.com/stretchr/testify/assert"
)

func TestGetAlldocChannels(t *testing.T) {
rt := NewRestTester(t, &RestTesterConfig{SyncFn: `function(doc) {channel(doc.channel);}`})
defer rt.Close()

version := rt.PutDoc("doc", `{"channel":["CHAN1"]}`)
updatedVersion := rt.UpdateDoc("doc", version, `{"channel":["CHAN2"]}`)
updatedVersion = rt.UpdateDoc("doc", updatedVersion, `{"channel":["CHAN1"]}`)
updatedVersion = rt.UpdateDoc("doc", updatedVersion, `{"channel":["CHAN1", "CHAN2"]}`)
updatedVersion = rt.UpdateDoc("doc", updatedVersion, `{"channel":["CHAN3"]}`)
updatedVersion = rt.UpdateDoc("doc", updatedVersion, `{"channel":["CHAN1"]}`)

response := rt.SendDiagnosticRequest("GET", "/{{.keyspace}}/doc/_all_channels", "")
RequireStatus(t, response, http.StatusOK)

var channelMap map[string][]string
err := json.Unmarshal(response.BodyBytes(), &channelMap)
assert.NoError(t, err)
assert.ElementsMatch(t, channelMap["CHAN1"], []string{"6-0", "1-2", "3-5"})
assert.ElementsMatch(t, channelMap["CHAN2"], []string{"4-5", "2-3"})
assert.ElementsMatch(t, channelMap["CHAN3"], []string{"5-6"})

for i := 1; i <= 10; i++ {
updatedVersion = rt.UpdateDoc("doc", updatedVersion, `{}`)
updatedVersion = rt.UpdateDoc("doc", updatedVersion, `{"channel":["CHAN3"]}`)
}
response = rt.SendAdminRequest("GET", "/{{.keyspace}}/doc", "")
RequireStatus(t, response, http.StatusOK)
response = rt.SendDiagnosticRequest("GET", "/{{.keyspace}}/doc/_all_channels", "")
RequireStatus(t, response, http.StatusOK)

err = json.Unmarshal(response.BodyBytes(), &channelMap)
assert.NoError(t, err)

// If the channel is still in channel_set, then the total will be 5 entries in history and 1 in channel_set
assert.Equal(t, len(channelMap["CHAN3"]), db.DocumentHistoryMaxEntriesPerChannel+1)

}
6 changes: 5 additions & 1 deletion rest/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,11 @@ func CreateMetricRouter(sc *ServerContext) *mux.Router {

func createDiagnosticRouter(sc *ServerContext) *mux.Router {
r := CreatePingRouter(sc)

dbr := r.PathPrefix("/{db:" + dbRegex + "}/").Subrouter()
dbr.StrictSlash(true)
keyspace := r.PathPrefix("/{keyspace:" + dbRegex + "}/").Subrouter()
keyspace.StrictSlash(true)
keyspace.Handle("/{docid:"+docRegex+"}/_all_channels", makeHandler(sc, adminPrivs, []Permission{PermReadAppData}, nil, (*handler).handleGetDocChannels)).Methods("GET")
return r
}

Expand Down
Loading