1041 lines
27 KiB
Go
1041 lines
27 KiB
Go
package gocb
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
|
|
gocbcore "github.com/couchbase/gocbcore/v9"
|
|
)
|
|
|
|
type kvProvider interface {
|
|
Add(opts gocbcore.AddOptions, cb gocbcore.StoreCallback) (gocbcore.PendingOp, error)
|
|
Set(opts gocbcore.SetOptions, cb gocbcore.StoreCallback) (gocbcore.PendingOp, error)
|
|
Replace(opts gocbcore.ReplaceOptions, cb gocbcore.StoreCallback) (gocbcore.PendingOp, error)
|
|
Get(opts gocbcore.GetOptions, cb gocbcore.GetCallback) (gocbcore.PendingOp, error)
|
|
GetOneReplica(opts gocbcore.GetOneReplicaOptions, cb gocbcore.GetReplicaCallback) (gocbcore.PendingOp, error)
|
|
Observe(opts gocbcore.ObserveOptions, cb gocbcore.ObserveCallback) (gocbcore.PendingOp, error)
|
|
ObserveVb(opts gocbcore.ObserveVbOptions, cb gocbcore.ObserveVbCallback) (gocbcore.PendingOp, error)
|
|
GetMeta(opts gocbcore.GetMetaOptions, cb gocbcore.GetMetaCallback) (gocbcore.PendingOp, error)
|
|
Delete(opts gocbcore.DeleteOptions, cb gocbcore.DeleteCallback) (gocbcore.PendingOp, error)
|
|
LookupIn(opts gocbcore.LookupInOptions, cb gocbcore.LookupInCallback) (gocbcore.PendingOp, error)
|
|
MutateIn(opts gocbcore.MutateInOptions, cb gocbcore.MutateInCallback) (gocbcore.PendingOp, error)
|
|
GetAndTouch(opts gocbcore.GetAndTouchOptions, cb gocbcore.GetAndTouchCallback) (gocbcore.PendingOp, error)
|
|
GetAndLock(opts gocbcore.GetAndLockOptions, cb gocbcore.GetAndLockCallback) (gocbcore.PendingOp, error)
|
|
Unlock(opts gocbcore.UnlockOptions, cb gocbcore.UnlockCallback) (gocbcore.PendingOp, error)
|
|
Touch(opts gocbcore.TouchOptions, cb gocbcore.TouchCallback) (gocbcore.PendingOp, error)
|
|
Increment(opts gocbcore.CounterOptions, cb gocbcore.CounterCallback) (gocbcore.PendingOp, error)
|
|
Decrement(opts gocbcore.CounterOptions, cb gocbcore.CounterCallback) (gocbcore.PendingOp, error)
|
|
Append(opts gocbcore.AdjoinOptions, cb gocbcore.AdjoinCallback) (gocbcore.PendingOp, error)
|
|
Prepend(opts gocbcore.AdjoinOptions, cb gocbcore.AdjoinCallback) (gocbcore.PendingOp, error)
|
|
ConfigSnapshot() (*gocbcore.ConfigSnapshot, error)
|
|
}
|
|
|
|
// Cas represents the specific state of a document on the cluster.
|
|
type Cas gocbcore.Cas
|
|
|
|
// InsertOptions are options that can be applied to an Insert operation.
|
|
type InsertOptions struct {
|
|
Expiry time.Duration
|
|
PersistTo uint
|
|
ReplicateTo uint
|
|
DurabilityLevel DurabilityLevel
|
|
Transcoder Transcoder
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// Insert creates a new document in the Collection.
|
|
func (c *Collection) Insert(id string, val interface{}, opts *InsertOptions) (mutOut *MutationResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &InsertOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("Insert", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetTranscoder(opts.Transcoder)
|
|
opm.SetValue(val)
|
|
opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = opm.Wait(agent.Add(gocbcore.AddOptions{
|
|
Key: opm.DocumentID(),
|
|
Value: opm.ValueBytes(),
|
|
Flags: opm.ValueFlags(),
|
|
Expiry: durationToExpiry(opts.Expiry),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
DurabilityLevel: opm.DurabilityLevel(),
|
|
DurabilityLevelTimeout: opm.DurabilityTimeout(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.StoreResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
mutOut = &MutationResult{}
|
|
mutOut.cas = Cas(res.Cas)
|
|
mutOut.mt = opm.EnhanceMt(res.MutationToken)
|
|
|
|
opm.Resolve(mutOut.mt)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
// UpsertOptions are options that can be applied to an Upsert operation.
|
|
type UpsertOptions struct {
|
|
Expiry time.Duration
|
|
PersistTo uint
|
|
ReplicateTo uint
|
|
DurabilityLevel DurabilityLevel
|
|
Transcoder Transcoder
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// Upsert creates a new document in the Collection if it does not exist, if it does exist then it updates it.
|
|
func (c *Collection) Upsert(id string, val interface{}, opts *UpsertOptions) (mutOut *MutationResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &UpsertOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("Upsert", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetTranscoder(opts.Transcoder)
|
|
opm.SetValue(val)
|
|
opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = opm.Wait(agent.Set(gocbcore.SetOptions{
|
|
Key: opm.DocumentID(),
|
|
Value: opm.ValueBytes(),
|
|
Flags: opm.ValueFlags(),
|
|
Expiry: durationToExpiry(opts.Expiry),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
DurabilityLevel: opm.DurabilityLevel(),
|
|
DurabilityLevelTimeout: opm.DurabilityTimeout(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.StoreResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
mutOut = &MutationResult{}
|
|
mutOut.cas = Cas(res.Cas)
|
|
mutOut.mt = opm.EnhanceMt(res.MutationToken)
|
|
|
|
opm.Resolve(mutOut.mt)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
// ReplaceOptions are the options available to a Replace operation.
|
|
type ReplaceOptions struct {
|
|
Expiry time.Duration
|
|
Cas Cas
|
|
PersistTo uint
|
|
ReplicateTo uint
|
|
DurabilityLevel DurabilityLevel
|
|
Transcoder Transcoder
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// Replace updates a document in the collection.
|
|
func (c *Collection) Replace(id string, val interface{}, opts *ReplaceOptions) (mutOut *MutationResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &ReplaceOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("Replace", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetTranscoder(opts.Transcoder)
|
|
opm.SetValue(val)
|
|
opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = opm.Wait(agent.Replace(gocbcore.ReplaceOptions{
|
|
Key: opm.DocumentID(),
|
|
Value: opm.ValueBytes(),
|
|
Flags: opm.ValueFlags(),
|
|
Expiry: durationToExpiry(opts.Expiry),
|
|
Cas: gocbcore.Cas(opts.Cas),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
DurabilityLevel: opm.DurabilityLevel(),
|
|
DurabilityLevelTimeout: opm.DurabilityTimeout(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.StoreResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
mutOut = &MutationResult{}
|
|
mutOut.cas = Cas(res.Cas)
|
|
mutOut.mt = opm.EnhanceMt(res.MutationToken)
|
|
|
|
opm.Resolve(mutOut.mt)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetOptions are the options available to a Get operation.
|
|
type GetOptions struct {
|
|
WithExpiry bool
|
|
// Project causes the Get operation to only fetch the fields indicated
|
|
// by the paths. The result of the operation is then treated as a
|
|
// standard GetResult.
|
|
Project []string
|
|
Transcoder Transcoder
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// Get performs a fetch operation against the collection. This can take 3 paths, a standard full document
|
|
// fetch, a subdocument full document fetch also fetching document expiry (when WithExpiry is set),
|
|
// or a subdocument fetch (when Project is used).
|
|
func (c *Collection) Get(id string, opts *GetOptions) (docOut *GetResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &GetOptions{}
|
|
}
|
|
|
|
if len(opts.Project) == 0 && !opts.WithExpiry {
|
|
return c.getDirect(id, opts)
|
|
}
|
|
|
|
return c.getProjected(id, opts)
|
|
}
|
|
|
|
func (c *Collection) getDirect(id string, opts *GetOptions) (docOut *GetResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &GetOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("Get", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetTranscoder(opts.Transcoder)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = opm.Wait(agent.Get(gocbcore.GetOptions{
|
|
Key: opm.DocumentID(),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.GetResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
doc := &GetResult{
|
|
Result: Result{
|
|
cas: Cas(res.Cas),
|
|
},
|
|
transcoder: opm.Transcoder(),
|
|
contents: res.Value,
|
|
flags: res.Flags,
|
|
}
|
|
|
|
docOut = doc
|
|
|
|
opm.Resolve(nil)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *Collection) getProjected(id string, opts *GetOptions) (docOut *GetResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &GetOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("Get", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetTranscoder(opts.Transcoder)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if opts.Transcoder != nil {
|
|
return nil, errors.New("Cannot specify custom transcoder for projected gets")
|
|
}
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
numProjects := len(opts.Project)
|
|
if opts.WithExpiry {
|
|
numProjects = 1 + numProjects
|
|
}
|
|
|
|
projections := opts.Project
|
|
if numProjects > 16 {
|
|
projections = nil
|
|
}
|
|
|
|
var ops []LookupInSpec
|
|
|
|
if opts.WithExpiry {
|
|
ops = append(ops, GetSpec("$document.exptime", &GetSpecOptions{IsXattr: true}))
|
|
}
|
|
|
|
if len(projections) == 0 {
|
|
ops = append(ops, GetSpec("", nil))
|
|
} else {
|
|
for _, path := range projections {
|
|
ops = append(ops, GetSpec(path, nil))
|
|
}
|
|
}
|
|
|
|
result, err := c.internalLookupIn(opm, ops, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
doc := &GetResult{}
|
|
if opts.WithExpiry {
|
|
// if expiration was requested then extract and remove it from the results
|
|
err = result.ContentAt(0, &doc.expiry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ops = ops[1:]
|
|
result.contents = result.contents[1:]
|
|
}
|
|
|
|
doc.transcoder = opm.Transcoder()
|
|
doc.cas = result.cas
|
|
if projections == nil {
|
|
err = doc.fromFullProjection(ops, result, opts.Project)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
err = doc.fromSubDoc(ops, result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return doc, nil
|
|
}
|
|
|
|
// ExistsOptions are the options available to the Exists command.
|
|
type ExistsOptions struct {
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// Exists checks if a document exists for the given id.
|
|
func (c *Collection) Exists(id string, opts *ExistsOptions) (docOut *ExistsResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &ExistsOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("Exists", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = opm.Wait(agent.GetMeta(gocbcore.GetMetaOptions{
|
|
Key: opm.DocumentID(),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan,
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.GetMetaResult, err error) {
|
|
if errors.Is(err, ErrDocumentNotFound) {
|
|
docOut = &ExistsResult{
|
|
Result: Result{
|
|
cas: Cas(0),
|
|
},
|
|
docExists: false,
|
|
}
|
|
opm.Resolve(nil)
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
if res != nil {
|
|
docOut = &ExistsResult{
|
|
Result: Result{
|
|
cas: Cas(res.Cas),
|
|
},
|
|
docExists: res.Deleted == 0,
|
|
}
|
|
}
|
|
|
|
opm.Resolve(nil)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *Collection) getOneReplica(
|
|
span requestSpanContext,
|
|
id string,
|
|
replicaIdx int,
|
|
transcoder Transcoder,
|
|
retryStrategy RetryStrategy,
|
|
cancelCh chan struct{},
|
|
timeout time.Duration,
|
|
) (docOut *GetReplicaResult, errOut error) {
|
|
opm := c.newKvOpManager("getOneReplica", span)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetTranscoder(transcoder)
|
|
opm.SetRetryStrategy(retryStrategy)
|
|
opm.SetTimeout(timeout)
|
|
opm.SetCancelCh(cancelCh)
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if replicaIdx == 0 {
|
|
err = opm.Wait(agent.Get(gocbcore.GetOptions{
|
|
Key: opm.DocumentID(),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.GetResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
docOut = &GetReplicaResult{}
|
|
docOut.cas = Cas(res.Cas)
|
|
docOut.transcoder = opm.Transcoder()
|
|
docOut.contents = res.Value
|
|
docOut.flags = res.Flags
|
|
docOut.isReplica = false
|
|
|
|
opm.Resolve(nil)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
err = opm.Wait(agent.GetOneReplica(gocbcore.GetOneReplicaOptions{
|
|
Key: opm.DocumentID(),
|
|
ReplicaIdx: replicaIdx,
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.GetReplicaResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
docOut = &GetReplicaResult{}
|
|
docOut.cas = Cas(res.Cas)
|
|
docOut.transcoder = opm.Transcoder()
|
|
docOut.contents = res.Value
|
|
docOut.flags = res.Flags
|
|
docOut.isReplica = true
|
|
|
|
opm.Resolve(nil)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetAllReplicaOptions are the options available to the GetAllReplicas command.
|
|
type GetAllReplicaOptions struct {
|
|
Transcoder Transcoder
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// GetAllReplicasResult represents the results of a GetAllReplicas operation.
|
|
type GetAllReplicasResult struct {
|
|
lock sync.Mutex
|
|
totalRequests uint32
|
|
totalResults uint32
|
|
resCh chan *GetReplicaResult
|
|
cancelCh chan struct{}
|
|
}
|
|
|
|
func (r *GetAllReplicasResult) addResult(res *GetReplicaResult) {
|
|
// We use a lock here because the alternative means that there is a race
|
|
// between the channel writes from multiple results and the channels being
|
|
// closed. IE: T1-Incr, T2-Incr, T2-Send, T2-Close, T1-Send[PANIC]
|
|
r.lock.Lock()
|
|
|
|
r.totalResults++
|
|
resultCount := r.totalResults
|
|
|
|
if resultCount <= r.totalRequests {
|
|
r.resCh <- res
|
|
}
|
|
|
|
if resultCount == r.totalRequests {
|
|
close(r.cancelCh)
|
|
close(r.resCh)
|
|
}
|
|
|
|
r.lock.Unlock()
|
|
}
|
|
|
|
// Next fetches the next replica result.
|
|
func (r *GetAllReplicasResult) Next() *GetReplicaResult {
|
|
return <-r.resCh
|
|
}
|
|
|
|
// Close cancels all remaining get replica requests.
|
|
func (r *GetAllReplicasResult) Close() error {
|
|
// See addResult discussion on lock usage.
|
|
r.lock.Lock()
|
|
|
|
// Note that this number increment must be high enough to be clear that
|
|
// the result set was closed, but low enough that it won't overflow if
|
|
// additional result objects are processed after the close.
|
|
prevResultCount := r.totalResults
|
|
r.totalResults += 100000
|
|
|
|
// We only have to close everything if the addResult method didn't already
|
|
// close them due to already having completed every request
|
|
if prevResultCount < r.totalRequests {
|
|
close(r.cancelCh)
|
|
close(r.resCh)
|
|
}
|
|
|
|
r.lock.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAllReplicas returns the value of a particular document from all replica servers. This will return an iterable
|
|
// which streams results one at a time.
|
|
func (c *Collection) GetAllReplicas(id string, opts *GetAllReplicaOptions) (docOut *GetAllReplicasResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &GetAllReplicaOptions{}
|
|
}
|
|
|
|
span := c.startKvOpTrace("GetAllReplicas", nil)
|
|
defer span.Finish()
|
|
|
|
// Timeout needs to be adjusted here, since we use it at the bottom of this
|
|
// function, but the remaining options are all passed downwards and get handled
|
|
// by those functions rather than us.
|
|
timeout := opts.Timeout
|
|
if timeout == 0 {
|
|
timeout = c.timeoutsConfig.KVTimeout
|
|
}
|
|
|
|
deadline := time.Now().Add(timeout)
|
|
transcoder := opts.Transcoder
|
|
retryStrategy := opts.RetryStrategy
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
snapshot, err := agent.ConfigSnapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
numReplicas, err := snapshot.NumReplicas()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
numServers := numReplicas + 1
|
|
outCh := make(chan *GetReplicaResult, numServers)
|
|
cancelCh := make(chan struct{})
|
|
|
|
repRes := &GetAllReplicasResult{
|
|
totalRequests: uint32(numServers),
|
|
resCh: outCh,
|
|
cancelCh: cancelCh,
|
|
}
|
|
|
|
// Loop all the servers and populate the result object
|
|
for replicaIdx := 0; replicaIdx < numServers; replicaIdx++ {
|
|
go func(replicaIdx int) {
|
|
// This timeout value will cause the getOneReplica operation to timeout after our deadline has expired,
|
|
// as the deadline has already begun. getOneReplica timing out before our deadline would cause inconsistent
|
|
// behaviour.
|
|
res, err := c.getOneReplica(span, id, replicaIdx, transcoder, retryStrategy, cancelCh, timeout)
|
|
if err != nil {
|
|
logDebugf("Failed to fetch replica from replica %d: %s", replicaIdx, err)
|
|
} else {
|
|
repRes.addResult(res)
|
|
}
|
|
}(replicaIdx)
|
|
}
|
|
|
|
// Start a timer to close it after the deadline
|
|
go func() {
|
|
select {
|
|
case <-time.After(time.Until(deadline)):
|
|
// If we timeout, we should close the result
|
|
err := repRes.Close()
|
|
if err != nil {
|
|
logDebugf("failed to close GetAllReplicas response: %s", err)
|
|
}
|
|
return
|
|
case <-cancelCh:
|
|
// If the cancel channel closes, we are done
|
|
return
|
|
}
|
|
}()
|
|
|
|
return repRes, nil
|
|
}
|
|
|
|
// GetAnyReplicaOptions are the options available to the GetAnyReplica command.
|
|
type GetAnyReplicaOptions struct {
|
|
Transcoder Transcoder
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// GetAnyReplica returns the value of a particular document from a replica server.
|
|
func (c *Collection) GetAnyReplica(id string, opts *GetAnyReplicaOptions) (docOut *GetReplicaResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &GetAnyReplicaOptions{}
|
|
}
|
|
|
|
span := c.startKvOpTrace("GetAnyReplica", nil)
|
|
defer span.Finish()
|
|
|
|
repRes, err := c.GetAllReplicas(id, &GetAllReplicaOptions{
|
|
Timeout: opts.Timeout,
|
|
Transcoder: opts.Transcoder,
|
|
RetryStrategy: opts.RetryStrategy,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Try to fetch at least one result
|
|
res := repRes.Next()
|
|
if res == nil {
|
|
return nil, &KeyValueError{
|
|
InnerError: ErrDocumentUnretrievable,
|
|
BucketName: c.bucketName(),
|
|
ScopeName: c.scope,
|
|
CollectionName: c.collectionName,
|
|
}
|
|
}
|
|
|
|
// Close the results channel since we don't care about any of the
|
|
// remaining result objects at this point.
|
|
err = repRes.Close()
|
|
if err != nil {
|
|
logDebugf("failed to close GetAnyReplica response: %s", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// RemoveOptions are the options available to the Remove command.
|
|
type RemoveOptions struct {
|
|
Cas Cas
|
|
PersistTo uint
|
|
ReplicateTo uint
|
|
DurabilityLevel DurabilityLevel
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// Remove removes a document from the collection.
|
|
func (c *Collection) Remove(id string, opts *RemoveOptions) (mutOut *MutationResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &RemoveOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("Remove", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetDuraOptions(opts.PersistTo, opts.ReplicateTo, opts.DurabilityLevel)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = opm.Wait(agent.Delete(gocbcore.DeleteOptions{
|
|
Key: opm.DocumentID(),
|
|
Cas: gocbcore.Cas(opts.Cas),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
DurabilityLevel: opm.DurabilityLevel(),
|
|
DurabilityLevelTimeout: opm.DurabilityTimeout(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.DeleteResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
mutOut = &MutationResult{}
|
|
mutOut.cas = Cas(res.Cas)
|
|
mutOut.mt = opm.EnhanceMt(res.MutationToken)
|
|
|
|
opm.Resolve(mutOut.mt)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetAndTouchOptions are the options available to the GetAndTouch operation.
|
|
type GetAndTouchOptions struct {
|
|
Transcoder Transcoder
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// GetAndTouch retrieves a document and simultaneously updates its expiry time.
|
|
func (c *Collection) GetAndTouch(id string, expiry time.Duration, opts *GetAndTouchOptions) (docOut *GetResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &GetAndTouchOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("GetAndTouch", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetTranscoder(opts.Transcoder)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = opm.Wait(agent.GetAndTouch(gocbcore.GetAndTouchOptions{
|
|
Key: opm.DocumentID(),
|
|
Expiry: durationToExpiry(expiry),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.GetAndTouchResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
if res != nil {
|
|
doc := &GetResult{
|
|
Result: Result{
|
|
cas: Cas(res.Cas),
|
|
},
|
|
transcoder: opm.Transcoder(),
|
|
contents: res.Value,
|
|
flags: res.Flags,
|
|
}
|
|
|
|
docOut = doc
|
|
}
|
|
|
|
opm.Resolve(nil)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetAndLockOptions are the options available to the GetAndLock operation.
|
|
type GetAndLockOptions struct {
|
|
Transcoder Transcoder
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// GetAndLock locks a document for a period of time, providing exclusive RW access to it.
|
|
// A lockTime value of over 30 seconds will be treated as 30 seconds. The resolution used to send this value to
|
|
// the server is seconds and is calculated using uint32(lockTime/time.Second).
|
|
func (c *Collection) GetAndLock(id string, lockTime time.Duration, opts *GetAndLockOptions) (docOut *GetResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &GetAndLockOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("GetAndLock", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetTranscoder(opts.Transcoder)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = opm.Wait(agent.GetAndLock(gocbcore.GetAndLockOptions{
|
|
Key: opm.DocumentID(),
|
|
LockTime: uint32(lockTime / time.Second),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.GetAndLockResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
if res != nil {
|
|
doc := &GetResult{
|
|
Result: Result{
|
|
cas: Cas(res.Cas),
|
|
},
|
|
transcoder: opm.Transcoder(),
|
|
contents: res.Value,
|
|
flags: res.Flags,
|
|
}
|
|
|
|
docOut = doc
|
|
}
|
|
|
|
opm.Resolve(nil)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
// UnlockOptions are the options available to the GetAndLock operation.
|
|
type UnlockOptions struct {
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// Unlock unlocks a document which was locked with GetAndLock.
|
|
func (c *Collection) Unlock(id string, cas Cas, opts *UnlockOptions) (errOut error) {
|
|
if opts == nil {
|
|
opts = &UnlockOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("Unlock", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = opm.Wait(agent.Unlock(gocbcore.UnlockOptions{
|
|
Key: opm.DocumentID(),
|
|
Cas: gocbcore.Cas(cas),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.UnlockResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
mt := opm.EnhanceMt(res.MutationToken)
|
|
opm.Resolve(mt)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
// TouchOptions are the options available to the Touch operation.
|
|
type TouchOptions struct {
|
|
Timeout time.Duration
|
|
RetryStrategy RetryStrategy
|
|
}
|
|
|
|
// Touch touches a document, specifying a new expiry time for it.
|
|
func (c *Collection) Touch(id string, expiry time.Duration, opts *TouchOptions) (mutOut *MutationResult, errOut error) {
|
|
if opts == nil {
|
|
opts = &TouchOptions{}
|
|
}
|
|
|
|
opm := c.newKvOpManager("Touch", nil)
|
|
defer opm.Finish()
|
|
|
|
opm.SetDocumentID(id)
|
|
opm.SetRetryStrategy(opts.RetryStrategy)
|
|
opm.SetTimeout(opts.Timeout)
|
|
|
|
if err := opm.CheckReadyForOp(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agent, err := c.getKvProvider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = opm.Wait(agent.Touch(gocbcore.TouchOptions{
|
|
Key: opm.DocumentID(),
|
|
Expiry: durationToExpiry(expiry),
|
|
CollectionName: opm.CollectionName(),
|
|
ScopeName: opm.ScopeName(),
|
|
RetryStrategy: opm.RetryStrategy(),
|
|
TraceContext: opm.TraceSpan(),
|
|
Deadline: opm.Deadline(),
|
|
}, func(res *gocbcore.TouchResult, err error) {
|
|
if err != nil {
|
|
errOut = opm.EnhanceErr(err)
|
|
opm.Reject()
|
|
return
|
|
}
|
|
|
|
mutOut = &MutationResult{}
|
|
mutOut.cas = Cas(res.Cas)
|
|
mutOut.mt = opm.EnhanceMt(res.MutationToken)
|
|
|
|
opm.Resolve(mutOut.mt)
|
|
}))
|
|
if err != nil {
|
|
errOut = err
|
|
}
|
|
return
|
|
}
|
|
|
|
// Binary creates and returns a BinaryCollection object.
|
|
func (c *Collection) Binary() *BinaryCollection {
|
|
return &BinaryCollection{collection: c}
|
|
}
|