Skip to content

Commit

Permalink
ING-907: Added support for locking operations to Data API.
Browse files Browse the repository at this point in the history
  • Loading branch information
Brett Lawson committed Sep 24, 2024
1 parent 74879c8 commit c867666
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 0 deletions.
88 changes: 88 additions & 0 deletions dataapiv1/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,92 @@ paths:
$ref: '#/components/responses/ServiceUnavailable'
'504':
$ref: '#/components/responses/GatewayTimeout'
'/v1.alpha/buckets/{bucketName}/scopes/{scopeName}/collections/{collectionName}/documents/{documentKey}/lock':
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- $ref: '#/components/parameters/BucketName'
- $ref: '#/components/parameters/ScopeName'
- $ref: '#/components/parameters/CollectionName'
- $ref: '#/components/parameters/DocumentKey'
post:
operationId: lockDocument
tags:
- Locking Operations
parameters:
- $ref: '#/components/parameters/AcceptEncodingHeader'
requestBody:
required: true
content:
'application/json':
schema:
type: object
properties:
lockTime:
description: The maximum period of time the document should remain locked.
type: integer
format: uint32
responses:
'200':
description: Successful locked the document
headers:
Content-Encoding:
$ref: "#/components/headers/ContentEncoding"
ETag:
$ref: "#/components/headers/ETag"
X-CB-Flags:
$ref: "#/components/headers/DocumentFlags"
content:
'*':
schema:
type: string
format: binary
'400':
$ref: '#/components/responses/BadRequest'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'500':
$ref: '#/components/responses/InternalServerError'
'503':
$ref: '#/components/responses/ServiceUnavailable'
'504':
$ref: '#/components/responses/GatewayTimeout'
'/v1.alpha/buckets/{bucketName}/scopes/{scopeName}/collections/{collectionName}/documents/{documentKey}/unlock':
parameters:
- $ref: '#/components/parameters/AuthorizationHeader'
- $ref: '#/components/parameters/BucketName'
- $ref: '#/components/parameters/ScopeName'
- $ref: '#/components/parameters/CollectionName'
- $ref: '#/components/parameters/DocumentKey'
post:
operationId: unlockDocument
tags:
- Binary Operations
parameters:
- $ref: '#/components/parameters/IfMatchHeader'
responses:
'200':
description: Successful unlocked the document.
headers:
X-CB-MutationToken:
$ref: "#/components/headers/MutationToken"
'400':
$ref: '#/components/responses/BadRequest'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'500':
$ref: '#/components/responses/InternalServerError'
'503':
$ref: '#/components/responses/ServiceUnavailable'
'504':
$ref: '#/components/responses/GatewayTimeout'
components:
securitySchemes:
BasicAuth:
Expand Down Expand Up @@ -461,6 +547,7 @@ components:
- DocumentExists
- CasMismatch
- DocumentLocked
- DocumentNotLocked
- ValueTooLarge
- DocumentNotJson
- PathMismatch
Expand All @@ -481,6 +568,7 @@ components:
- ErrorCodeDocumentExists
- ErrorCodeCasMismatch
- ErrorCodeDocumentLocked
- ErrorCodeDocumentNotLocked
- ErrorCodeValueTooLarge
- ErrorCodeDocumentNotJson
- ErrorCodePathMismatch
Expand Down
123 changes: 123 additions & 0 deletions gateway/dapiimpl/server_v1/dataapi_locking.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package server_v1

import (
"bytes"
"context"
"errors"

"github.com/couchbase/gocbcorex"
"github.com/couchbase/gocbcorex/memdx"
"github.com/couchbase/stellar-gateway/dataapiv1"
)

func (s *DataApiServer) LockDocument(
ctx context.Context, in dataapiv1.LockDocumentRequestObject,
) (dataapiv1.LockDocumentResponseObject, error) {
bucketAgent, oboUser, errSt := s.authHandler.GetMemdOboAgent(ctx, in.Params.Authorization, in.BucketName)
if errSt != nil {
return nil, errSt.Err()
}

key, errSt := s.parseKey(in.DocumentKey)
if errSt != nil {
return nil, errSt.Err()
}

var opts gocbcorex.GetAndLockOptions
opts.OnBehalfOf = oboUser
opts.ScopeName = in.ScopeName
opts.CollectionName = in.CollectionName
opts.Key = key

if in.Body.LockTime != nil {
opts.LockTime = *in.Body.LockTime
}

result, err := bucketAgent.GetAndLock(ctx, &opts)
if err != nil {
if errors.Is(err, memdx.ErrDocLocked) {
return nil, s.errorHandler.NewDocLockedStatus(err, in.BucketName, in.ScopeName, in.CollectionName, in.DocumentKey).Err()
} else if errors.Is(err, memdx.ErrDocNotFound) {
return nil, s.errorHandler.NewDocMissingStatus(err, in.BucketName, in.ScopeName, in.CollectionName, in.DocumentKey).Err()
} else if errors.Is(err, memdx.ErrUnknownCollectionName) {
return nil, s.errorHandler.NewCollectionMissingStatus(err, in.BucketName, in.ScopeName, in.CollectionName).Err()
} else if errors.Is(err, memdx.ErrUnknownScopeName) {
return nil, s.errorHandler.NewScopeMissingStatus(err, in.BucketName, in.ScopeName).Err()
} else if errors.Is(err, memdx.ErrAccessError) {
return nil, s.errorHandler.NewCollectionNoWriteAccessStatus(err, in.BucketName, in.ScopeName, in.CollectionName).Err()
}
return nil, s.errorHandler.NewGenericStatus(err).Err()
}

resp := dataapiv1.LockDocument200AsteriskResponse{
Headers: dataapiv1.LockDocument200ResponseHeaders{
ETag: casToHttpEtag(result.Cas),
XCBFlags: uint32(result.Flags),
},
}

contentType := flagsToHttpContentType(result.Flags)

contentEncoding, respValue, errSt :=
CompressHandler{}.MaybeCompressContent(result.Value, 0, in.Params.AcceptEncoding)
if errSt != nil {
return nil, errSt.Err()
}

resp.ContentType = contentType
resp.Headers.ContentEncoding = contentEncoding
resp.Body = bytes.NewReader(respValue)
resp.ContentLength = int64(len(respValue))

return resp, nil
}

func (s *DataApiServer) UnlockDocument(
ctx context.Context, in dataapiv1.UnlockDocumentRequestObject,
) (dataapiv1.UnlockDocumentResponseObject, error) {
bucketAgent, oboUser, errSt := s.authHandler.GetMemdOboAgent(ctx, in.Params.Authorization, in.BucketName)
if errSt != nil {
return nil, errSt.Err()
}

key, errSt := s.parseKey(in.DocumentKey)
if errSt != nil {
return nil, errSt.Err()
}

cas, errSt := s.parseCAS(in.Params.IfMatch)
if errSt != nil {
return nil, errSt.Err()
}

var opts gocbcorex.UnlockOptions
opts.OnBehalfOf = oboUser
opts.ScopeName = in.ScopeName
opts.CollectionName = in.CollectionName
opts.Key = key
opts.Cas = cas

result, err := bucketAgent.Unlock(ctx, &opts)
if err != nil {
if errors.Is(err, memdx.ErrCasMismatch) {
return nil, s.errorHandler.NewDocCasMismatchStatus(err, in.BucketName, in.ScopeName, in.CollectionName, in.DocumentKey).Err()
} else if errors.Is(err, memdx.ErrDocNotLocked) {
return nil, s.errorHandler.NewDocNotLockedStatus(err, in.BucketName, in.ScopeName, in.CollectionName, in.DocumentKey).Err()
} else if errors.Is(err, memdx.ErrDocNotFound) {
return nil, s.errorHandler.NewDocMissingStatus(err, in.BucketName, in.ScopeName, in.CollectionName, in.DocumentKey).Err()
} else if errors.Is(err, memdx.ErrUnknownCollectionName) {
return nil, s.errorHandler.NewCollectionMissingStatus(err, in.BucketName, in.ScopeName, in.CollectionName).Err()
} else if errors.Is(err, memdx.ErrUnknownScopeName) {
return nil, s.errorHandler.NewScopeMissingStatus(err, in.BucketName, in.ScopeName).Err()
} else if errors.Is(err, memdx.ErrAccessError) {
return nil, s.errorHandler.NewCollectionNoWriteAccessStatus(err, in.BucketName, in.ScopeName, in.CollectionName).Err()
}
return nil, s.errorHandler.NewGenericStatus(err).Err()
}

return dataapiv1.UnlockDocument200Response{
Headers: dataapiv1.UnlockDocument200ResponseHeaders{
XCBMutationToken: tokenFromGocbcorex(in.BucketName, result.MutationToken),
},
}, nil
}
13 changes: 13 additions & 0 deletions gateway/dapiimpl/server_v1/errorhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,19 @@ func (e ErrorHandler) NewDocLockedStatus(baseErr error, bucketName, scopeName, c
return st
}

func (e ErrorHandler) NewDocNotLockedStatus(baseErr error, bucketName, scopeName, collectionName, docId string) *Status {
st := &Status{
StatusCode: http.StatusBadRequest,
Code: dataapiv1.ErrorCodeDocumentNotLocked,
Message: fmt.Sprintf("Cannot unlock an unlocked document '%s' in '%s/%s/%s'.",
docId, bucketName, scopeName, collectionName),
Resource: fmt.Sprintf("/buckets/%s/scopes/%s/collections/%s/documents/%s",
bucketName, scopeName, collectionName, docId),
}
st = e.tryAttachExtraContext(st, baseErr)
return st
}

func (e ErrorHandler) NewDocExistsStatus(baseErr error, bucketName, scopeName, collectionName, docId string) *Status {
st := &Status{
StatusCode: http.StatusConflict,
Expand Down

0 comments on commit c867666

Please sign in to comment.